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