xdot.py
Go to the documentation of this file.
00001 #!/usr/bin/env python
00002 #
00003 # Copyright 2008 Jose Fonseca
00004 #
00005 # This program is free software: you can redistribute it and/or modify it
00006 # under the terms of the GNU Lesser General Public License as published
00007 # by the Free Software Foundation, either version 3 of the License, or
00008 # (at your option) any later version.
00009 #
00010 # This program is distributed in the hope that it will be useful,
00011 # but WITHOUT ANY WARRANTY; without even the implied warranty of
00012 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
00013 # GNU Lesser General Public License for more details.
00014 #
00015 # You should have received a copy of the GNU Lesser General Public License
00016 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
00017 #
00018 
00019 '''Visualize dot graphs via the xdot format.'''
00020 
00021 __author__ = "Jose Fonseca"
00022 
00023 __version__ = "0.4"
00024 
00025 
00026 import os
00027 import sys
00028 import subprocess
00029 import math
00030 import colorsys
00031 import time
00032 import re
00033 
00034 import gobject
00035 import gtk
00036 import gtk.gdk
00037 import gtk.keysyms
00038 import cairo
00039 import pango
00040 import pangocairo
00041 
00042 
00043 # See http://www.graphviz.org/pub/scm/graphviz-cairo/plugin/cairo/gvrender_cairo.c
00044 
00045 # For pygtk inspiration and guidance see:
00046 # - http://mirageiv.berlios.de/
00047 # - http://comix.sourceforge.net/
00048 
00049 
00050 class Pen:
00051     """Store pen attributes."""
00052 
00053     def __init__(self):
00054         # set default attributes
00055         self.color = (0.0, 0.0, 0.0, 1.0)
00056         self.fillcolor = (0.0, 0.0, 0.0, 1.0)
00057         self.linewidth = 1.0
00058         self.fontsize = 14.0
00059         self.fontname = "Times-Roman"
00060         self.dash = ()
00061 
00062     def copy(self):
00063         """Create a copy of this pen."""
00064         pen = Pen()
00065         pen.__dict__ = self.__dict__.copy()
00066         return pen
00067 
00068     def highlighted(self):
00069         pen = self.copy()
00070         pen.color = (1, 0, 0, 1)
00071         pen.fillcolor = (1, .8, .8, 1)
00072         return pen
00073 
00074 
00075 class Shape:
00076     """Abstract base class for all the drawing shapes."""
00077 
00078     def __init__(self):
00079         pass
00080 
00081     def draw(self, cr, highlight=False):
00082         """Draw this shape with the given cairo context"""
00083         raise NotImplementedError
00084 
00085     def select_pen(self, highlight):
00086         if highlight:
00087             if not hasattr(self, 'highlight_pen'):
00088                 self.highlight_pen = self.pen.highlighted()
00089             return self.highlight_pen
00090         else:
00091             return self.pen
00092 
00093 
00094 class TextShape(Shape):
00095 
00096     #fontmap = pangocairo.CairoFontMap()
00097     #fontmap.set_resolution(72)
00098     #context = fontmap.create_context()
00099 
00100     LEFT, CENTER, RIGHT = -1, 0, 1
00101 
00102     def __init__(self, pen, x, y, j, w, t):
00103         Shape.__init__(self)
00104         self.pen = pen.copy()
00105         self.x = x
00106         self.y = y
00107         self.j = j
00108         self.w = w
00109         self.t = t
00110 
00111     def draw(self, cr, highlight=False):
00112 
00113         try:
00114             layout = self.layout
00115         except AttributeError:
00116             layout = cr.create_layout()
00117 
00118             # set font options
00119             # see http://lists.freedesktop.org/archives/cairo/2007-February/009688.html
00120             context = layout.get_context()
00121             fo = cairo.FontOptions()
00122             fo.set_antialias(cairo.ANTIALIAS_DEFAULT)
00123             fo.set_hint_style(cairo.HINT_STYLE_NONE)
00124             fo.set_hint_metrics(cairo.HINT_METRICS_OFF)
00125             try:
00126                 pangocairo.context_set_font_options(context, fo)
00127             except TypeError:
00128                 # XXX: Some broken pangocairo bindings show the error
00129                 # 'TypeError: font_options must be a cairo.FontOptions or None'
00130                 pass
00131 
00132             # set font
00133             font = pango.FontDescription()
00134             font.set_family(self.pen.fontname)
00135             font.set_absolute_size(self.pen.fontsize*pango.SCALE)
00136             layout.set_font_description(font)
00137 
00138             # set text
00139             layout.set_text(self.t)
00140 
00141             # cache it
00142             self.layout = layout
00143         else:
00144             cr.update_layout(layout)
00145 
00146         descent = 2 # XXX get descender from font metrics
00147 
00148         width, height = layout.get_size()
00149         width = float(width)/pango.SCALE
00150         height = float(height)/pango.SCALE
00151         # we know the width that dot thinks this text should have
00152         # we do not necessarily have a font with the same metrics
00153         # scale it so that the text fits inside its box
00154         if width > self.w:
00155             f = self.w / width
00156             width = self.w # equivalent to width *= f
00157             height *= f
00158             descent *= f
00159         else:
00160             f = 1.0
00161 
00162         if self.j == self.LEFT:
00163             x = self.x
00164         elif self.j == self.CENTER:
00165             x = self.x - 0.5*width
00166         elif self.j == self.RIGHT:
00167             x = self.x - width
00168         else:
00169             assert 0
00170 
00171         y = self.y - height + descent
00172 
00173         cr.move_to(x, y)
00174 
00175         cr.save()
00176         cr.scale(f, f)
00177         cr.set_source_rgba(*self.select_pen(highlight).color)
00178         cr.show_layout(layout)
00179         cr.restore()
00180 
00181         if 0: # DEBUG
00182             # show where dot thinks the text should appear
00183             cr.set_source_rgba(1, 0, 0, .9)
00184             if self.j == self.LEFT:
00185                 x = self.x
00186             elif self.j == self.CENTER:
00187                 x = self.x - 0.5*self.w
00188             elif self.j == self.RIGHT:
00189                 x = self.x - self.w
00190             cr.move_to(x, self.y)
00191             cr.line_to(x+self.w, self.y)
00192             cr.stroke()
00193 
00194 
00195 class EllipseShape(Shape):
00196 
00197     def __init__(self, pen, x0, y0, w, h, filled=False):
00198         Shape.__init__(self)
00199         self.pen = pen.copy()
00200         self.x0 = x0
00201         self.y0 = y0
00202         self.w = w
00203         self.h = h
00204         self.filled = filled
00205 
00206     def draw(self, cr, highlight=False):
00207         cr.save()
00208         cr.translate(self.x0, self.y0)
00209         cr.scale(self.w, self.h)
00210         cr.move_to(1.0, 0.0)
00211         cr.arc(0.0, 0.0, 1.0, 0, 2.0*math.pi)
00212         cr.restore()
00213         pen = self.select_pen(highlight)
00214         if self.filled:
00215             cr.set_source_rgba(*pen.fillcolor)
00216             cr.fill()
00217         else:
00218             cr.set_dash(pen.dash)
00219             cr.set_line_width(pen.linewidth)
00220             cr.set_source_rgba(*pen.color)
00221             cr.stroke()
00222 
00223 
00224 class PolygonShape(Shape):
00225 
00226     def __init__(self, pen, points, filled=False):
00227         Shape.__init__(self)
00228         self.pen = pen.copy()
00229         self.points = points
00230         self.filled = filled
00231 
00232     def draw(self, cr, highlight=False):
00233         x0, y0 = self.points[-1]
00234         cr.move_to(x0, y0)
00235         for x, y in self.points:
00236             cr.line_to(x, y)
00237         cr.close_path()
00238         pen = self.select_pen(highlight)
00239         if self.filled:
00240             cr.set_source_rgba(*pen.fillcolor)
00241             cr.fill_preserve()
00242             cr.fill()
00243         else:
00244             cr.set_dash(pen.dash)
00245             cr.set_line_width(pen.linewidth)
00246             cr.set_source_rgba(*pen.color)
00247             cr.stroke()
00248 
00249 
00250 class LineShape(Shape):
00251 
00252     def __init__(self, pen, points):
00253         Shape.__init__(self)
00254         self.pen = pen.copy()
00255         self.points = points
00256 
00257     def draw(self, cr, highlight=False):
00258         x0, y0 = self.points[0]
00259         cr.move_to(x0, y0)
00260         for x1, y1 in self.points[1:]:
00261             cr.line_to(x1, y1)
00262         pen = self.select_pen(highlight)
00263         cr.set_dash(pen.dash)
00264         cr.set_line_width(pen.linewidth)
00265         cr.set_source_rgba(*pen.color)
00266         cr.stroke()
00267 
00268 
00269 class BezierShape(Shape):
00270 
00271     def __init__(self, pen, points, filled=False):
00272         Shape.__init__(self)
00273         self.pen = pen.copy()
00274         self.points = points
00275         self.filled = filled
00276 
00277     def draw(self, cr, highlight=False):
00278         x0, y0 = self.points[0]
00279         cr.move_to(x0, y0)
00280         for i in xrange(1, len(self.points), 3):
00281             x1, y1 = self.points[i]
00282             x2, y2 = self.points[i + 1]
00283             x3, y3 = self.points[i + 2]
00284             cr.curve_to(x1, y1, x2, y2, x3, y3)
00285         pen = self.select_pen(highlight)
00286         if self.filled:
00287             cr.set_source_rgba(*pen.fillcolor)
00288             cr.fill_preserve()
00289             cr.fill()
00290         else:
00291             cr.set_dash(pen.dash)
00292             cr.set_line_width(pen.linewidth)
00293             cr.set_source_rgba(*pen.color)
00294             cr.stroke()
00295 
00296 
00297 class CompoundShape(Shape):
00298 
00299     def __init__(self, shapes):
00300         Shape.__init__(self)
00301         self.shapes = shapes
00302 
00303     def draw(self, cr, highlight=False):
00304         for shape in self.shapes:
00305             shape.draw(cr, highlight=highlight)
00306 
00307 
00308 class Url(object):
00309 
00310     def __init__(self, item, url, highlight=None):
00311         self.item = item
00312         self.url = url
00313         if highlight is None:
00314             highlight = set([item])
00315         self.highlight = highlight
00316 
00317 
00318 class Jump(object):
00319 
00320     def __init__(self, item, x, y, highlight=None, url=None):
00321         self.item = item
00322         self.x = x
00323         self.y = y
00324         if highlight is None:
00325             highlight = set([item])
00326         self.highlight = highlight
00327         self.url = url
00328 
00329 
00330 class Element(CompoundShape):
00331     """Base class for graph nodes and edges."""
00332 
00333     def __init__(self, shapes):
00334         CompoundShape.__init__(self, shapes)
00335 
00336     def get_url(self, x, y):
00337         return None
00338 
00339     def get_jump(self, x, y):
00340         return None
00341 
00342 
00343 class Node(Element):
00344 
00345     def __init__(self, x, y, w, h, shapes, url):
00346         Element.__init__(self, shapes)
00347 
00348         self.x = x
00349         self.y = y
00350 
00351         self.x1 = x - 0.5*w
00352         self.y1 = y - 0.5*h
00353         self.x2 = x + 0.5*w
00354         self.y2 = y + 0.5*h
00355 
00356         self.url = url
00357 
00358     def is_inside(self, x, y):
00359         return self.x1 <= x and x <= self.x2 and self.y1 <= y and y <= self.y2
00360 
00361     def get_url(self, x, y):
00362         if self.url is None:
00363             return None
00364         #print (x, y), (self.x1, self.y1), "-", (self.x2, self.y2)
00365         if self.is_inside(x, y):
00366             return Url(self, self.url)
00367         return None
00368 
00369     def get_jump(self, x, y):
00370         if self.is_inside(x, y):
00371             return Jump(self, self.x, self.y)
00372         return None
00373 
00374 
00375 def square_distance(x1, y1, x2, y2):
00376     deltax = x2 - x1
00377     deltay = y2 - y1
00378     return deltax*deltax + deltay*deltay
00379 
00380 
00381 class Edge(Element):
00382 
00383     def __init__(self, src, dst, points, shapes, url):
00384         Element.__init__(self, shapes)
00385         self.src = src
00386         self.dst = dst
00387         self.points = points
00388         self.url = url
00389 
00390     RADIUS = 10
00391 
00392     def get_jump(self, x, y):
00393         if square_distance(x, y, *self.points[0]) <= self.RADIUS*self.RADIUS:
00394             return Jump(self, self.dst.x, self.dst.y, highlight=set([self, self.dst]),url=self.url)
00395         if square_distance(x, y, *self.points[-1]) <= self.RADIUS*self.RADIUS:
00396             return Jump(self, self.src.x, self.src.y, highlight=set([self, self.src]),url=self.url)
00397         return None
00398 
00399 
00400 class Graph(Shape):
00401 
00402     def __init__(self, width=1, height=1, shapes=(), nodes=(), edges=(), subgraph_shapes={}):
00403         Shape.__init__(self)
00404 
00405         self.width = width
00406         self.height = height
00407         self.shapes = shapes
00408         self.nodes = nodes
00409         self.edges = edges
00410         self.subgraph_shapes = subgraph_shapes
00411 
00412     def get_size(self):
00413         return self.width, self.height
00414 
00415     def draw(self, cr, highlight_items=None):
00416         if highlight_items is None:
00417             highlight_items = ()
00418         cr.set_source_rgba(0.0, 0.0, 0.0, 1.0)
00419 
00420         cr.set_line_cap(cairo.LINE_CAP_BUTT)
00421         cr.set_line_join(cairo.LINE_JOIN_MITER)
00422 
00423         for shape in self.shapes:
00424             shape.draw(cr)
00425         for edge in self.edges:
00426             edge.draw(cr, highlight=(edge in highlight_items))
00427         for node in self.nodes:
00428             node.draw(cr, highlight=(node in highlight_items))
00429 
00430     def get_url(self, x, y):
00431         for node in self.nodes:
00432             url = node.get_url(x, y)
00433             if url is not None:
00434                 return url
00435         return None
00436 
00437     def get_jump(self, x, y):
00438         for edge in self.edges:
00439             jump = edge.get_jump(x, y)
00440             if jump is not None:
00441                 return jump
00442         for node in self.nodes:
00443             jump = node.get_jump(x, y)
00444             if jump is not None:
00445                 return jump
00446         return None
00447 
00448 
00449 class XDotAttrParser:
00450     """Parser for xdot drawing attributes.
00451     See also:
00452     - http://www.graphviz.org/doc/info/output.html#d:xdot
00453     """
00454 
00455     def __init__(self, parser, buf):
00456         self.parser = parser
00457         self.buf = self.unescape(buf)
00458         self.pos = 0
00459         
00460         self.pen = Pen()
00461         self.shapes = []
00462 
00463     def __nonzero__(self):
00464         return self.pos < len(self.buf)
00465 
00466     def unescape(self, buf):
00467         buf = buf.replace('\\"', '"')
00468         buf = buf.replace('\\n', '\n')
00469         return buf
00470 
00471     def read_code(self):
00472         pos = self.buf.find(" ", self.pos)
00473         res = self.buf[self.pos:pos]
00474         self.pos = pos + 1
00475         while self.pos < len(self.buf) and self.buf[self.pos].isspace():
00476             self.pos += 1
00477         return res
00478 
00479     def read_number(self):
00480         return int(self.read_code())
00481 
00482     def read_float(self):
00483         return float(self.read_code())
00484 
00485     def read_point(self):
00486         x = self.read_number()
00487         y = self.read_number()
00488         return self.transform(x, y)
00489 
00490     def read_text(self):
00491         num = self.read_number()
00492         pos = self.buf.find("-", self.pos) + 1
00493         self.pos = pos + num
00494         res = self.buf[pos:self.pos]
00495         while self.pos < len(self.buf) and self.buf[self.pos].isspace():
00496             self.pos += 1
00497         return res
00498 
00499     def read_polygon(self):
00500         n = self.read_number()
00501         p = []
00502         for i in range(n):
00503             x, y = self.read_point()
00504             p.append((x, y))
00505         return p
00506 
00507     def read_color(self):
00508         # See http://www.graphviz.org/doc/info/attrs.html#k:color
00509         c = self.read_text()
00510         c1 = c[:1]
00511         if c1 == '#':
00512             hex2float = lambda h: float(int(h, 16)/255.0)
00513             r = hex2float(c[1:3])
00514             g = hex2float(c[3:5])
00515             b = hex2float(c[5:7])
00516             try:
00517                 a = hex2float(c[7:9])
00518             except (IndexError, ValueError):
00519                 a = 1.0
00520             return r, g, b, a
00521         elif c1.isdigit() or c1 == ".":
00522             # "H,S,V" or "H S V" or "H, S, V" or any other variation
00523             h, s, v = map(float, c.replace(",", " ").split())
00524             r, g, b = colorsys.hsv_to_rgb(h, s, v)
00525             a = 1.0
00526             return r, g, b, a
00527         else:
00528             return self.lookup_color(c)
00529 
00530     def lookup_color(self, c):
00531         try:
00532             color = gtk.gdk.color_parse(c)
00533         except ValueError:
00534             pass
00535         else:
00536             s = 1.0/65535.0
00537             r = color.red*s
00538             g = color.green*s
00539             b = color.blue*s
00540             a = 1.0
00541             return r, g, b, a
00542 
00543         try:
00544             dummy, scheme, index = c.split('/')
00545             r, g, b = brewer_colors[scheme][int(index)]
00546         except (ValueError, KeyError):
00547             pass
00548         else:
00549             s = 1.0/255.0
00550             r = r*s
00551             g = g*s
00552             b = b*s
00553             a = 1.0
00554             return r, g, b, a
00555                 
00556         sys.stderr.write("unknown color '%s'\n" % c)
00557         return None
00558 
00559     def parse(self):
00560         s = self
00561 
00562         while s:
00563             op = s.read_code()
00564             if op == "c":
00565                 color = s.read_color()
00566                 if color is not None:
00567                     self.handle_color(color, filled=False)
00568             elif op == "C":
00569                 color = s.read_color()
00570                 if color is not None:
00571                     self.handle_color(color, filled=True)
00572             elif op == "S":
00573                 # http://www.graphviz.org/doc/info/attrs.html#k:style
00574                 style = s.read_text()
00575                 if style.startswith("setlinewidth("):
00576                     lw = style.split("(")[1].split(")")[0]
00577                     lw = float(lw)
00578                     self.handle_linewidth(lw)
00579                 elif style in ("solid", "dashed"):
00580                     self.handle_linestyle(style)
00581             elif op == "F":
00582                 size = s.read_float()
00583                 name = s.read_text()
00584                 self.handle_font(size, name)
00585             elif op == "T":
00586                 x, y = s.read_point()
00587                 j = s.read_number()
00588                 w = s.read_number()
00589                 t = s.read_text()
00590                 self.handle_text(x, y, j, w, t)
00591             elif op == "E":
00592                 x0, y0 = s.read_point()
00593                 w = s.read_number()
00594                 h = s.read_number()
00595                 self.handle_ellipse(x0, y0, w, h, filled=True)
00596             elif op == "e":
00597                 x0, y0 = s.read_point()
00598                 w = s.read_number()
00599                 h = s.read_number()
00600                 self.handle_ellipse(x0, y0, w, h, filled=False)
00601             elif op == "L":
00602                 points = self.read_polygon()
00603                 self.handle_line(points)
00604             elif op == "B":
00605                 points = self.read_polygon()
00606                 self.handle_bezier(points, filled=False)
00607             elif op == "b":
00608                 points = self.read_polygon()
00609                 self.handle_bezier(points, filled=True)
00610             elif op == "P":
00611                 points = self.read_polygon()
00612                 self.handle_polygon(points, filled=True)
00613             elif op == "p":
00614                 points = self.read_polygon()
00615                 self.handle_polygon(points, filled=False)
00616             else:
00617                 sys.stderr.write("unknown xdot opcode '%s'\n" % op)
00618                 break
00619 
00620         return self.shapes
00621     
00622     def transform(self, x, y):
00623         return self.parser.transform(x, y)
00624 
00625     def handle_color(self, color, filled=False):
00626         if filled:
00627             self.pen.fillcolor = color
00628         else:
00629             self.pen.color = color
00630 
00631     def handle_linewidth(self, linewidth):
00632         self.pen.linewidth = linewidth
00633 
00634     def handle_linestyle(self, style):
00635         if style == "solid":
00636             self.pen.dash = ()
00637         elif style == "dashed":
00638             self.pen.dash = (6, )       # 6pt on, 6pt off
00639 
00640     def handle_font(self, size, name):
00641         self.pen.fontsize = size
00642         self.pen.fontname = name
00643 
00644     def handle_text(self, x, y, j, w, t):
00645         self.shapes.append(TextShape(self.pen, x, y, j, w, t))
00646 
00647     def handle_ellipse(self, x0, y0, w, h, filled=False):
00648         if filled:
00649             # xdot uses this to mean "draw a filled shape with an outline"
00650             self.shapes.append(EllipseShape(self.pen, x0, y0, w, h, filled=True))
00651         self.shapes.append(EllipseShape(self.pen, x0, y0, w, h))
00652 
00653     def handle_line(self, points):
00654         self.shapes.append(LineShape(self.pen, points))
00655 
00656     def handle_bezier(self, points, filled=False):
00657         if filled:
00658             # xdot uses this to mean "draw a filled shape with an outline"
00659             self.shapes.append(BezierShape(self.pen, points, filled=True))
00660         self.shapes.append(BezierShape(self.pen, points))
00661 
00662     def handle_polygon(self, points, filled=False):
00663         if filled:
00664             # xdot uses this to mean "draw a filled shape with an outline"
00665             self.shapes.append(PolygonShape(self.pen, points, filled=True))
00666         self.shapes.append(PolygonShape(self.pen, points))
00667 
00668 
00669 EOF = -1
00670 SKIP = -2
00671 
00672 
00673 class ParseError(Exception):
00674 
00675     def __init__(self, msg=None, filename=None, line=None, col=None):
00676         self.msg = msg
00677         self.filename = filename
00678         self.line = line
00679         self.col = col
00680 
00681     def __str__(self):
00682         return ':'.join([str(part) for part in (self.filename, self.line, self.col, self.msg) if part != None])
00683         
00684 
00685 class Scanner:
00686     """Stateless scanner."""
00687 
00688     # should be overriden by derived classes
00689     tokens = []
00690     symbols = {}
00691     literals = {}
00692     ignorecase = False
00693 
00694     def __init__(self):
00695         flags = re.DOTALL
00696         if self.ignorecase:
00697             flags |= re.IGNORECASE
00698         self.tokens_re = re.compile(
00699             '|'.join(['(' + regexp + ')' for type, regexp, test_lit in self.tokens]),
00700              flags
00701         )
00702 
00703     def next(self, buf, pos):
00704         if pos >= len(buf):
00705             return EOF, '', pos
00706         mo = self.tokens_re.match(buf, pos)
00707         if mo:
00708             text = mo.group()
00709             type, regexp, test_lit = self.tokens[mo.lastindex - 1]
00710             pos = mo.end()
00711             if test_lit:
00712                 type = self.literals.get(text, type)
00713             return type, text, pos
00714         else:
00715             c = buf[pos]
00716             return self.symbols.get(c, None), c, pos + 1
00717 
00718 
00719 class Token:
00720 
00721     def __init__(self, type, text, line, col):
00722         self.type = type
00723         self.text = text
00724         self.line = line
00725         self.col = col
00726 
00727 
00728 class Lexer:
00729 
00730     # should be overriden by derived classes
00731     scanner = None
00732     tabsize = 8
00733 
00734     newline_re = re.compile(r'\r\n?|\n')
00735 
00736     def __init__(self, buf = None, pos = 0, filename = None, fp = None):
00737         if fp is not None:
00738             try:
00739                 fileno = fp.fileno()
00740                 length = os.path.getsize(fp.name)
00741                 import mmap
00742             except:
00743                 # read whole file into memory
00744                 buf = fp.read()
00745                 pos = 0
00746             else:
00747                 # map the whole file into memory
00748                 if length:
00749                     # length must not be zero
00750                     buf = mmap.mmap(fileno, length, access = mmap.ACCESS_READ)
00751                     pos = os.lseek(fileno, 0, 1)
00752                 else:
00753                     buf = ''
00754                     pos = 0
00755 
00756             if filename is None:
00757                 try:
00758                     filename = fp.name
00759                 except AttributeError:
00760                     filename = None
00761 
00762         self.buf = buf
00763         self.pos = pos
00764         self.line = 1
00765         self.col = 1
00766         self.filename = filename
00767 
00768     def next(self):
00769         while True:
00770             # save state
00771             pos = self.pos
00772             line = self.line
00773             col = self.col
00774 
00775             type, text, endpos = self.scanner.next(self.buf, pos)
00776             assert pos + len(text) == endpos
00777             self.consume(text)
00778             type, text = self.filter(type, text)
00779             self.pos = endpos
00780 
00781             if type == SKIP:
00782                 continue
00783             elif type is None:
00784                 msg = 'unexpected char '
00785                 if text >= ' ' and text <= '~':
00786                     msg += "'%s'" % text
00787                 else:
00788                     msg += "0x%X" % ord(text)
00789                 raise ParseError(msg, self.filename, line, col)
00790             else:
00791                 break
00792         return Token(type = type, text = text, line = line, col = col)
00793 
00794     def consume(self, text):
00795         # update line number
00796         pos = 0
00797         for mo in self.newline_re.finditer(text, pos):
00798             self.line += 1
00799             self.col = 1
00800             pos = mo.end()
00801 
00802         # update column number
00803         while True:
00804             tabpos = text.find('\t', pos)
00805             if tabpos == -1:
00806                 break
00807             self.col += tabpos - pos
00808             self.col = ((self.col - 1)//self.tabsize + 1)*self.tabsize + 1
00809             pos = tabpos + 1
00810         self.col += len(text) - pos
00811 
00812 
00813 class Parser:
00814 
00815     def __init__(self, lexer):
00816         self.lexer = lexer
00817         self.lookahead = self.lexer.next()
00818 
00819     def match(self, type):
00820         if self.lookahead.type != type:
00821             raise ParseError(
00822                 msg = 'unexpected token %r' % self.lookahead.text, 
00823                 filename = self.lexer.filename, 
00824                 line = self.lookahead.line, 
00825                 col = self.lookahead.col)
00826 
00827     def skip(self, type):
00828         while self.lookahead.type != type:
00829             self.consume()
00830 
00831     def consume(self):
00832         token = self.lookahead
00833         self.lookahead = self.lexer.next()
00834         return token
00835 
00836 
00837 ID = 0
00838 STR_ID = 1
00839 HTML_ID = 2
00840 EDGE_OP = 3
00841 
00842 LSQUARE = 4
00843 RSQUARE = 5
00844 LCURLY = 6
00845 RCURLY = 7
00846 COMMA = 8
00847 COLON = 9
00848 SEMI = 10
00849 EQUAL = 11
00850 PLUS = 12
00851 
00852 STRICT = 13
00853 GRAPH = 14
00854 DIGRAPH = 15
00855 NODE = 16
00856 EDGE = 17
00857 SUBGRAPH = 18
00858 
00859 
00860 class DotScanner(Scanner):
00861 
00862     # token regular expression table
00863     tokens = [
00864         # whitespace and comments
00865         (SKIP,
00866             r'[ \t\f\r\n\v]+|'
00867             r'//[^\r\n]*|'
00868             r'/\*.*?\*/|'
00869             r'#[^\r\n]*',
00870         False),
00871 
00872         # Alphanumeric IDs
00873         (ID, r'[a-zA-Z_\x80-\xff][a-zA-Z0-9_\x80-\xff]*', True),
00874 
00875         # Numeric IDs
00876         (ID, r'-?(?:\.[0-9]+|[0-9]+(?:\.[0-9]*)?)', False),
00877 
00878         # String IDs
00879         (STR_ID, r'"[^"\\]*(?:\\.[^"\\]*)*"', False),
00880 
00881         # HTML IDs
00882         (HTML_ID, r'<[^<>]*(?:<[^<>]*>[^<>]*)*>', False),
00883 
00884         # Edge operators
00885         (EDGE_OP, r'-[>-]', False),
00886     ]
00887 
00888     # symbol table
00889     symbols = {
00890         '[': LSQUARE,
00891         ']': RSQUARE,
00892         '{': LCURLY,
00893         '}': RCURLY,
00894         ',': COMMA,
00895         ':': COLON,
00896         ';': SEMI,
00897         '=': EQUAL,
00898         '+': PLUS,
00899     }
00900 
00901     # literal table
00902     literals = {
00903         'strict': STRICT,
00904         'graph': GRAPH,
00905         'digraph': DIGRAPH,
00906         'node': NODE,
00907         'edge': EDGE,
00908         'subgraph': SUBGRAPH,
00909     }
00910 
00911     ignorecase = True
00912 
00913 
00914 class DotLexer(Lexer):
00915 
00916     scanner = DotScanner()
00917 
00918     def filter(self, type, text):
00919         # TODO: handle charset
00920         if type == STR_ID:
00921             text = text[1:-1]
00922 
00923             # line continuations
00924             text = text.replace('\\\r\n', '')
00925             text = text.replace('\\\r', '')
00926             text = text.replace('\\\n', '')
00927             
00928             text = text.replace('\\r', '\r')
00929             text = text.replace('\\n', '\n')
00930             text = text.replace('\\t', '\t')
00931             text = text.replace('\\', '')
00932 
00933             type = ID
00934 
00935         elif type == HTML_ID:
00936             text = text[1:-1]
00937             type = ID
00938 
00939         return type, text
00940 
00941 
00942 class DotParser(Parser):
00943 
00944     def __init__(self, lexer):
00945         Parser.__init__(self, lexer)
00946         self.graph_attrs = {}
00947         self.node_attrs = {}
00948         self.edge_attrs = {}
00949 
00950     def parse(self):
00951         self.parse_graph()
00952         self.match(EOF)
00953 
00954     def parse_graph(self):
00955         if self.lookahead.type == STRICT:
00956             self.consume()
00957         self.skip(LCURLY)
00958         self.consume()
00959         while self.lookahead.type != RCURLY:
00960             self.parse_stmt()
00961         self.consume()
00962 
00963     def parse_subgraph(self):
00964         id = None
00965         shapes_before = set(self.shapes)
00966         if self.lookahead.type == SUBGRAPH:
00967             self.consume()
00968             if self.lookahead.type == ID:
00969                 id = self.lookahead.text
00970                 self.consume()
00971         if self.lookahead.type == LCURLY:
00972             self.consume()
00973             while self.lookahead.type != RCURLY:
00974                 self.parse_stmt()
00975             self.consume()
00976         new_shapes = set(self.shapes) - shapes_before
00977         self.subgraph_shapes[id] = [s for s in new_shapes if not any([s in ss for ss in self.subgraph_shapes.values()])]
00978         return id
00979 
00980     def parse_stmt(self):
00981         if self.lookahead.type == GRAPH:
00982             self.consume()
00983             attrs = self.parse_attrs()
00984             self.graph_attrs.update(attrs)
00985             self.handle_graph(attrs)
00986         elif self.lookahead.type == NODE:
00987             self.consume()
00988             self.node_attrs.update(self.parse_attrs())
00989         elif self.lookahead.type == EDGE:
00990             self.consume()
00991             self.edge_attrs.update(self.parse_attrs())
00992         elif self.lookahead.type in (SUBGRAPH, LCURLY):
00993             self.parse_subgraph()
00994         else:
00995             id = self.parse_node_id()
00996             if self.lookahead.type == EDGE_OP:
00997                 self.consume()
00998                 node_ids = [id, self.parse_node_id()]
00999                 while self.lookahead.type == EDGE_OP:
01000                     node_ids.append(self.parse_node_id())
01001                 attrs = self.parse_attrs()
01002                 for i in range(0, len(node_ids) - 1):
01003                     self.handle_edge(node_ids[i], node_ids[i + 1], attrs)
01004             elif self.lookahead.type == EQUAL:
01005                 self.consume()
01006                 self.parse_id()
01007             else:
01008                 attrs = self.parse_attrs()
01009                 self.handle_node(id, attrs)
01010         if self.lookahead.type == SEMI:
01011             self.consume()
01012 
01013     def parse_attrs(self):
01014         attrs = {}
01015         while self.lookahead.type == LSQUARE:
01016             self.consume()
01017             while self.lookahead.type != RSQUARE:
01018                 name, value = self.parse_attr()
01019                 attrs[name] = value
01020                 if self.lookahead.type == COMMA:
01021                     self.consume()
01022             self.consume()
01023         return attrs
01024 
01025     def parse_attr(self):
01026         name = self.parse_id()
01027         if self.lookahead.type == EQUAL:
01028             self.consume()
01029             value = self.parse_id()
01030         else:
01031             value = 'true'
01032         return name, value
01033 
01034     def parse_node_id(self):
01035         node_id = self.parse_id()
01036         if self.lookahead.type == COLON:
01037             self.consume()
01038             port = self.parse_id()
01039             if self.lookahead.type == COLON:
01040                 self.consume()
01041                 compass_pt = self.parse_id()
01042             else:
01043                 compass_pt = None
01044         else:
01045             port = None
01046             compass_pt = None
01047         # XXX: we don't really care about port and compass point values when parsing xdot
01048         return node_id
01049 
01050     def parse_id(self):
01051         self.match(ID)
01052         id = self.lookahead.text
01053         self.consume()
01054         return id
01055 
01056     def handle_graph(self, attrs):
01057         pass
01058 
01059     def handle_node(self, id, attrs):
01060         pass
01061 
01062     def handle_edge(self, src_id, dst_id, attrs):
01063         pass
01064 
01065 
01066 class XDotParser(DotParser):
01067 
01068     def __init__(self, xdotcode):
01069         lexer = DotLexer(buf = xdotcode)
01070         DotParser.__init__(self, lexer)
01071         
01072         self.nodes = []
01073         self.edges = []
01074         self.shapes = []
01075         self.node_by_name = {}
01076         self.top_graph = True
01077         self.subgraph_shapes = {}
01078 
01079     def handle_graph(self, attrs):
01080         if self.top_graph:
01081             try:
01082                 bb = attrs['bb']
01083             except KeyError:
01084                 return
01085 
01086             if not bb:
01087                 return
01088 
01089             xmin, ymin, xmax, ymax = map(float, bb.split(","))
01090 
01091             self.xoffset = -xmin
01092             self.yoffset = -ymax
01093             self.xscale = 1.0
01094             self.yscale = -1.0
01095             # FIXME: scale from points to pixels
01096 
01097             self.width = xmax - xmin
01098             self.height = ymax - ymin
01099 
01100             self.top_graph = False
01101         
01102         for attr in ("_draw_", "_ldraw_", "_hdraw_", "_tdraw_", "_hldraw_", "_tldraw_"):
01103             if attr in attrs:
01104                 parser = XDotAttrParser(self, attrs[attr])
01105                 self.shapes.extend(parser.parse())
01106 
01107     def handle_node(self, id, attrs):
01108         try:
01109             pos = attrs['pos']
01110         except KeyError:
01111             return
01112 
01113         x, y = self.parse_node_pos(pos)
01114         w = float(attrs['width'])*72
01115         h = float(attrs['height'])*72
01116         shapes = []
01117         for attr in ("_draw_", "_ldraw_"):
01118             if attr in attrs:
01119                 parser = XDotAttrParser(self, attrs[attr])
01120                 shapes.extend(parser.parse())
01121         url = attrs.get('URL', None)
01122         node = Node(x, y, w, h, shapes, url)
01123         self.node_by_name[id] = node
01124         if shapes:
01125             self.nodes.append(node)
01126 
01127     def handle_edge(self, src_id, dst_id, attrs):
01128         try:
01129             pos = attrs['pos']
01130         except KeyError:
01131             return
01132         
01133         points = self.parse_edge_pos(pos)
01134         shapes = []
01135         for attr in ("_draw_", "_ldraw_", "_hdraw_", "_tdraw_", "_hldraw_", "_tldraw_"):
01136             if attr in attrs:
01137                 parser = XDotAttrParser(self, attrs[attr])
01138                 shapes.extend(parser.parse())
01139         url = attrs.get('URL', None)
01140         if shapes:
01141             src = self.node_by_name[src_id]
01142             dst = self.node_by_name[dst_id]
01143             self.edges.append(Edge(src, dst, points, shapes, url))
01144 
01145     def parse(self):
01146         DotParser.parse(self)
01147 
01148         """
01149         for k,shapes in self.subgraph_shapes.iteritems():
01150           self.shapes += shapes
01151         """
01152 
01153         return Graph(self.width, self.height, self.shapes, self.nodes, self.edges, self.subgraph_shapes)
01154 
01155     def parse_node_pos(self, pos):
01156         x, y = pos.split(",")
01157         return self.transform(float(x), float(y))
01158 
01159     def parse_edge_pos(self, pos):
01160         points = []
01161         for entry in pos.split(' '):
01162             fields = entry.split(',')
01163             try:
01164                 x, y = fields
01165             except ValueError:
01166                 # TODO: handle start/end points
01167                 continue
01168             else:
01169                 points.append(self.transform(float(x), float(y)))
01170         return points
01171 
01172     def transform(self, x, y):
01173         # XXX: this is not the right place for this code
01174         x = (x + self.xoffset)*self.xscale
01175         y = (y + self.yoffset)*self.yscale
01176         return x, y
01177 
01178 
01179 class Animation(object):
01180 
01181     step = 0.03 # seconds
01182 
01183     def __init__(self, dot_widget):
01184         self.dot_widget = dot_widget
01185         self.timeout_id = None
01186 
01187     def start(self):
01188         self.timeout_id = gobject.timeout_add(int(self.step * 1000), self.tick)
01189 
01190     def stop(self):
01191         self.dot_widget.animation = NoAnimation(self.dot_widget)
01192         if self.timeout_id is not None:
01193             gobject.source_remove(self.timeout_id)
01194             self.timeout_id = None
01195 
01196     def tick(self):
01197         self.stop()
01198 
01199 
01200 class NoAnimation(Animation):
01201 
01202     def start(self):
01203         pass
01204 
01205     def stop(self):
01206         pass
01207 
01208 
01209 class LinearAnimation(Animation):
01210 
01211     duration = 0.6
01212 
01213     def start(self):
01214         self.started = time.time()
01215         Animation.start(self)
01216 
01217     def tick(self):
01218         t = (time.time() - self.started) / self.duration
01219         self.animate(max(0, min(t, 1)))
01220         return (t < 1)
01221 
01222     def animate(self, t):
01223         pass
01224 
01225 
01226 class MoveToAnimation(LinearAnimation):
01227 
01228     def __init__(self, dot_widget, target_x, target_y):
01229         Animation.__init__(self, dot_widget)
01230         self.source_x = dot_widget.x
01231         self.source_y = dot_widget.y
01232         self.target_x = target_x
01233         self.target_y = target_y
01234 
01235     def animate(self, t):
01236         sx, sy = self.source_x, self.source_y
01237         tx, ty = self.target_x, self.target_y
01238         self.dot_widget.x = tx * t + sx * (1-t)
01239         self.dot_widget.y = ty * t + sy * (1-t)
01240         self.dot_widget.queue_draw()
01241 
01242 
01243 class ZoomToAnimation(MoveToAnimation):
01244 
01245     def __init__(self, dot_widget, target_x, target_y):
01246         MoveToAnimation.__init__(self, dot_widget, target_x, target_y)
01247         self.source_zoom = dot_widget.zoom_ratio
01248         self.target_zoom = self.source_zoom
01249         self.extra_zoom = 0
01250 
01251         middle_zoom = 0.5 * (self.source_zoom + self.target_zoom)
01252 
01253         distance = math.hypot(self.source_x - self.target_x,
01254                               self.source_y - self.target_y)
01255         rect = self.dot_widget.get_allocation()
01256         visible = min(rect.width, rect.height) / self.dot_widget.zoom_ratio
01257         visible *= 0.9
01258         if distance > 0:
01259             desired_middle_zoom = visible / distance
01260             self.extra_zoom = min(0, 4 * (desired_middle_zoom - middle_zoom))
01261 
01262     def animate(self, t):
01263         a, b, c = self.source_zoom, self.extra_zoom, self.target_zoom
01264         self.dot_widget.zoom_ratio = c*t + b*t*(1-t) + a*(1-t)
01265         self.dot_widget.zoom_to_fit_on_resize = False
01266         MoveToAnimation.animate(self, t)
01267 
01268 
01269 class DragAction(object):
01270 
01271     def __init__(self, dot_widget):
01272         self.dot_widget = dot_widget
01273 
01274     def on_button_press(self, event):
01275         self.startmousex = self.prevmousex = event.x
01276         self.startmousey = self.prevmousey = event.y
01277         self.start()
01278 
01279     def on_motion_notify(self, event):
01280         if event.is_hint:
01281             x, y, state = event.window.get_pointer()
01282         else:
01283             x, y, state = event.x, event.y, event.state
01284         deltax = self.prevmousex - x
01285         deltay = self.prevmousey - y
01286         self.drag(deltax, deltay)
01287         self.prevmousex = x
01288         self.prevmousey = y
01289 
01290     def on_button_release(self, event):
01291         self.stopmousex = event.x
01292         self.stopmousey = event.y
01293         self.stop()
01294 
01295     def draw(self, cr):
01296         pass
01297 
01298     def start(self):
01299         pass
01300 
01301     def drag(self, deltax, deltay):
01302         pass
01303 
01304     def stop(self):
01305         pass
01306 
01307     def abort(self):
01308         pass
01309 
01310 
01311 class NullAction(DragAction):
01312 
01313     def on_motion_notify(self, event):
01314         if event.is_hint:
01315             x, y, state = event.window.get_pointer()
01316         else:
01317             x, y, state = event.x, event.y, event.state
01318         dot_widget = self.dot_widget
01319         item = dot_widget.get_url(x, y)
01320         if item is None:
01321             item = dot_widget.get_jump(x, y)
01322         if item is not None:
01323             dot_widget.window.set_cursor(gtk.gdk.Cursor(gtk.gdk.HAND2))
01324             dot_widget.set_highlight(item.highlight)
01325         else:
01326             dot_widget.window.set_cursor(gtk.gdk.Cursor(gtk.gdk.ARROW))
01327             dot_widget.set_highlight(None)
01328 
01329 
01330 class PanAction(DragAction):
01331 
01332     def start(self):
01333         self.dot_widget.window.set_cursor(gtk.gdk.Cursor(gtk.gdk.FLEUR))
01334 
01335     def drag(self, deltax, deltay):
01336         self.dot_widget.x += deltax / self.dot_widget.zoom_ratio
01337         self.dot_widget.y += deltay / self.dot_widget.zoom_ratio
01338         self.dot_widget.queue_draw()
01339 
01340     def stop(self):
01341         self.dot_widget.window.set_cursor(gtk.gdk.Cursor(gtk.gdk.ARROW))
01342 
01343     abort = stop
01344 
01345 
01346 class ZoomAction(DragAction):
01347 
01348     def drag(self, deltax, deltay):
01349         self.dot_widget.zoom_ratio *= 1.005 ** (deltax + deltay)
01350         self.dot_widget.zoom_to_fit_on_resize = False
01351         self.dot_widget.queue_draw()
01352 
01353     def stop(self):
01354         self.dot_widget.queue_draw()
01355 
01356 
01357 class ZoomAreaAction(DragAction):
01358 
01359     def drag(self, deltax, deltay):
01360         self.dot_widget.queue_draw()
01361 
01362     def draw(self, cr):
01363         cr.save()
01364         cr.set_source_rgba(.5, .5, 1.0, 0.25)
01365         cr.rectangle(self.startmousex, self.startmousey,
01366                      self.prevmousex - self.startmousex,
01367                      self.prevmousey - self.startmousey)
01368         cr.fill()
01369         cr.set_source_rgba(.5, .5, 1.0, 1.0)
01370         cr.set_line_width(1)
01371         cr.rectangle(self.startmousex - .5, self.startmousey - .5,
01372                      self.prevmousex - self.startmousex + 1,
01373                      self.prevmousey - self.startmousey + 1)
01374         cr.stroke()
01375         cr.restore()
01376 
01377     def stop(self):
01378         x1, y1 = self.dot_widget.window2graph(self.startmousex,
01379                                               self.startmousey)
01380         x2, y2 = self.dot_widget.window2graph(self.stopmousex,
01381                                               self.stopmousey)
01382         self.dot_widget.zoom_to_area(x1, y1, x2, y2)
01383 
01384     def abort(self):
01385         self.dot_widget.queue_draw()
01386 
01387 
01388 class DotWidget(gtk.DrawingArea):
01389     """PyGTK widget that draws dot graphs."""
01390 
01391     __gsignals__ = {
01392         'expose-event': 'override',
01393         'clicked' : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_STRING, gtk.gdk.Event))
01394     }
01395 
01396     filter = 'dot'
01397 
01398     def __init__(self):
01399         gtk.DrawingArea.__init__(self)
01400 
01401         self.graph = Graph()
01402         self.openfilename = None
01403 
01404         self.set_flags(gtk.CAN_FOCUS)
01405 
01406         self.add_events(gtk.gdk.BUTTON_PRESS_MASK | gtk.gdk.BUTTON_RELEASE_MASK)
01407         self.connect("button-press-event", self.on_area_button_press)
01408         self.connect("button-release-event", self.on_area_button_release)
01409         self.add_events(gtk.gdk.POINTER_MOTION_MASK | gtk.gdk.POINTER_MOTION_HINT_MASK | gtk.gdk.BUTTON_RELEASE_MASK)
01410         self.connect("motion-notify-event", self.on_area_motion_notify)
01411         self.connect("scroll-event", self.on_area_scroll_event)
01412         self.connect("size-allocate", self.on_area_size_allocate)
01413 
01414         self.connect('key-press-event', self.on_key_press_event)
01415 
01416         self.x, self.y = 0.0, 0.0
01417         self.zoom_ratio = 1.0
01418         self.zoom_to_fit_on_resize = False
01419         self.animation = NoAnimation(self)
01420         self.drag_action = NullAction(self)
01421         self.presstime = None
01422         self.highlight = None
01423 
01424     def set_filter(self, filter):
01425         self.filter = filter
01426 
01427     def set_dotcode(self, dotcode, filename='<stdin>'):
01428         if isinstance(dotcode, unicode):
01429             dotcode = dotcode.encode('utf8')
01430         p = subprocess.Popen(
01431             [self.filter, '-Txdot'],
01432             stdin=subprocess.PIPE,
01433             stdout=subprocess.PIPE,
01434             stderr=subprocess.PIPE,
01435             shell=False,
01436             universal_newlines=True
01437         )
01438         xdotcode, error = p.communicate(dotcode)
01439         if p.returncode != 0:
01440             print "UNABLE TO SHELL TO DOT", error
01441             dialog = gtk.MessageDialog(type=gtk.MESSAGE_ERROR,
01442                                        message_format=error,
01443                                        buttons=gtk.BUTTONS_OK)
01444             dialog.set_title('Dot Viewer')
01445             dialog.run()
01446             dialog.destroy()
01447             return False
01448         try:
01449             self.set_xdotcode(xdotcode)
01450         except ParseError, ex:
01451             dialog = gtk.MessageDialog(type=gtk.MESSAGE_ERROR,
01452                                        message_format=str(ex),
01453                                        buttons=gtk.BUTTONS_OK)
01454             dialog.set_title('Dot Viewer')
01455             dialog.run()
01456             dialog.destroy()
01457             return False
01458         else:
01459             self.openfilename = filename
01460             return True
01461 
01462     def set_xdotcode(self, xdotcode):
01463         #print xdotcode
01464         parser = XDotParser(xdotcode)
01465         self.graph = parser.parse()
01466         self.zoom_image(self.zoom_ratio, center=False)
01467 
01468     def reload(self):
01469         if self.openfilename is not None:
01470             try:
01471                 fp = file(self.openfilename, 'rt')
01472                 self.set_dotcode(fp.read(), self.openfilename)
01473                 fp.close()
01474             except IOError:
01475                 pass
01476 
01477     def do_expose_event(self, event):
01478         cr = self.window.cairo_create()
01479 
01480         # set a clip region for the expose event
01481         cr.rectangle(
01482             event.area.x, event.area.y,
01483             event.area.width, event.area.height
01484         )
01485         cr.clip()
01486 
01487         cr.set_source_rgba(1.0, 1.0, 1.0, 1.0)
01488         cr.paint()
01489 
01490         cr.save()
01491         rect = self.get_allocation()
01492         cr.translate(0.5*rect.width, 0.5*rect.height)
01493         cr.scale(self.zoom_ratio, self.zoom_ratio)
01494         cr.translate(-self.x, -self.y)
01495 
01496         self.graph.draw(cr, highlight_items=self.highlight)
01497         cr.restore()
01498 
01499         self.drag_action.draw(cr)
01500 
01501         return False
01502 
01503     def get_current_pos(self):
01504         return self.x, self.y
01505 
01506     def set_current_pos(self, x, y):
01507         self.x = x
01508         self.y = y
01509         self.queue_draw()
01510 
01511     def set_highlight(self, items):
01512         if self.highlight != items:
01513             self.highlight = items
01514             self.queue_draw()
01515 
01516     def zoom_image(self, zoom_ratio, center=False, pos=None):
01517         if center:
01518             self.x = self.graph.width/2
01519             self.y = self.graph.height/2
01520         elif pos is not None:
01521             rect = self.get_allocation()
01522             x, y = pos
01523             x -= 0.5*rect.width
01524             y -= 0.5*rect.height
01525             self.x += x / self.zoom_ratio - x / zoom_ratio
01526             self.y += y / self.zoom_ratio - y / zoom_ratio
01527         self.zoom_ratio = zoom_ratio
01528         self.zoom_to_fit_on_resize = False
01529         self.queue_draw()
01530 
01531     def zoom_to_area(self, x1, y1, x2, y2):
01532         rect = self.get_allocation()
01533         width = abs(x1 - x2)
01534         height = abs(y1 - y2)
01535         self.zoom_ratio = min(
01536             float(rect.width)/float(width),
01537             float(rect.height)/float(height)
01538         )
01539         self.zoom_to_fit_on_resize = False
01540         self.x = (x1 + x2) / 2
01541         self.y = (y1 + y2) / 2
01542         self.queue_draw()
01543 
01544     def zoom_to_fit(self):
01545         rect = self.get_allocation()
01546         rect.x += self.ZOOM_TO_FIT_MARGIN
01547         rect.y += self.ZOOM_TO_FIT_MARGIN
01548         rect.width -= 2 * self.ZOOM_TO_FIT_MARGIN
01549         rect.height -= 2 * self.ZOOM_TO_FIT_MARGIN
01550         zoom_ratio = min(
01551             float(rect.width)/float(self.graph.width),
01552             float(rect.height)/float(self.graph.height)
01553         )
01554         self.zoom_image(zoom_ratio, center=True)
01555         self.zoom_to_fit_on_resize = True
01556 
01557     ZOOM_INCREMENT = 1.25
01558     ZOOM_TO_FIT_MARGIN = 12
01559 
01560     def on_zoom_in(self, action):
01561         self.zoom_image(self.zoom_ratio * self.ZOOM_INCREMENT)
01562 
01563     def on_zoom_out(self, action):
01564         self.zoom_image(self.zoom_ratio / self.ZOOM_INCREMENT)
01565 
01566     def on_zoom_fit(self, action):
01567         self.zoom_to_fit()
01568 
01569     def on_zoom_100(self, action):
01570         self.zoom_image(1.0)
01571 
01572     POS_INCREMENT = 100
01573 
01574     def on_key_press_event(self, widget, event):
01575         if event.keyval == gtk.keysyms.Left:
01576             self.x -= self.POS_INCREMENT/self.zoom_ratio
01577             self.queue_draw()
01578             return True
01579         if event.keyval == gtk.keysyms.Right:
01580             self.x += self.POS_INCREMENT/self.zoom_ratio
01581             self.queue_draw()
01582             return True
01583         if event.keyval == gtk.keysyms.Up:
01584             self.y -= self.POS_INCREMENT/self.zoom_ratio
01585             self.queue_draw()
01586             return True
01587         if event.keyval == gtk.keysyms.Down:
01588             self.y += self.POS_INCREMENT/self.zoom_ratio
01589             self.queue_draw()
01590             return True
01591         if event.keyval == gtk.keysyms.Page_Up:
01592             self.zoom_image(self.zoom_ratio * self.ZOOM_INCREMENT)
01593             self.queue_draw()
01594             return True
01595         if event.keyval == gtk.keysyms.Page_Down:
01596             self.zoom_image(self.zoom_ratio / self.ZOOM_INCREMENT)
01597             self.queue_draw()
01598             return True
01599         if event.keyval == gtk.keysyms.Escape:
01600             self.drag_action.abort()
01601             self.drag_action = NullAction(self)
01602             return True
01603         if event.keyval == gtk.keysyms.r:
01604             self.reload()
01605             return True
01606         if event.keyval == gtk.keysyms.q:
01607             gtk.main_quit()
01608             return True
01609         return False
01610 
01611     def get_drag_action(self, event):
01612         state = event.state
01613         if event.button in (1, 2): # left or middle button
01614             if state & gtk.gdk.CONTROL_MASK:
01615                 return ZoomAction
01616             elif state & gtk.gdk.SHIFT_MASK:
01617                 return ZoomAreaAction
01618             else:
01619                 return PanAction
01620         return NullAction
01621 
01622     def on_area_button_press(self, area, event):
01623         self.animation.stop()
01624         self.drag_action.abort()
01625         action_type = self.get_drag_action(event)
01626         self.drag_action = action_type(self)
01627         self.drag_action.on_button_press(event)
01628         self.presstime = time.time()
01629         self.pressx = event.x
01630         self.pressy = event.y
01631         return False
01632 
01633     def is_click(self, event, click_fuzz=4, click_timeout=1.0):
01634         assert event.type == gtk.gdk.BUTTON_RELEASE
01635         if self.presstime is None:
01636             # got a button release without seeing the press?
01637             return False
01638         # XXX instead of doing this complicated logic, shouldn't we listen
01639         # for gtk's clicked event instead?
01640         deltax = self.pressx - event.x
01641         deltay = self.pressy - event.y
01642         return (time.time() < self.presstime + click_timeout
01643                 and math.hypot(deltax, deltay) < click_fuzz)
01644 
01645     def on_area_button_release(self, area, event):
01646         self.drag_action.on_button_release(event)
01647         self.drag_action = NullAction(self)
01648         if event.button == 1 and self.is_click(event):
01649             x, y = int(event.x), int(event.y)
01650             url = self.get_url(x, y)
01651             if url is not None:
01652                 self.emit('clicked', unicode(url.url), event)
01653             else:
01654                 jump = self.get_jump(x, y)
01655                 if jump is not None:
01656                     self.animate_to(jump.x, jump.y)
01657 
01658             return True
01659         if event.button == 1 or event.button == 2:
01660             return True
01661         return False
01662 
01663     def on_area_scroll_event(self, area, event):
01664         if event.direction == gtk.gdk.SCROLL_UP:
01665             self.zoom_image(self.zoom_ratio * self.ZOOM_INCREMENT,
01666                             pos=(event.x, event.y))
01667             return True
01668         if event.direction == gtk.gdk.SCROLL_DOWN:
01669             self.zoom_image(self.zoom_ratio / self.ZOOM_INCREMENT,
01670                             pos=(event.x, event.y))
01671             return True
01672         return False
01673 
01674     def on_area_motion_notify(self, area, event):
01675         self.drag_action.on_motion_notify(event)
01676         return True
01677 
01678     def on_area_size_allocate(self, area, allocation):
01679         if self.zoom_to_fit_on_resize:
01680             self.zoom_to_fit()
01681 
01682     def animate_to(self, x, y):
01683         self.animation = ZoomToAnimation(self, x, y)
01684         self.animation.start()
01685 
01686     def window2graph(self, x, y):
01687         rect = self.get_allocation()
01688         x -= 0.5*rect.width
01689         y -= 0.5*rect.height
01690         x /= self.zoom_ratio
01691         y /= self.zoom_ratio
01692         x += self.x
01693         y += self.y
01694         return x, y
01695 
01696     def get_url(self, x, y):
01697         x, y = self.window2graph(x, y)
01698         return self.graph.get_url(x, y)
01699 
01700     def get_jump(self, x, y):
01701         x, y = self.window2graph(x, y)
01702         return self.graph.get_jump(x, y)
01703 
01704 
01705 class DotWindow(gtk.Window):
01706 
01707     ui = '''
01708     <ui>
01709         <toolbar name="ToolBar">
01710             <toolitem action="Open"/>
01711             <toolitem action="Reload"/>
01712             <separator/>
01713             <toolitem action="ZoomIn"/>
01714             <toolitem action="ZoomOut"/>
01715             <toolitem action="ZoomFit"/>
01716             <toolitem action="Zoom100"/>
01717         </toolbar>
01718     </ui>
01719     '''
01720 
01721     def __init__(self):
01722         gtk.Window.__init__(self)
01723 
01724         self.graph = Graph()
01725 
01726         window = self
01727 
01728         window.set_title('Dot Viewer')
01729         window.set_default_size(512, 512)
01730         vbox = gtk.VBox()
01731         window.add(vbox)
01732 
01733         self.widget = DotWidget()
01734 
01735         # Create a UIManager instance
01736         uimanager = self.uimanager = gtk.UIManager()
01737 
01738         # Add the accelerator group to the toplevel window
01739         accelgroup = uimanager.get_accel_group()
01740         window.add_accel_group(accelgroup)
01741 
01742         # Create an ActionGroup
01743         actiongroup = gtk.ActionGroup('Actions')
01744         self.actiongroup = actiongroup
01745 
01746         # Create actions
01747         actiongroup.add_actions((
01748             ('Open', gtk.STOCK_OPEN, None, None, None, self.on_open),
01749             ('Reload', gtk.STOCK_REFRESH, None, None, None, self.on_reload),
01750             ('ZoomIn', gtk.STOCK_ZOOM_IN, None, None, None, self.widget.on_zoom_in),
01751             ('ZoomOut', gtk.STOCK_ZOOM_OUT, None, None, None, self.widget.on_zoom_out),
01752             ('ZoomFit', gtk.STOCK_ZOOM_FIT, None, None, None, self.widget.on_zoom_fit),
01753             ('Zoom100', gtk.STOCK_ZOOM_100, None, None, None, self.widget.on_zoom_100),
01754         ))
01755 
01756         # Add the actiongroup to the uimanager
01757         uimanager.insert_action_group(actiongroup, 0)
01758 
01759         # Add a UI descrption
01760         uimanager.add_ui_from_string(self.ui)
01761 
01762         # Create a Toolbar
01763         toolbar = uimanager.get_widget('/ToolBar')
01764         vbox.pack_start(toolbar, False)
01765 
01766         vbox.pack_start(self.widget)
01767 
01768         self.set_focus(self.widget)
01769 
01770         self.show_all()
01771 
01772     def update(self, filename):
01773         import os
01774         if not hasattr(self, "last_mtime"):
01775             self.last_mtime = None
01776 
01777         current_mtime = os.stat(filename).st_mtime
01778         if current_mtime != self.last_mtime:
01779             self.last_mtime = current_mtime
01780             self.open_file(filename,True)
01781 
01782         return True
01783 
01784     def set_filter(self, filter):
01785         self.widget.set_filter(filter)
01786 
01787     def set_dotcode(self, dotcode, filename='<stdin>',refresh=False):
01788         if self.widget.set_dotcode(dotcode, filename):
01789             self.set_title(os.path.basename(filename) + ' - Dot Viewer')
01790             if not refresh:
01791                 self.widget.zoom_to_fit()
01792 
01793     def set_xdotcode(self, xdotcode, filename='<stdin>'):
01794         if self.widget.set_xdotcode(xdotcode):
01795             self.set_title(os.path.basename(filename) + ' - Dot Viewer')
01796             self.widget.zoom_to_fit()
01797 
01798     def open_file(self, filename, refresh=False):
01799         try:
01800             fp = file(filename, 'rt')
01801             self.set_dotcode(fp.read(), filename ,refresh)
01802             fp.close()
01803         except IOError, ex:
01804             dlg = gtk.MessageDialog(type=gtk.MESSAGE_ERROR,
01805                                     message_format=str(ex),
01806                                     buttons=gtk.BUTTONS_OK)
01807             dlg.set_title('Dot Viewer')
01808             dlg.run()
01809             dlg.destroy()
01810 
01811     def on_open(self, action):
01812         chooser = gtk.FileChooserDialog(title="Open dot File",
01813                                         action=gtk.FILE_CHOOSER_ACTION_OPEN,
01814                                         buttons=(gtk.STOCK_CANCEL,
01815                                                  gtk.RESPONSE_CANCEL,
01816                                                  gtk.STOCK_OPEN,
01817                                                  gtk.RESPONSE_OK))
01818         chooser.set_default_response(gtk.RESPONSE_OK)
01819         filter = gtk.FileFilter()
01820         filter.set_name("Graphviz dot files")
01821         filter.add_pattern("*.dot")
01822         chooser.add_filter(filter)
01823         filter = gtk.FileFilter()
01824         filter.set_name("All files")
01825         filter.add_pattern("*")
01826         chooser.add_filter(filter)
01827         if chooser.run() == gtk.RESPONSE_OK:
01828             filename = chooser.get_filename()
01829             chooser.destroy()
01830             self.open_file(filename)
01831         else:
01832             chooser.destroy()
01833 
01834     def on_reload(self, action):
01835         self.widget.reload()
01836 
01837 
01838 def main():
01839     import optparse
01840 
01841     parser = optparse.OptionParser(
01842         usage='\n\t%prog [file]',
01843         version='%%prog %s' % __version__)
01844     parser.add_option(
01845         '-f', '--filter',
01846         type='choice', choices=('dot', 'neato', 'twopi', 'circo', 'fdp'),
01847         dest='filter', default='dot',
01848         help='graphviz filter: dot, neato, twopi, circo, or fdp [default: %default]')
01849 
01850     (options, args) = parser.parse_args(sys.argv[1:])
01851     if len(args) > 1:
01852         parser.error('incorrect number of arguments')
01853 
01854     win = DotWindow()
01855     win.connect('destroy', gtk.main_quit)
01856     win.set_filter(options.filter)
01857     if len(args) >= 1:
01858         if args[0] == '-':
01859             win.set_dotcode(sys.stdin.read())
01860         else:
01861             win.open_file(args[0])
01862             gobject.timeout_add(1000, win.update, args[0])
01863     gtk.main()
01864 
01865 
01866 # Apache-Style Software License for ColorBrewer software and ColorBrewer Color
01867 # Schemes, Version 1.1
01868 # 
01869 # Copyright (c) 2002 Cynthia Brewer, Mark Harrower, and The Pennsylvania State
01870 # University. All rights reserved.
01871 # 
01872 # Redistribution and use in source and binary forms, with or without
01873 # modification, are permitted provided that the following conditions are met:
01874 # 
01875 #    1. Redistributions as source code must retain the above copyright notice,
01876 #    this list of conditions and the following disclaimer.  
01877 #
01878 #    2. The end-user documentation included with the redistribution, if any,
01879 #    must include the following acknowledgment:
01880 # 
01881 #       This product includes color specifications and designs developed by
01882 #       Cynthia Brewer (http://colorbrewer.org/).
01883 # 
01884 #    Alternately, this acknowledgment may appear in the software itself, if and
01885 #    wherever such third-party acknowledgments normally appear.  
01886 #
01887 #    3. The name "ColorBrewer" must not be used to endorse or promote products
01888 #    derived from this software without prior written permission. For written
01889 #    permission, please contact Cynthia Brewer at cbrewer@psu.edu.
01890 #
01891 #    4. Products derived from this software may not be called "ColorBrewer",
01892 #    nor may "ColorBrewer" appear in their name, without prior written
01893 #    permission of Cynthia Brewer. 
01894 # 
01895 # THIS SOFTWARE IS PROVIDED "AS IS" AND ANY EXPRESSED OR IMPLIED WARRANTIES,
01896 # INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
01897 # FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL CYNTHIA
01898 # BREWER, MARK HARROWER, OR THE PENNSYLVANIA STATE UNIVERSITY BE LIABLE FOR ANY
01899 # DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
01900 # (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
01901 # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
01902 # ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
01903 # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
01904 # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 
01905 brewer_colors = {
01906     'accent3': [(127, 201, 127), (190, 174, 212), (253, 192, 134)],
01907     'accent4': [(127, 201, 127), (190, 174, 212), (253, 192, 134), (255, 255, 153)],
01908     'accent5': [(127, 201, 127), (190, 174, 212), (253, 192, 134), (255, 255, 153), (56, 108, 176)],
01909     'accent6': [(127, 201, 127), (190, 174, 212), (253, 192, 134), (255, 255, 153), (56, 108, 176), (240, 2, 127)],
01910     'accent7': [(127, 201, 127), (190, 174, 212), (253, 192, 134), (255, 255, 153), (56, 108, 176), (240, 2, 127), (191, 91, 23)],
01911     '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)],
01912     'blues3': [(222, 235, 247), (158, 202, 225), (49, 130, 189)],
01913     'blues4': [(239, 243, 255), (189, 215, 231), (107, 174, 214), (33, 113, 181)],
01914     'blues5': [(239, 243, 255), (189, 215, 231), (107, 174, 214), (49, 130, 189), (8, 81, 156)],
01915     'blues6': [(239, 243, 255), (198, 219, 239), (158, 202, 225), (107, 174, 214), (49, 130, 189), (8, 81, 156)],
01916     'blues7': [(239, 243, 255), (198, 219, 239), (158, 202, 225), (107, 174, 214), (66, 146, 198), (33, 113, 181), (8, 69, 148)],
01917     '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)],
01918     '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)],
01919     '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)],
01920     '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)],
01921     'brbg3': [(216, 179, 101), (245, 245, 245), (90, 180, 172)],
01922     'brbg4': [(166, 97, 26), (223, 194, 125), (128, 205, 193), (1, 133, 113)],
01923     'brbg5': [(166, 97, 26), (223, 194, 125), (245, 245, 245), (128, 205, 193), (1, 133, 113)],
01924     'brbg6': [(140, 81, 10), (216, 179, 101), (246, 232, 195), (199, 234, 229), (90, 180, 172), (1, 102, 94)],
01925     'brbg7': [(140, 81, 10), (216, 179, 101), (246, 232, 195), (245, 245, 245), (199, 234, 229), (90, 180, 172), (1, 102, 94)],
01926     '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)],
01927     '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)],
01928     'bugn3': [(229, 245, 249), (153, 216, 201), (44, 162, 95)],
01929     'bugn4': [(237, 248, 251), (178, 226, 226), (102, 194, 164), (35, 139, 69)],
01930     'bugn5': [(237, 248, 251), (178, 226, 226), (102, 194, 164), (44, 162, 95), (0, 109, 44)],
01931     'bugn6': [(237, 248, 251), (204, 236, 230), (153, 216, 201), (102, 194, 164), (44, 162, 95), (0, 109, 44)],
01932     'bugn7': [(237, 248, 251), (204, 236, 230), (153, 216, 201), (102, 194, 164), (65, 174, 118), (35, 139, 69), (0, 88, 36)],
01933     '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)],
01934     '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)],
01935     'bupu3': [(224, 236, 244), (158, 188, 218), (136, 86, 167)],
01936     'bupu4': [(237, 248, 251), (179, 205, 227), (140, 150, 198), (136, 65, 157)],
01937     'bupu5': [(237, 248, 251), (179, 205, 227), (140, 150, 198), (136, 86, 167), (129, 15, 124)],
01938     'bupu6': [(237, 248, 251), (191, 211, 230), (158, 188, 218), (140, 150, 198), (136, 86, 167), (129, 15, 124)],
01939     'bupu7': [(237, 248, 251), (191, 211, 230), (158, 188, 218), (140, 150, 198), (140, 107, 177), (136, 65, 157), (110, 1, 107)],
01940     '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)],
01941     '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)],
01942     'dark23': [(27, 158, 119), (217, 95, 2), (117, 112, 179)],
01943     'dark24': [(27, 158, 119), (217, 95, 2), (117, 112, 179), (231, 41, 138)],
01944     'dark25': [(27, 158, 119), (217, 95, 2), (117, 112, 179), (231, 41, 138), (102, 166, 30)],
01945     'dark26': [(27, 158, 119), (217, 95, 2), (117, 112, 179), (231, 41, 138), (102, 166, 30), (230, 171, 2)],
01946     'dark27': [(27, 158, 119), (217, 95, 2), (117, 112, 179), (231, 41, 138), (102, 166, 30), (230, 171, 2), (166, 118, 29)],
01947     '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)],
01948     'gnbu3': [(224, 243, 219), (168, 221, 181), (67, 162, 202)],
01949     'gnbu4': [(240, 249, 232), (186, 228, 188), (123, 204, 196), (43, 140, 190)],
01950     'gnbu5': [(240, 249, 232), (186, 228, 188), (123, 204, 196), (67, 162, 202), (8, 104, 172)],
01951     'gnbu6': [(240, 249, 232), (204, 235, 197), (168, 221, 181), (123, 204, 196), (67, 162, 202), (8, 104, 172)],
01952     'gnbu7': [(240, 249, 232), (204, 235, 197), (168, 221, 181), (123, 204, 196), (78, 179, 211), (43, 140, 190), (8, 88, 158)],
01953     '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)],
01954     '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)],
01955     'greens3': [(229, 245, 224), (161, 217, 155), (49, 163, 84)],
01956     'greens4': [(237, 248, 233), (186, 228, 179), (116, 196, 118), (35, 139, 69)],
01957     'greens5': [(237, 248, 233), (186, 228, 179), (116, 196, 118), (49, 163, 84), (0, 109, 44)],
01958     'greens6': [(237, 248, 233), (199, 233, 192), (161, 217, 155), (116, 196, 118), (49, 163, 84), (0, 109, 44)],
01959     'greens7': [(237, 248, 233), (199, 233, 192), (161, 217, 155), (116, 196, 118), (65, 171, 93), (35, 139, 69), (0, 90, 50)],
01960     '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)],
01961     '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)],
01962     'greys3': [(240, 240, 240), (189, 189, 189), (99, 99, 99)],
01963     'greys4': [(247, 247, 247), (204, 204, 204), (150, 150, 150), (82, 82, 82)],
01964     'greys5': [(247, 247, 247), (204, 204, 204), (150, 150, 150), (99, 99, 99), (37, 37, 37)],
01965     'greys6': [(247, 247, 247), (217, 217, 217), (189, 189, 189), (150, 150, 150), (99, 99, 99), (37, 37, 37)],
01966     'greys7': [(247, 247, 247), (217, 217, 217), (189, 189, 189), (150, 150, 150), (115, 115, 115), (82, 82, 82), (37, 37, 37)],
01967     '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)],
01968     '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)],
01969     'oranges3': [(254, 230, 206), (253, 174, 107), (230, 85, 13)],
01970     'oranges4': [(254, 237, 222), (253, 190, 133), (253, 141, 60), (217, 71, 1)],
01971     'oranges5': [(254, 237, 222), (253, 190, 133), (253, 141, 60), (230, 85, 13), (166, 54, 3)],
01972     'oranges6': [(254, 237, 222), (253, 208, 162), (253, 174, 107), (253, 141, 60), (230, 85, 13), (166, 54, 3)],
01973     'oranges7': [(254, 237, 222), (253, 208, 162), (253, 174, 107), (253, 141, 60), (241, 105, 19), (217, 72, 1), (140, 45, 4)],
01974     '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)],
01975     '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)],
01976     'orrd3': [(254, 232, 200), (253, 187, 132), (227, 74, 51)],
01977     'orrd4': [(254, 240, 217), (253, 204, 138), (252, 141, 89), (215, 48, 31)],
01978     'orrd5': [(254, 240, 217), (253, 204, 138), (252, 141, 89), (227, 74, 51), (179, 0, 0)],
01979     'orrd6': [(254, 240, 217), (253, 212, 158), (253, 187, 132), (252, 141, 89), (227, 74, 51), (179, 0, 0)],
01980     'orrd7': [(254, 240, 217), (253, 212, 158), (253, 187, 132), (252, 141, 89), (239, 101, 72), (215, 48, 31), (153, 0, 0)],
01981     '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)],
01982     '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)],
01983     '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)],
01984     '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)],
01985     '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)],
01986     'paired3': [(166, 206, 227), (31, 120, 180), (178, 223, 138)],
01987     'paired4': [(166, 206, 227), (31, 120, 180), (178, 223, 138), (51, 160, 44)],
01988     'paired5': [(166, 206, 227), (31, 120, 180), (178, 223, 138), (51, 160, 44), (251, 154, 153)],
01989     'paired6': [(166, 206, 227), (31, 120, 180), (178, 223, 138), (51, 160, 44), (251, 154, 153), (227, 26, 28)],
01990     'paired7': [(166, 206, 227), (31, 120, 180), (178, 223, 138), (51, 160, 44), (251, 154, 153), (227, 26, 28), (253, 191, 111)],
01991     '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)],
01992     '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)],
01993     'pastel13': [(251, 180, 174), (179, 205, 227), (204, 235, 197)],
01994     'pastel14': [(251, 180, 174), (179, 205, 227), (204, 235, 197), (222, 203, 228)],
01995     'pastel15': [(251, 180, 174), (179, 205, 227), (204, 235, 197), (222, 203, 228), (254, 217, 166)],
01996     'pastel16': [(251, 180, 174), (179, 205, 227), (204, 235, 197), (222, 203, 228), (254, 217, 166), (255, 255, 204)],
01997     'pastel17': [(251, 180, 174), (179, 205, 227), (204, 235, 197), (222, 203, 228), (254, 217, 166), (255, 255, 204), (229, 216, 189)],
01998     '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)],
01999     '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)],
02000     'pastel23': [(179, 226, 205), (253, 205, 172), (203, 213, 232)],
02001     'pastel24': [(179, 226, 205), (253, 205, 172), (203, 213, 232), (244, 202, 228)],
02002     'pastel25': [(179, 226, 205), (253, 205, 172), (203, 213, 232), (244, 202, 228), (230, 245, 201)],
02003     'pastel26': [(179, 226, 205), (253, 205, 172), (203, 213, 232), (244, 202, 228), (230, 245, 201), (255, 242, 174)],
02004     'pastel27': [(179, 226, 205), (253, 205, 172), (203, 213, 232), (244, 202, 228), (230, 245, 201), (255, 242, 174), (241, 226, 204)],
02005     '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)],
02006     '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)],
02007     '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)],
02008     'piyg3': [(233, 163, 201), (247, 247, 247), (161, 215, 106)],
02009     'piyg4': [(208, 28, 139), (241, 182, 218), (184, 225, 134), (77, 172, 38)],
02010     'piyg5': [(208, 28, 139), (241, 182, 218), (247, 247, 247), (184, 225, 134), (77, 172, 38)],
02011     'piyg6': [(197, 27, 125), (233, 163, 201), (253, 224, 239), (230, 245, 208), (161, 215, 106), (77, 146, 33)],
02012     'piyg7': [(197, 27, 125), (233, 163, 201), (253, 224, 239), (247, 247, 247), (230, 245, 208), (161, 215, 106), (77, 146, 33)],
02013     '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)],
02014     '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)],
02015     '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)],
02016     '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)],
02017     'prgn3': [(175, 141, 195), (247, 247, 247), (127, 191, 123)],
02018     'prgn4': [(123, 50, 148), (194, 165, 207), (166, 219, 160), (0, 136, 55)],
02019     'prgn5': [(123, 50, 148), (194, 165, 207), (247, 247, 247), (166, 219, 160), (0, 136, 55)],
02020     'prgn6': [(118, 42, 131), (175, 141, 195), (231, 212, 232), (217, 240, 211), (127, 191, 123), (27, 120, 55)],
02021     'prgn7': [(118, 42, 131), (175, 141, 195), (231, 212, 232), (247, 247, 247), (217, 240, 211), (127, 191, 123), (27, 120, 55)],
02022     '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)],
02023     '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)],
02024     'pubu3': [(236, 231, 242), (166, 189, 219), (43, 140, 190)],
02025     'pubu4': [(241, 238, 246), (189, 201, 225), (116, 169, 207), (5, 112, 176)],
02026     'pubu5': [(241, 238, 246), (189, 201, 225), (116, 169, 207), (43, 140, 190), (4, 90, 141)],
02027     'pubu6': [(241, 238, 246), (208, 209, 230), (166, 189, 219), (116, 169, 207), (43, 140, 190), (4, 90, 141)],
02028     'pubu7': [(241, 238, 246), (208, 209, 230), (166, 189, 219), (116, 169, 207), (54, 144, 192), (5, 112, 176), (3, 78, 123)],
02029     '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)],
02030     '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)],
02031     'pubugn3': [(236, 226, 240), (166, 189, 219), (28, 144, 153)],
02032     'pubugn4': [(246, 239, 247), (189, 201, 225), (103, 169, 207), (2, 129, 138)],
02033     'pubugn5': [(246, 239, 247), (189, 201, 225), (103, 169, 207), (28, 144, 153), (1, 108, 89)],
02034     'pubugn6': [(246, 239, 247), (208, 209, 230), (166, 189, 219), (103, 169, 207), (28, 144, 153), (1, 108, 89)],
02035     'pubugn7': [(246, 239, 247), (208, 209, 230), (166, 189, 219), (103, 169, 207), (54, 144, 192), (2, 129, 138), (1, 100, 80)],
02036     '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)],
02037     '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)],
02038     '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)],
02039     '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)],
02040     'puor3': [(241, 163, 64), (247, 247, 247), (153, 142, 195)],
02041     'puor4': [(230, 97, 1), (253, 184, 99), (178, 171, 210), (94, 60, 153)],
02042     'puor5': [(230, 97, 1), (253, 184, 99), (247, 247, 247), (178, 171, 210), (94, 60, 153)],
02043     'puor6': [(179, 88, 6), (241, 163, 64), (254, 224, 182), (216, 218, 235), (153, 142, 195), (84, 39, 136)],
02044     'puor7': [(179, 88, 6), (241, 163, 64), (254, 224, 182), (247, 247, 247), (216, 218, 235), (153, 142, 195), (84, 39, 136)],
02045     '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)],
02046     '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)],
02047     'purd3': [(231, 225, 239), (201, 148, 199), (221, 28, 119)],
02048     'purd4': [(241, 238, 246), (215, 181, 216), (223, 101, 176), (206, 18, 86)],
02049     'purd5': [(241, 238, 246), (215, 181, 216), (223, 101, 176), (221, 28, 119), (152, 0, 67)],
02050     'purd6': [(241, 238, 246), (212, 185, 218), (201, 148, 199), (223, 101, 176), (221, 28, 119), (152, 0, 67)],
02051     'purd7': [(241, 238, 246), (212, 185, 218), (201, 148, 199), (223, 101, 176), (231, 41, 138), (206, 18, 86), (145, 0, 63)],
02052     '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)],
02053     '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)],
02054     'purples3': [(239, 237, 245), (188, 189, 220), (117, 107, 177)],
02055     'purples4': [(242, 240, 247), (203, 201, 226), (158, 154, 200), (106, 81, 163)],
02056     'purples5': [(242, 240, 247), (203, 201, 226), (158, 154, 200), (117, 107, 177), (84, 39, 143)],
02057     'purples6': [(242, 240, 247), (218, 218, 235), (188, 189, 220), (158, 154, 200), (117, 107, 177), (84, 39, 143)],
02058     'purples7': [(242, 240, 247), (218, 218, 235), (188, 189, 220), (158, 154, 200), (128, 125, 186), (106, 81, 163), (74, 20, 134)],
02059     '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)],
02060     '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)],
02061     '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)],
02062     '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)],
02063     'rdbu3': [(239, 138, 98), (247, 247, 247), (103, 169, 207)],
02064     'rdbu4': [(202, 0, 32), (244, 165, 130), (146, 197, 222), (5, 113, 176)],
02065     'rdbu5': [(202, 0, 32), (244, 165, 130), (247, 247, 247), (146, 197, 222), (5, 113, 176)],
02066     'rdbu6': [(178, 24, 43), (239, 138, 98), (253, 219, 199), (209, 229, 240), (103, 169, 207), (33, 102, 172)],
02067     'rdbu7': [(178, 24, 43), (239, 138, 98), (253, 219, 199), (247, 247, 247), (209, 229, 240), (103, 169, 207), (33, 102, 172)],
02068     '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)],
02069     '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)],
02070     '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)],
02071     '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)],
02072     'rdgy3': [(239, 138, 98), (255, 255, 255), (153, 153, 153)],
02073     'rdgy4': [(202, 0, 32), (244, 165, 130), (186, 186, 186), (64, 64, 64)],
02074     'rdgy5': [(202, 0, 32), (244, 165, 130), (255, 255, 255), (186, 186, 186), (64, 64, 64)],
02075     'rdgy6': [(178, 24, 43), (239, 138, 98), (253, 219, 199), (224, 224, 224), (153, 153, 153), (77, 77, 77)],
02076     'rdgy7': [(178, 24, 43), (239, 138, 98), (253, 219, 199), (255, 255, 255), (224, 224, 224), (153, 153, 153), (77, 77, 77)],
02077     '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)],
02078     '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)],
02079     'rdpu3': [(253, 224, 221), (250, 159, 181), (197, 27, 138)],
02080     'rdpu4': [(254, 235, 226), (251, 180, 185), (247, 104, 161), (174, 1, 126)],
02081     'rdpu5': [(254, 235, 226), (251, 180, 185), (247, 104, 161), (197, 27, 138), (122, 1, 119)],
02082     'rdpu6': [(254, 235, 226), (252, 197, 192), (250, 159, 181), (247, 104, 161), (197, 27, 138), (122, 1, 119)],
02083     'rdpu7': [(254, 235, 226), (252, 197, 192), (250, 159, 181), (247, 104, 161), (221, 52, 151), (174, 1, 126), (122, 1, 119)],
02084     '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)],
02085     '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)],
02086     '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)],
02087     '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)],
02088     'rdylbu3': [(252, 141, 89), (255, 255, 191), (145, 191, 219)],
02089     'rdylbu4': [(215, 25, 28), (253, 174, 97), (171, 217, 233), (44, 123, 182)],
02090     'rdylbu5': [(215, 25, 28), (253, 174, 97), (255, 255, 191), (171, 217, 233), (44, 123, 182)],
02091     'rdylbu6': [(215, 48, 39), (252, 141, 89), (254, 224, 144), (224, 243, 248), (145, 191, 219), (69, 117, 180)],
02092     'rdylbu7': [(215, 48, 39), (252, 141, 89), (254, 224, 144), (255, 255, 191), (224, 243, 248), (145, 191, 219), (69, 117, 180)],
02093     '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)],
02094     '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)],
02095     '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)],
02096     '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)],
02097     'rdylgn3': [(252, 141, 89), (255, 255, 191), (145, 207, 96)],
02098     'rdylgn4': [(215, 25, 28), (253, 174, 97), (166, 217, 106), (26, 150, 65)],
02099     'rdylgn5': [(215, 25, 28), (253, 174, 97), (255, 255, 191), (166, 217, 106), (26, 150, 65)],
02100     'rdylgn6': [(215, 48, 39), (252, 141, 89), (254, 224, 139), (217, 239, 139), (145, 207, 96), (26, 152, 80)],
02101     'rdylgn7': [(215, 48, 39), (252, 141, 89), (254, 224, 139), (255, 255, 191), (217, 239, 139), (145, 207, 96), (26, 152, 80)],
02102     '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)],
02103     '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)],
02104     'reds3': [(254, 224, 210), (252, 146, 114), (222, 45, 38)],
02105     'reds4': [(254, 229, 217), (252, 174, 145), (251, 106, 74), (203, 24, 29)],
02106     'reds5': [(254, 229, 217), (252, 174, 145), (251, 106, 74), (222, 45, 38), (165, 15, 21)],
02107     'reds6': [(254, 229, 217), (252, 187, 161), (252, 146, 114), (251, 106, 74), (222, 45, 38), (165, 15, 21)],
02108     'reds7': [(254, 229, 217), (252, 187, 161), (252, 146, 114), (251, 106, 74), (239, 59, 44), (203, 24, 29), (153, 0, 13)],
02109     '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)],
02110     '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)],
02111     'set13': [(228, 26, 28), (55, 126, 184), (77, 175, 74)],
02112     'set14': [(228, 26, 28), (55, 126, 184), (77, 175, 74), (152, 78, 163)],
02113     'set15': [(228, 26, 28), (55, 126, 184), (77, 175, 74), (152, 78, 163), (255, 127, 0)],
02114     'set16': [(228, 26, 28), (55, 126, 184), (77, 175, 74), (152, 78, 163), (255, 127, 0), (255, 255, 51)],
02115     'set17': [(228, 26, 28), (55, 126, 184), (77, 175, 74), (152, 78, 163), (255, 127, 0), (255, 255, 51), (166, 86, 40)],
02116     '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)],
02117     '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)],
02118     'set23': [(102, 194, 165), (252, 141, 98), (141, 160, 203)],
02119     'set24': [(102, 194, 165), (252, 141, 98), (141, 160, 203), (231, 138, 195)],
02120     'set25': [(102, 194, 165), (252, 141, 98), (141, 160, 203), (231, 138, 195), (166, 216, 84)],
02121     'set26': [(102, 194, 165), (252, 141, 98), (141, 160, 203), (231, 138, 195), (166, 216, 84), (255, 217, 47)],
02122     'set27': [(102, 194, 165), (252, 141, 98), (141, 160, 203), (231, 138, 195), (166, 216, 84), (255, 217, 47), (229, 196, 148)],
02123     '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)],
02124     '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)],
02125     '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)],
02126     '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)],
02127     'set33': [(141, 211, 199), (255, 255, 179), (190, 186, 218)],
02128     'set34': [(141, 211, 199), (255, 255, 179), (190, 186, 218), (251, 128, 114)],
02129     'set35': [(141, 211, 199), (255, 255, 179), (190, 186, 218), (251, 128, 114), (128, 177, 211)],
02130     'set36': [(141, 211, 199), (255, 255, 179), (190, 186, 218), (251, 128, 114), (128, 177, 211), (253, 180, 98)],
02131     'set37': [(141, 211, 199), (255, 255, 179), (190, 186, 218), (251, 128, 114), (128, 177, 211), (253, 180, 98), (179, 222, 105)],
02132     '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)],
02133     '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)],
02134     '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)],
02135     '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)],
02136     'spectral3': [(252, 141, 89), (255, 255, 191), (153, 213, 148)],
02137     'spectral4': [(215, 25, 28), (253, 174, 97), (171, 221, 164), (43, 131, 186)],
02138     'spectral5': [(215, 25, 28), (253, 174, 97), (255, 255, 191), (171, 221, 164), (43, 131, 186)],
02139     'spectral6': [(213, 62, 79), (252, 141, 89), (254, 224, 139), (230, 245, 152), (153, 213, 148), (50, 136, 189)],
02140     'spectral7': [(213, 62, 79), (252, 141, 89), (254, 224, 139), (255, 255, 191), (230, 245, 152), (153, 213, 148), (50, 136, 189)],
02141     '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)],
02142     '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)],
02143     'ylgn3': [(247, 252, 185), (173, 221, 142), (49, 163, 84)],
02144     'ylgn4': [(255, 255, 204), (194, 230, 153), (120, 198, 121), (35, 132, 67)],
02145     'ylgn5': [(255, 255, 204), (194, 230, 153), (120, 198, 121), (49, 163, 84), (0, 104, 55)],
02146     'ylgn6': [(255, 255, 204), (217, 240, 163), (173, 221, 142), (120, 198, 121), (49, 163, 84), (0, 104, 55)],
02147     'ylgn7': [(255, 255, 204), (217, 240, 163), (173, 221, 142), (120, 198, 121), (65, 171, 93), (35, 132, 67), (0, 90, 50)],
02148     '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)],
02149     '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)],
02150     'ylgnbu3': [(237, 248, 177), (127, 205, 187), (44, 127, 184)],
02151     'ylgnbu4': [(255, 255, 204), (161, 218, 180), (65, 182, 196), (34, 94, 168)],
02152     'ylgnbu5': [(255, 255, 204), (161, 218, 180), (65, 182, 196), (44, 127, 184), (37, 52, 148)],
02153     'ylgnbu6': [(255, 255, 204), (199, 233, 180), (127, 205, 187), (65, 182, 196), (44, 127, 184), (37, 52, 148)],
02154     'ylgnbu7': [(255, 255, 204), (199, 233, 180), (127, 205, 187), (65, 182, 196), (29, 145, 192), (34, 94, 168), (12, 44, 132)],
02155     '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)],
02156     '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)],
02157     'ylorbr3': [(255, 247, 188), (254, 196, 79), (217, 95, 14)],
02158     'ylorbr4': [(255, 255, 212), (254, 217, 142), (254, 153, 41), (204, 76, 2)],
02159     'ylorbr5': [(255, 255, 212), (254, 217, 142), (254, 153, 41), (217, 95, 14), (153, 52, 4)],
02160     'ylorbr6': [(255, 255, 212), (254, 227, 145), (254, 196, 79), (254, 153, 41), (217, 95, 14), (153, 52, 4)],
02161     'ylorbr7': [(255, 255, 212), (254, 227, 145), (254, 196, 79), (254, 153, 41), (236, 112, 20), (204, 76, 2), (140, 45, 4)],
02162     '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)],
02163     '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)],
02164     'ylorrd3': [(255, 237, 160), (254, 178, 76), (240, 59, 32)],
02165     'ylorrd4': [(255, 255, 178), (254, 204, 92), (253, 141, 60), (227, 26, 28)],
02166     'ylorrd5': [(255, 255, 178), (254, 204, 92), (253, 141, 60), (240, 59, 32), (189, 0, 38)],
02167     'ylorrd6': [(255, 255, 178), (254, 217, 118), (254, 178, 76), (253, 141, 60), (240, 59, 32), (189, 0, 38)],
02168     'ylorrd7': [(255, 255, 178), (254, 217, 118), (254, 178, 76), (253, 141, 60), (252, 78, 42), (227, 26, 28), (177, 0, 38)],
02169     '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)],
02170 }
02171 
02172 
02173 if __name__ == '__main__':
02174     main()


xdot
Author(s): Jose Fonseca, ROS package and WX frontend by Jonathan Bohren
autogenerated on Mon Oct 6 2014 07:25:57