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