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


cob_script_server
Author(s): Florian Weisshardt
autogenerated on Thu Aug 27 2015 12:42:55