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