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


cob_script_server
Author(s): Florian Weisshardt
autogenerated on Sun Jun 9 2019 20:20:12