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