[Epydoc-commits] SF.net SVN: epydoc: [1190] trunk/epydoc/src/epydoc/docwriter/dotgraph.py
Brought to you by:
edloper
From: <ed...@us...> - 2006-04-07 23:38:54
|
Revision: 1190 Author: edloper Date: 2006-04-07 16:38:47 -0700 (Fri, 07 Apr 2006) ViewCVS: http://svn.sourceforge.net/epydoc/?rev=1190&view=rev Log Message: ----------- - Added 'center' option to DotGraph.to_html() - If graphviz dot prints to stderr, generate a warning with that output. - DotGraphNode's can now define a 'port' variable; edges connecting to it will connect to that port, unless otherwise specified. - Added 'uml' style for package_tree: draws the package as a nested set of uml package symbols. - Decrased minimum inter-node spacing in import graph - selected elements now use bold outlines (for some reason, filled,rounded style isn't working with my version of dot) Modified Paths: -------------- trunk/epydoc/src/epydoc/docwriter/dotgraph.py Modified: trunk/epydoc/src/epydoc/docwriter/dotgraph.py =================================================================== --- trunk/epydoc/src/epydoc/docwriter/dotgraph.py 2006-04-07 23:35:25 UTC (rev 1189) +++ trunk/epydoc/src/epydoc/docwriter/dotgraph.py 2006-04-07 23:38:47 UTC (rev 1190) @@ -116,7 +116,7 @@ self.uid = '%s_%s' % (self.uid, n) self._uids.add(self.uid) - def to_html(self, image_url): + def to_html(self, image_url, center=True): """ Return the HTML code that should be uesd to display this graph (including a client-side image map). @@ -138,12 +138,13 @@ title_align = 'center' table_width = '' - s = '<center>' + if center: s = '<center>' if title or caption: - s += ('<table border="0" cellpadding="0" cellspacing="0" ' - '%s>\n <tr><td align="center">\n') % table_width + s += ('<p><table border="0" cellpadding="0" cellspacing="0" ' + 'class="graph"%s>\n <tr><td align="center">\n' % + table_width) s += (' %s\n <img src="%s" alt=%r usemap="#%s" ' - 'ismap="ismap" class=%s">\n' % + 'ismap="ismap" class="%s">\n' % (cmapx.strip(), image_url, title, self.uid, css_class)) if title or caption: s += ' </td></tr>\n <tr><td align=%r>\n' % title_align @@ -153,8 +154,8 @@ s += ' -- ' if caption: s += '<span class="graph-caption">%s</span>' % caption - s += '\n </th></tr>\n</table>' - s += '</center>' + s += '\n </th></tr>\n</table></p>' + if center: s += '</center>' return s def link(self, docstring_linker): @@ -217,6 +218,8 @@ # Decode into unicode, if necessary. if language == 'cmapx' and result is not None: result = result.decode('utf-8') + if err: + log.warning("Graphviz dot warning(s):\n%s" % err) except OSError, e: log.warning("Unable to render Graphviz dot graph:\n%s" % e) #log.debug(self.to_dotfile()) @@ -257,6 +260,7 @@ self._attribs = attribs self.id = self.__class__._next_id self.__class__._next_id += 1 + self.port = None def __getitem__(self, attr): return self._attribs[attr] @@ -275,7 +279,7 @@ """ attribs = ['%s="%s"' % (k,v) for (k,v) in self._attribs.items()] if self._html_label: - attribs.insert(0, 'label=<%s>' % self._html_label) + attribs.insert(0, 'label=<%s>' % (self._html_label,)) if attribs: attribs = ' [%s]' % (','.join(attribs)) return 'node%d%s' % (self.id, attribs) @@ -300,9 +304,16 @@ """ Return the dot commands that should be used to render this edge. """ - attribs = ','.join(['%s="%s"' % (k,v) for (k,v) - in self._attribs.items()]) + # Set head & tail ports, if the nodes have preferred ports. + attribs = self._attribs.copy() + if (self.start.port is not None and 'headport' not in attribs): + attribs['headport'] = self.start.port + if (self.end.port is not None and 'tailport' not in attribs): + attribs['tailport'] = self.end.port + # Convert attribs to a string + attribs = ','.join(['%s="%s"' % (k,v) for (k,v) in attribs.items()]) if attribs: attribs = ' [%s]' % attribs + # Return the dotfile edge. return 'node%d -> node%d%s' % (self.start.id, self.end.id, attribs) ###################################################################### @@ -314,12 +325,20 @@ Return a `DotGraph` that graphically displays the package hierarchies for the given packages. """ + if options.get('style', 'uml') == 'uml': # default to uml style? + if get_dot_version() >= [2]: + return _nested_package_uml_graph(packages, linker, context, + **options) + elif 'style' in options: + log.warning('UML style package trees require dot version 2.0+') + graph = DotGraph('Package Tree for %s' % name_list(packages, context), - edge_defaults={'sametail':True}) + body='ranksep=.3\n;nodesep=.1\n', + edge_defaults={'dir':'none'}) # Options - if options.get('dir', 'LR') != 'TB': # default: left-to-right - graph.body += 'rankdir=%s\n' % options.get('dir', 'LR') + if options.get('dir', 'TB') != 'TB': # default: top-to-bottom + graph.body += 'rankdir=%s\n' % options.get('dir', 'TB') # Get a list of all modules in the package. queue = list(packages) @@ -334,11 +353,111 @@ # Add an edge for each package/submodule relationship. for module in modules: for submodule in module.submodules: - graph.edges.append(DotGraphEdge(nodes[module], nodes[submodule])) + graph.edges.append(DotGraphEdge(nodes[module], nodes[submodule], + headport='tab')) return graph +def _nested_package_uml_graph(packages, linker, context=None, **options): + """ + Return a `DotGraph` that graphically displays the package + hierarchies for the given packages as a nested set of UML + symbols. + """ + graph = DotGraph('Package Tree for %s' % name_list(packages, context)) + # Remove any packages whose containers are also in the list. + root_packages = [] + for package1 in packages: + for package2 in packages: + if (package1 is not package2 and + package2.canonical_name.dominates(package1.canonical_name)): + break + else: + root_packages.append(package1) + # If the context is a variable, then get its value. + if isinstance(context, VariableDoc) and context.value is not UNKNOWN: + context = context.value + # Build one (complex) node for each root package. + for package in root_packages: + html_label, _, _ = _nested_uml_package_label(package, linker, context) + node = DotGraphNode(html_label=html_label, shape='plaintext', + url=linker.url_for(package), + tooltip=package.canonical_name) + graph.nodes.append(node) + return graph +def _nested_uml_package_label(package, linker, context): + """ + :Return: (label, depth, width) where: + + - `label` is the HTML label + - `depth` is the depth of the package tree + - `width` is the max width of the HTML label, roughly in + units of characters. + + :todo: Add hrefs/tooltips to appropriate <td> or <table> cells. + """ + MAX_ROW_WIDTH = 80 # unit is roughly characters. + pkg_name = package.canonical_name + pkg_url = linker.url_for(package) or NOOP_URL + + if not package.is_package or len(package.submodules) == 0: + pkg_color = _nested_uml_package_color(package, context, 1) + label = MODULE_NODE_HTML % (pkg_color, pkg_color, pkg_url, + pkg_name, pkg_name[-1]) + return (label, 1, len(pkg_name[-1])+3) + + submodule_labels = [_nested_uml_package_label(submodule, linker, context) + for submodule in package.submodules] + + ROW_HDR = '<TABLE BORDER="0" CELLBORDER="0"><TR>' + # Build the body of the package's icon. + body = '<TABLE BORDER="0" CELLBORDER="0">' + body += '<TR><TD ALIGN="LEFT">%s</TD></TR>' % pkg_name[-1] + body += '<TR><TD>%s' % ROW_HDR + row_width = [0] + for i, (label, depth, width) in enumerate(submodule_labels): + if row_width[-1] > 0 and width+row_width[-1] > MAX_ROW_WIDTH: + body += '</TR></TABLE></TD></TR>' + body += '<TR><TD>%s' % ROW_HDR + row_width.append(0) + #submodule_url = linker.url_for(package.submodules[i]) or '#' + #submodule_name = package.submodules[i].canonical_name + body += '<TD ALIGN="LEFT">%s</TD>' % label + row_width [-1] += width + body += '</TR></TABLE></TD></TR></TABLE>' + + # Put together our return value. + depth = max([d for (l,d,w) in submodule_labels])+1 + pkg_color = _nested_uml_package_color(package, context, depth) + label = ('<TABLE BORDER="0" CELLBORDER="0" CELLSPACING="0"><TR>' + '<TD ALIGN="LEFT" HEIGHT="8" WIDTH="16" FIXEDSIZE="true" ' + 'BORDER="1" VALIGN="BOTTOM" BGCOLOR="%s"></TD></TR><TR>' + '<TD COLSPAN="5" VALIGN="TOP" ALIGN="LEFT" BORDER="1" ' + 'BGCOLOR="%s" HREF="%s" TOOLTIP="%s">%s</TD></TR></TABLE>' % + (pkg_color, pkg_color, pkg_url, pkg_name, body)) + width = max(max(row_width), len(pkg_name[-1])+3) + return label, depth, width + +def _nested_uml_package_color(package, context, depth): + if package == context: return SELECTED_BG + else: + # Parse the base color. + if re.match(MODULE_BG, 'r#[0-9a-fA-F]{6}$'): + base = int(MODULE_BG[1:], 16) + else: + base = int('d8e8ff', 16) + red = (base & 0xff0000) >> 16 + green = (base & 0x00ff00) >> 8 + blue = (base & 0x0000ff) + # Make it darker with each level of depth. + red = max(0, red-(depth-1)*10) + green = max(0, green-(depth-1)*10) + blue = max(0, blue-(depth-1)*10) + # Convert it back to a color string + return '#%06x' % ((red<<16)+(green<<8)+blue) + +###################################################################### def class_tree_graph(bases, linker, context=None, **options): """ Return a `DotGraph` that graphically displays the package @@ -346,7 +465,6 @@ """ graph = DotGraph('Class Hierarchy for %s' % name_list(bases, context), body='ranksep=0.3\n', - node_defaults={'shape':'box', 'width': 0, 'height': 0}, edge_defaults={'sametail':True, 'dir':'none'}) # Options @@ -381,8 +499,9 @@ return graph +###################################################################### def import_graph(modules, docindex, linker, context=None, **options): - graph = DotGraph('Import Graph') + graph = DotGraph('Import Graph', body='ranksep=.3\n;nodesep=.3\n') # Options if options.get('dir', 'RL') != 'TB': # default: right-to-left. @@ -406,6 +525,7 @@ return graph +###################################################################### def call_graph(api_docs, docindex, linker, context=None, **options): """ :param options: @@ -513,12 +633,18 @@ specialize_valdoc_node(node, val_doc, context, linker.url_for(val_doc)) return nodes +NOOP_URL = '#' +NOOP_URL = 'javascript:;' # this option is more evil. + MODULE_NODE_HTML = ''' - <TABLE BORDER="0" CELLBORDER="1" CELLSPACING="0" CELLPADDING="0"> - <TR><TD BORDER="0"></TD><TD BORDER="0"></TD> - <TD HEIGHT="8" BGCOLOR="%s"></TD></TR> - <TR><TD COLSPAN="3" BGCOLOR="%s">%s</TD></TR></TABLE>'''.strip() -MODULE_BG = '#d0e0ff' + <TABLE BORDER="0" CELLBORDER="0" CELLSPACING="0" + CELLPADDING="0" PORT="table" ALIGN="LEFT"> + <TR><TD ALIGN="LEFT" VALIGN="BOTTOM" HEIGHT="8" WIDTH="16" FIXEDSIZE="true" + BGCOLOR="%s" BORDER="1" PORT="tab"></TD></TR> + <TR><TD ALIGN="LEFT" VALIGN="TOP" BGCOLOR="%s" BORDER="1" + PORT="body" HREF="%s" TOOLTIP="%s">%s</TD></TR> + </TABLE>'''.strip() +MODULE_BG = '#d8e8ff' SELECTED_BG = '#ffd0d0' def specialize_valdoc_node(node, val_doc, context, url): @@ -526,20 +652,29 @@ Update the style attributes of `node` to reflext its type and context. """ + # We can only use html-style nodes if dot_version>2. dot_version = get_dot_version() - if val_doc != context: - if url: node['href'] = url - + + # If val_doc or context is a variable, get its value. if isinstance(val_doc, VariableDoc) and val_doc.value is not UNKNOWN: val_doc = val_doc.value - - if isinstance(val_doc, ModuleDoc) and dot_version > [2]: + if isinstance(context, VariableDoc) and context.value is not UNKNOWN: + context = context.value + + # Set the URL. (Do this even if it points to the page we're + # currently on; otherwise, the tooltip is ignored.) + node['href'] = url or NOOP_URL + + if isinstance(val_doc, ModuleDoc) and dot_version >= [2]: node['shape'] = 'plaintext' if val_doc == context: color = SELECTED_BG else: color = MODULE_BG node['tooltip'] = node['label'] - node['html_label'] = MODULE_NODE_HTML % (color, color, node['label']) + node['html_label'] = MODULE_NODE_HTML % (color, color, url, + val_doc.canonical_name, + node['label']) node['width'] = node['height'] = 0 + node.port = 'body' elif isinstance(val_doc, RoutineDoc): node['shape'] = 'box' @@ -550,7 +685,7 @@ node['tooltip'] = node['label'] if val_doc == context: node['fillcolor'] = SELECTED_BG - node['style'] = 'filled,rounded' + node['style'] = 'filled,rounded,bold' else: node['shape'] = 'box' @@ -559,7 +694,7 @@ node['tooltip'] = node['label'] if val_doc == context: node['fillcolor'] = SELECTED_BG - node['style'] = 'filled' + node['style'] = 'filled,bold' def name_list(api_docs, context=None): if context is not None: This was sent by the SourceForge.net collaborative development platform, the world's largest Open Source development site. |