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