00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
00014
00015
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
00043
00044
00045
00046
00047
00048
00049 class Pen:
00050 """Store pen attributes."""
00051
00052 def __init__(self):
00053
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
00096
00097
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
00118
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
00128
00129 pass
00130
00131
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
00138 layout.set_text(self.t)
00139
00140
00141 self.layout = layout
00142 else:
00143 cr.update_layout(layout)
00144
00145 descent = 2
00146
00147 width, height = layout.get_size()
00148 width = float(width)/pango.SCALE
00149 height = float(height)/pango.SCALE
00150
00151
00152
00153 if width > self.w:
00154 f = self.w / width
00155 width = self.w
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:
00181
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
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
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
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
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, )
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
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
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
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
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
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
00740 buf = fp.read()
00741 pos = 0
00742 else:
00743
00744 if length:
00745
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
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
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
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
00859 tokens = [
00860
00861 (SKIP,
00862 r'[ \t\f\r\n\v]+|'
00863 r'//[^\r\n]*|'
00864 r'/\*.*?\*/|'
00865 r'#[^\r\n]*',
00866 False),
00867
00868
00869 (ID, r'[a-zA-Z_\x80-\xff][a-zA-Z0-9_\x80-\xff]*', True),
00870
00871
00872 (ID, r'-?(?:\.[0-9]+|[0-9]+(?:\.[0-9]*)?)', False),
00873
00874
00875 (STR_ID, r'"[^"\\]*(?:\\.[^"\\]*)*"', False),
00876
00877
00878 (HTML_ID, r'<[^<>]*(?:<[^<>]*>[^<>]*)*>', False),
00879
00880
00881 (EDGE_OP, r'-[>-]', False),
00882 ]
00883
00884
00885 symbols = {
00886 '[': LSQUARE,
00887 ']': RSQUARE,
00888 '{': LCURLY,
00889 '}': RCURLY,
00890 ',': COMMA,
00891 ':': COLON,
00892 ';': SEMI,
00893 '=': EQUAL,
00894 '+': PLUS,
00895 }
00896
00897
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
00916 if type == STR_ID:
00917 text = text[1:-1]
00918
00919
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
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
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
01153 continue
01154 else:
01155 points.append(self.transform(float(x), float(y)))
01156 return points
01157
01158 def transform(self, x, y):
01159
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
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
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
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):
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
01622 return False
01623
01624
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
01721 uimanager = self.uimanager = gtk.UIManager()
01722
01723
01724 accelgroup = uimanager.get_accel_group()
01725 window.add_accel_group(accelgroup)
01726
01727
01728 actiongroup = gtk.ActionGroup('Actions')
01729 self.actiongroup = actiongroup
01730
01731
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
01742 uimanager.insert_action_group(actiongroup, 0)
01743
01744
01745 uimanager.add_ui_from_string(self.ui)
01746
01747
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
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 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()