[PATCH 1 of 3] Added plot core functionality and default style template
Francesco Degrassi
francesco.degrassi at emaze.net
Mon Aug 16 18:22:14 CDT 2010
mercurial/hgweb/webcommands.py | 53 +++++-
mercurial/plotmod.py | 282 +++++++++++++++++++++++++++++
mercurial/templates/paper/map | 3 +
mercurial/templates/paper/plot.tmpl | 141 ++++++++++++++
mercurial/templates/static/plot.js | 221 ++++++++++++++++++++++
mercurial/templates/static/style-paper.css | 7 +
6 files changed, 706 insertions(+), 1 deletions(-)
# HG changeset patch
# User Francesco Degrassi <francesco.degrassi at emaze.net>
# Date 1281996489 -7200
# Node ID 3a80fde7f80045ac9dbc2c1961a5c3c2a2fc9ca3
# Parent 4e804302d30cfb8de0b5f42be6b9cc3f61db7cfc
Added plot core functionality and default style template
diff -r 4e804302d30c -r 3a80fde7f800 mercurial/hgweb/webcommands.py
--- a/mercurial/hgweb/webcommands.py Sun Aug 15 11:05:04 2010 +0200
+++ b/mercurial/hgweb/webcommands.py Tue Aug 17 00:08:09 2010 +0200
@@ -13,6 +13,7 @@
from common import paritygen, staticfile, get_contact, ErrorResponse
from common import HTTP_OK, HTTP_FORBIDDEN, HTTP_NOT_FOUND
from mercurial import graphmod
+from mercurial import plotmod
# __all__ is populated with the allowed commands. Be sure to add to it if
# you're adding a new command, or the new command won't work.
@@ -20,7 +21,7 @@
__all__ = [
'log', 'rawfile', 'file', 'changelog', 'shortlog', 'changeset', 'rev',
'manifest', 'tags', 'branches', 'summary', 'filediff', 'diff', 'annotate',
- 'filelog', 'archive', 'static', 'graph',
+ 'filelog', 'archive', 'static', 'graph','plot',
]
def log(web, req, tmpl):
@@ -671,6 +672,56 @@
archival.archive(web.repo, req, cnode, artype, prefix=name)
return []
+def plot(web, req, tmpl):
+ rev = webutil.changectx(web.repo, req).rev()
+ revcount = web.maxshortchanges
+ if 'revcount' in req.form:
+ revcount = int(req.form.get('revcount', [revcount])[0])
+ tmpl.defaults['sessionvars']['revcount'] = revcount
+ plot_width = 36
+ if 'plot_width' in req.form:
+ plot_width = int(req.form.get('plot_width', [plot_width])[0])
+ tmpl.defaults['sessionvars']['plot_width'] = plot_width
+
+ lessvars = copy.copy(tmpl.defaults['sessionvars'])
+ lessvars['revcount'] = revcount / 2
+ morevars = copy.copy(tmpl.defaults['sessionvars'])
+ morevars['revcount'] = revcount * 2
+ widervars = copy.copy(tmpl.defaults['sessionvars'])
+ widervars['plot_width'] = plot_width * 2
+ narrowervars = copy.copy(tmpl.defaults['sessionvars'])
+ narrowervars['plot_width'] = plot_width / 2
+
+ max_rev = len(web.repo) - 1
+ revcount = min(max_rev, revcount)
+ revnode = web.repo.changelog.node(rev)
+ revnode_hex = hex(revnode)
+ uprev = min(max_rev, rev + revcount)
+ downrev = max(0, rev - revcount)
+ count = len(web.repo)
+ changenav = webutil.revnavgen(rev, revcount, count, web.repo.changectx)
+
+ dag = graphmod.revisions(web.repo, rev, downrev)
+ tree = list(plotmod.plot(web.repo, dag))
+ data = []
+ for (column, color, closed, labeldata, ctx, branches, edges) in tree:
+ node = short(ctx.node())
+ date = templatefilters.time.ctime(ctx.date()[0])
+ age = "%s (%s)" % (date, templatefilters.age(ctx.date()))
+ desc = templatefilters.firstline(ctx.description())
+ desc = cgi.escape(templatefilters.nonempty(desc))
+ user = cgi.escape(templatefilters.person(ctx.user()))
+ branch = ctx.branch()
+ branch = branch, web.repo.branchtags().get(branch) == ctx.node()
+ data.append(((column, color, closed, labeldata,
+ node, desc, user, age, branch, ctx.tags()), branches, edges))
+
+ return tmpl('plot', rev=rev, revcount=revcount, uprev=uprev,
+ lessvars=lessvars, morevars=morevars, downrev=downrev,
+ jsdata=data,
+ node=revnode_hex, changenav=changenav, plot_width=plot_width,
+ narrowervars=narrowervars, widervars=widervars)
+
def static(web, req, tmpl):
fname = req.form['file'][0]
diff -r 4e804302d30c -r 3a80fde7f800 mercurial/plotmod.py
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/mercurial/plotmod.py Tue Aug 17 00:08:09 2010 +0200
@@ -0,0 +1,282 @@
+# Revision plot generator for Mercurial
+#
+# Copyright 2010 Francesco Degrassi <francesco.degrassi at emaze.net>
+# Copyright 2008 Dirkjan Ochtman <dirkjan at ochtman.nl>
+# Copyright 2007 Joel Rosdahl <joel at rosdahl.net>
+#
+# This software may be used and distributed according to the terms of the
+# GNU General Public License version 2 or any later version.
+
+"""Generates plot data from the DAG
+The data is enough to properly draw each named branch on its own column
+and with a single color.
+"""
+
+from copy import deepcopy
+
+def plot(repo, dag):
+ """Generates plot data from the DAG
+
+ For each DAG node this function emits tuples::
+
+ ((col, subcol), color, closed, (label, above), data, branches, edges)
+
+ with the following contents:
+ - Tuple (col, subcol): column and subcolumn index of the plot for the node
+ - color: color of node
+ - closed: True if node closes branch, False otherwise
+ - Tuple (label, above): label for the node and if draw above or below
+ - data: the node data object itself
+ - branches: list of tuples representing column position of open branches
+ at the time of this node's revision
+ - edges: a list of tuples indicating the edges between the current node
+ and its parents.
+ """
+
+ pivot_branches = \
+ repo.ui.configlist('web','plot_pivot_branches', default=['default'])
+ if ('default' not in pivot_branches):
+ pivot_branches.append('default')
+
+ max_anon_branches = float(1000)
+
+ branch_origin={}
+ branch_data={}
+ open_branches=set()
+ records=[]
+ branch_head_records=[{}]
+ branch_heads={}
+ labels = {}
+ edag = []
+ last_branch = None
+ columnizer = Columnizer(pivot_branches)
+
+ def find_origin(node_data):
+ while True:
+ if node_data.branch() in pivot_branches \
+ or (node_data.parents()[0] == node_data):
+ return node_data.branch()
+ # mercurial allows to create a merge node on a branch different from
+ # the branches of both parents
+ if len (node_data.parents()) > 1 \
+ and node_data.parents()[0].branch() != node_data.branch() \
+ and node_data.parents()[1].branch() == node_data.branch():
+ # we are on the right-side parent's branch
+ node_data = node_data.parents()[1]
+ continue
+ node_data = node_data.parents()[0]
+
+ for (cur, type, data, parents) in dag:
+ branch = data.branch()
+ # Copy elements in a list for successive iterations
+ edag.append((cur, type, data, parents, branch))
+
+ # Find branch origin
+ for n in ([data] + [p for p in data.parents()]):
+ if n.branch() not in branch_origin.keys():
+ branch_origin[n.branch()] = find_origin(n)
+
+ # Sort nodes by branch
+ if branch not in branch_data.keys():
+ branch_data[branch] = []
+
+ branch_data[branch].append((cur, type, data, parents))
+
+ # Record which branches are open at each iteration
+ open_branches.add(branch)
+ records.append([ob for ob in open_branches])
+
+ # column assignment to branches that are open
+ if branch not in columnizer.branches():
+ columnizer.assign_column(branch, branch_origin[branch],
+ not _closed(data))
+ for node in data.parents():
+ if node.branch() not in columnizer.branches():
+ columnizer.assign_column(node.branch(),
+ branch_origin[node.branch()])
+
+ # Free columns where applicable
+ if last_branch and last_branch not in open_branches:
+ columnizer.free_column(last_branch, branch_origin[last_branch])
+ last_branch = branch
+
+ # Record parent's branches for next loop iteration
+ for p in data.parents():
+ open_branches.add(p.branch())
+ # We check both parent's branches because mercurial allows to
+ # create a merge node on a branch different from both parent's
+ if branch not in [p.branch() for p in data.parents()]:
+ # Branch was opened here, not open anymore
+ open_branches.remove(branch)
+ # Mark node as labeled, label_goes_above=True
+ labels[cur] = (branch, True)
+
+ # Handle anonymous branches
+ # Create data structures if needed
+ for n in ([data] + [p for p in data.parents()]):
+ if n.branch() not in branch_heads.keys():
+ branch_heads[n.branch()] = []
+
+ pos = 99999 # Append
+ if cur in branch_heads[branch]:
+ pos = branch_heads[branch].index(cur)
+ branch_heads[branch].remove(cur)
+ else: # New anon head
+ last_branch_heads = branch_head_records[-1]
+ if branch not in last_branch_heads.keys():
+ last_branch_heads[branch] = []
+ last_branch_heads[branch].append(cur)
+
+ father = True
+ for p in [parent for parent in data.parents()]:
+ if p.rev() not in branch_heads[p.branch()]:
+ if p.branch() == branch and father:
+ branch_heads[p.branch()].insert(pos, p.rev())
+ else:
+ branch_heads[p.branch()].append(p.rev())
+ father = False
+ branch_head_records.append(deepcopy(branch_heads))
+
+ # Label last node of each branch for tagging
+ for branch in branch_data.keys():
+ cur, type, data, parents = branch_data[branch][-1]
+ # Mark node as labeled if it is not already
+ if cur not in labels.keys():
+ labels[cur] = (branch, False)
+
+ # remap columns
+ remapper = Remapper()
+ for pb in pivot_branches:
+ remapper.append((pb,)).remap(columnizer.as_dict()[pb])
+ branch_column = remapper.result()
+
+ # assign colors
+ remapper = Remapper()
+ branch_colors={}
+ k = -1 #Negative color index is used for 'stable' colors
+ for pb in pivot_branches:
+ branch_colors[pb] = k
+ k -= 1
+ remapper.remap(columnizer.as_dict()[pb])
+ branch_colors.update(remapper.result())
+
+ # plotting
+ i = 0
+ plotbranches = set()
+ for (cur, type, data, parents, branch) in edag:
+ plotbranches.add(branch)
+
+ col = branch_column[branch]
+ color = branch_colors[branch]
+ # offset anonymous branches
+ if cur in branch_head_records[i].get(branch,[]):
+ subcol = branch_head_records[i][branch].index(cur)
+
+ edges = set()
+ # Plot edges for all open branches
+ if len(records) > i + 1:
+ active_branches = set(records[i]).intersection(set(records[i + 1]))
+ for b in active_branches.intersection(plotbranches):
+ if b not in branch_column: # Unknown plotbranch, skip edge
+ continue
+ active_heads = set(branch_head_records[i].get(b,[]))\
+ .intersection(set(branch_head_records[i + 1].get(b,[])))
+ for ab in active_heads:
+ bcol2 = bcol1 = branch_column[b]
+ bcolor = branch_colors[b]
+ bsubcol1 = branch_head_records[i][b].index(ab)
+ bsubcol2 = branch_head_records[i + 1][b].index(ab)
+ edges.add(((bcol1, bsubcol1), (bcol2, bsubcol2), bcolor))
+
+ # Plot edges from current node to parents
+ for p in data.parents():
+ plotbranches.add(p.branch())
+ if p.branch() not in branch_column: # Uknown parent branch, skip
+ continue
+ pcol = branch_column[p.branch()]
+ pcolor = branch_colors[p.branch()]
+ psubcol = branch_head_records[i + 1][p.branch()].index(p.rev())
+ edges.add(((col, subcol), (pcol, psubcol), pcolor))
+
+ labeldata = labels.get(cur, ('', False))
+
+ # Yield and move on
+ branches = [(b, branch_column[b], branch_colors[b]) for b in records[i]]
+ yield ((col, subcol), color, _closed(data),
+ labeldata, data, branches, edges)
+ i += 1
+
+
+class Columnizer:
+ def __init__(self, category_origins):
+ self.categories = category_origins
+ self.category_assigned_cols = \
+ dict([(origin, {}) for origin in category_origins])
+ self.category_free_cols = \
+ dict([(origin, []) for origin in category_origins])
+ self.category_column_num = \
+ dict([(origin, 0) for origin in category_origins])
+
+ def assign_column(self, b, origin, must_be_new=False):
+ if b in self.categories:
+ return
+ if origin not in self.categories:
+ raise ValueError('unsupported origin %s' % (origin,))
+ if not len(self.category_free_cols[origin]) or must_be_new:
+ col = self.category_column_num[origin]
+ self.category_column_num[origin] = \
+ self.category_column_num[origin] + 1
+ else:
+ col = self.category_free_cols[origin].pop(0)
+ self.category_assigned_cols[origin][b] = col
+
+ def free_column(self, b, origin):
+ if b in self.categories:
+ return
+ if origin not in self.categories:
+ raise ValueError('unsupported origin %s' % (origin,))
+ col = self.category_assigned_cols[origin][b]
+ self.category_free_cols[origin].append(col)
+ self.category_free_cols[origin].sort()
+
+ def as_dict(self):
+ return self.category_assigned_cols.copy()
+
+ def branches(self):
+ retval = []
+ for c in self.categories:
+ retval = retval + self.category_assigned_cols[c].keys()
+ return set(retval)
+
+
+class Remapper:
+ def __init__(self):
+ self.next_col = 0
+ self.dst = {}
+
+ def remap(self, src):
+ col_map={}
+ for k in sorted(src.keys(), key=lambda key: src.get(key)):
+ scol = src.get(k)
+ dcol = col_map.get(scol, self.next_col)
+ col_map[scol] = dcol
+ if dcol == self.next_col:
+ self.next_col += 1
+ self.dst[k] = dcol
+ return self
+
+ def append(self, src):
+ col_map={}
+ for k in src:
+ dcol = self.next_col
+ col_map[self.next_col] = dcol
+ self.next_col += 1
+ self.dst[k] = dcol
+ return self
+
+ def result(self):
+ return self.dst
+
+
+def _closed(node):
+ return 'close' in node.changeset()[5]
diff -r 4e804302d30c -r 3a80fde7f800 mercurial/templates/paper/map
--- a/mercurial/templates/paper/map Sun Aug 15 11:05:04 2010 +0200
+++ b/mercurial/templates/paper/map Tue Aug 17 00:08:09 2010 +0200
@@ -9,10 +9,12 @@
shortlog = shortlog.tmpl
shortlogentry = shortlogentry.tmpl
graph = graph.tmpl
+plot = plot.tmpl
naventry = '<a href="{url}log/{node|short}{sessionvars%urlparameter}">{label|escape}</a> '
navshortentry = '<a href="{url}shortlog/{node|short}{sessionvars%urlparameter}">{label|escape}</a> '
navgraphentry = '<a href="{url}graph/{node|short}{sessionvars%urlparameter}">{label|escape}</a> '
+navplotentry = '<a href="{url}plot/{node|short}{sessionvars%urlparameter}">{label|escape}</a> '
filenaventry = '<a href="{url}log/{node|short}/{file|urlescape}{sessionvars%urlparameter}">{label|escape}</a> '
filedifflink = '<a href="{url}diff/{node|short}/{file|urlescape}{sessionvars%urlparameter}">{file|escape}</a> '
filenodelink = '<a href="{url}file/{node|short}/{file|urlescape}{sessionvars%urlparameter}">{file|escape}</a> '
@@ -26,6 +28,7 @@
nav = '{before%naventry} {after%naventry}'
navshort = '{before%navshortentry}{after%navshortentry}'
navgraph = '{before%navgraphentry}{after%navgraphentry}'
+navplot = '{before%navplotentry}{after%navplotentry}'
filenav = '{before%filenaventry}{after%filenaventry}'
direntry = '
diff -r 4e804302d30c -r 3a80fde7f800 mercurial/templates/paper/plot.tmpl
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/mercurial/templates/paper/plot.tmpl Tue Aug 17 00:08:09 2010 +0200
@@ -0,0 +1,141 @@
+{header}
+<title>{repo|escape}: revision plot</title>
+<link rel="alternate" type="application/atom+xml"
+ href="{url}atom-log" title="Atom feed for {repo|escape}: log" />
+<link rel="alternate" type="application/rss+xml"
+ href="{url}rss-log" title="RSS feed for {repo|escape}: log" />
+<!--[if IE]><script type="text/javascript" src="{staticurl}excanvas.js"></script><![endif]-->
+</head>
+<body>
+
+<div class="container">
+<div class="menu">
+<div class="logo">
+<a href="http://mercurial.selenic.com/">
+<img src="{staticurl}hglogo.png" alt="mercurial" /></a>
+</div>
+<ul>
+<li><a href="{url}shortlog/{node|short}{sessionvars%urlparameter}">log</a></li>
+<li><a href="{url}graph{sessionvars%urlparameter}">graph</a></li>
+<li class="active">plot</li>
+<li><a href="{url}tags{sessionvars%urlparameter}">tags</a></li>
+<li><a href="{url}branches{sessionvars%urlparameter}">branches</a></li>
+</ul>
+<ul>
+<li><a href="{url}rev/{node|short}{sessionvars%urlparameter}">changeset</a></li>
+<li><a href="{url}file/{node|short}{path|urlescape}{sessionvars%urlparameter}">browse</a></li>
+</ul>
+</div>
+
+<div class="main">
+<h2><a href="{url}{sessionvars%urlparameter}">{repo|escape}</a></h2>
+<h3>plot</h3>
+
+<form class="search" action="{url}log">
+{sessionvars%hiddenformentry}
+<p><input name="rev" id="search1" type="text" size="30" /></p>
+<div id="hint">find changesets by author, revision,
+files, or words in the commit message</div>
+</form>
+
+<div class="navigate">
+<a href="{url}plot/{rev}{lessvars%urlparameter}">less</a>
+<a href="{url}plot/{rev}{morevars%urlparameter}">more</a>
+| rev {rev}: {changenav%navplot}
+</div>
+
+<noscript><p>The revision plot only works with JavaScript-enabled browsers.</p></noscript>
+
+<div id="wrapper">
+<div id="node_bgs" style="position:absolute; width: 100%; z-index: 10"></div>
+<div id="node_entries" style="position:absolute; z-index:100"></div>
+<canvas id="plot" width="500" height="600" style="position: relative; top:0px !important; z-index: 50"></canvas>
+</div>
+
+
+<script type="text/javascript" src="{staticurl}plot.js"></script>
+<script type="text/javascript">
+<!-- Hide script
+
+var tpl_entry = '<div style="_STYLE"><span class="desc">';
+tpl_entry += '<a href="{url}rev/_NODEID{sessionvars%urlparameter}" title="_NODEID">_DESC</a>';
+tpl_entry += '</span>_TAGS<span class="info">_DATE, by _USER</span></div>';
+
+var node_entries_data = [];
+var bg_entries_data = [];
+
+var addentry_callback = function(node, index, height) \{
+ var parity = index % 2;
+ var padding = 2;
+ var actual_height = height - 2*padding;
+ var nstyle = "display: block; padding: " + padding + "px; height: " + actual_height +"px";
+ var item = tpl_entry.replace(/_STYLE/, nstyle);
+ item = item.replace(/_PARITY/, 'parity' + parity);
+ item = item.replace(/_NODEID/, node.id);
+ item = item.replace(/_NODEID/, node.id);
+ item = item.replace(/_DESC/, node.desc);
+ item = item.replace(/_USER/, node.user);
+ item = item.replace(/_DATE/, node.age);
+
+ var tagspan = '';
+ if (node.tags.length || (node.branch != 'default' || node.tip)) \{
+ tagspan = '<span class="logtags">';
+ if (node.tip) \{
+ tagspan += '<span class="branchhead" title="' + node.branch + '">';
+ tagspan += node.branch + '</span> ';
+ } else if (!node.tip && node.branch != 'default') \{
+ tagspan += '<span class="branchname" title="' + node.branch + '">';
+ tagspan += node.branch + '</span> ';
+ }
+ if (node.tags.length) \{
+ for (var t in node.tags) \{
+ var tag = node.tags[t];
+ tagspan += '<span class="tag">' + tag + '</span> ';
+ }
+ }
+ tagspan += '</span>';
+ }
+
+ item = item.replace(/_TAGS/, tagspan);
+ node_entries_data.push(item);
+ bg_entries_data.push('<div class="bg parity'+parity+'" style="height: '+ height + 'px"> </div>');
+}
+
+
+
+var data = {jsdata|json};
+var plot = new Plot();
+var canvas = document.getElementById('plot');
+
+plot.setNodeCallback(addentry_callback);
+plot.scale({plot_width},40);
+plot.render(data);
+
+if (plot.getMaxWidth() > canvas.width) \{
+ // If default width was not enough, resize and repeat rendering
+ canvas.width = plot.getMaxWidth();
+ plot.setNodeCallback(null); // skip generating node_entries, we already did
+ plot.render(data);
+}
+
+document.getElementById('node_entries').innerHTML = node_entries_data.join("");
+document.getElementById('node_bgs').innerHTML = bg_entries_data.join("");
+
+document.getElementById('node_entries').style.left = (plot.getMaxWidth() + 30) + "px";
+// stop hiding script -->
+</script>
+
+<div style="clear: both"></div>
+
+<div class="navigate">
+<a href="{url}plot/{rev}{lessvars%urlparameter}">less</a>
+<a href="{url}plot/{rev}{morevars%urlparameter}">more</a>
+| rev {rev}: {changenav%navplot}
+| <a href="{url}plot/{rev}{narrowervars%urlparameter}">narrower</a>
+<a href="{url}plot/{rev}{widervars%urlparameter}">wider</a>
+</div>
+
+</div>
+</div>
+
+{footer}
diff -r 4e804302d30c -r 3a80fde7f800 mercurial/templates/static/plot.js
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/mercurial/templates/static/plot.js Tue Aug 17 00:08:09 2010 +0200
@@ -0,0 +1,221 @@
+var stable_colors = [
+ "#0000CC",
+ "#CC0000",
+ "#00CC00",
+ "#FF6600",
+ "#00FF00",
+ "#FF00FF",
+ "#808080",
+ "#FFCC00",
+ "#339966"
+];
+
+var common_colors = [
+ "#33CCCC",
+ "#C04000",
+ "#99CC00",
+ "#DDAA00",
+ "#666699",
+ "#969696",
+ "#003366",
+ "#FF7700",
+ "#339966",
+ "#3366FF",
+ "#003300"
+];
+
+
+function Plot() {
+ this.canvas = document.getElementById('plot');
+ if (navigator.userAgent.indexOf('MSIE') >= 0) this.canvas = window.G_vmlCanvasManager.initElement(this.canvas);
+ this.ctx = this.canvas.getContext('2d');
+ this.cell_width = 30;
+ this.cell_height = 30;
+ this.label_trailing_space = 10;
+ this.label_offset = 5;
+ this.node_radius = 4;
+ this.node_callback = null;
+ this.min_anon_branches_width = 6;
+
+ this.scale = function(width, height) {
+ this.cell_width = width;
+ this.cell_height = height;
+ }
+
+ this.getMaxWidth = function() {
+ return this.max_x + this.node_radius*2 + this.cell_width / 2 + 1;
+ }
+
+ this.setMinAnonBranchesWidth = function(anons) {
+ this.min_anon_branches_width = anons;
+ }
+
+ this.setNodeCallback = function(cb) {
+ this.node_callback = cb;
+ }
+
+ this.parseData = function(raw) {
+ var entry = {};
+ entry.node = {};
+ entry.node.column = raw[0][0];
+ entry.node.color = raw[0][1];
+ entry.node.closed = raw[0][2];
+ entry.node.label = raw[0][3][0];
+ entry.node.label_above = raw[0][3][1];
+ // Other data
+ entry.node.id = raw[0][4];
+ entry.node.desc = raw[0][5];
+ entry.node.user = raw[0][6];
+ entry.node.age = raw[0][7];
+ entry.node.branch = raw[0][8][0];
+ entry.node.tip = raw[0][8][1];
+ entry.node.tags = raw[0][9];
+ // End other data
+ entry.branches = raw[1];
+ entry.edges = [];
+ for (i in raw[2]) {
+ entry.edges.push({start: raw[2][i][0], end: raw[2][i][1], color: raw[2][i][2]});
+ }
+ return entry;
+ }
+
+ this.setColor = function(color) {
+
+ // Set the colour.
+ // Pick a different wheel based on the fact that the color index is positive or negative
+
+ if (color < 0) {
+ // Stable colors
+ this.ctx.lineWidth = 2;
+ color = stable_colors[(-1-color) % stable_colors.length];
+ } else {
+ this.ctx.lineWidth = 1;
+ color = common_colors[color % common_colors.length];
+ }
+ this.ctx.strokeStyle = color;
+ this.ctx.fillStyle = color;
+ return color;
+
+ }
+
+ this.edge = function(x0, y0, x1, y1, color) {
+ this.setColor(color);
+ this.ctx.beginPath();
+ this.ctx.moveTo(x0, y0);
+ this.ctx.lineTo(x1, y1);
+ this.ctx.stroke();
+ }
+
+ this.vertex = function(x, y, color, closed) {
+ this.ctx.beginPath();
+ color = this.setColor(color);
+ this.ctx.arc(x, y, this.node_radius, 0, Math.PI * 2, true);
+ this.ctx.fill();
+ if (closed) {
+ this.ctx.save();
+ this.ctx.lineWidth = 2;
+ this.ctx.strokeStyle = "black";
+ this.ctx.stroke();
+ this.ctx.restore();
+ }
+ }
+
+ this.label = function(x,y,color, label, above) {
+ this.ctx.save();
+ this.ctx.font = "13px monospace";
+ this.setColor(color);
+ this.ctx.rotate(-Math.PI/2);
+
+ var w = this.ctx.measureText(label).width;
+ if (y < w || !above){
+ y = y + w + this.label_trailing_space;
+ } else {
+ y -= this.label_trailing_space;
+ }
+ if ((y + this.cell_height/2) > this.canvas.height) // We must consider the translation used in render()
+ y = this.canvas.height - this.cell_height/2 -1; // 0-based offset
+
+ x -= this.label_offset;
+ this.ctx.translate(-y,x);
+ this.ctx.fillText(label, 0,0);
+ this.ctx.restore();
+ }
+
+ this.calc_x = function(column_data) {
+ max_anon_branches = this.cell_width / this.min_anon_branches_width;
+ col = column_data[0];
+ subcol = column_data[1];
+ return col + subcol / max_anon_branches;
+ }
+
+ this.render = function(data) {
+ // Initialize required vars
+ this.ctx.strokeStyle = 'rgb(0, 0, 0)';
+ this.ctx.fillStyle = 'rgb(0, 0, 0)';
+ this.row = 0;
+ this.max_x = 0;
+
+ this.max_x = 0;
+ this.canvas.height = this.cell_height * data.length;
+ this.ctx.translate(this.cell_width / 2, this.cell_height/2);
+
+ if (this.canvas.height > 32760) {
+ alert("Plot height too high, could be clipped or not shown at all, try with a lower revcount");
+ }
+
+ var i = 0;
+ for (var i in data) {
+ var entry = this.parseData(data[i]);
+ var branches = entry.branches;
+
+ for (var j in entry.edges) {
+
+ line = entry.edges[j];
+
+ var x0 = this.cell_width * this.calc_x(line.start);
+ var y0 = this.cell_height * this.row;
+ var x1 = this.cell_width * this.calc_x(line.end);
+ var y1 = this.cell_height * (this.row+1);
+
+ this.edge(x0, y0, x1, y1, line.color);
+ if (this.max_x < x0)
+ this.max_x = x0;
+ if (this.max_x < x1)
+ this.max_x = x1;
+
+ }
+
+ // Draw the revision node in the right column
+
+ var column = entry.node.column;
+ var color = entry.node.color;
+ var closed = entry.node.closed;
+
+ var x = this.cell_width * this.calc_x(column);
+ var y = this.cell_height * this.row;
+ this.vertex(x, y, color, closed);
+
+ var lx = this.cell_width * column[0]; // always draw label on the left of the leftmost subcolumn
+
+ if (entry.node.label)
+ this.label(lx,y,color,entry.node.label, entry.node.label_above);
+
+ if (this.max_x < x)
+ this.max_x = x;
+
+ /* UNUSED FOR NOW
+ for (var k in entry.branches) {
+ var branch = branches[k][0];
+ var column = branches[k][1];
+ var color = branches[k][2];
+ }
+ */
+
+ if (this.node_callback)
+ this.node_callback(entry.node, i, this.cell_height);
+ this.row += 1;
+ }
+
+ }
+
+}
diff -r 4e804302d30c -r 3a80fde7f800 mercurial/templates/static/style-paper.css
--- a/mercurial/templates/static/style-paper.css Sun Aug 15 11:05:04 2010 +0200
+++ b/mercurial/templates/static/style-paper.css Tue Aug 17 00:08:09 2010 +0200
@@ -252,3 +252,10 @@
position: relative;
top: -3px;
}
+
+div#node_entries div .info {
+ display: block;
+ font-size: 70%;
+ position: relative;
+ top: -3px;
+}
More information about the Mercurial-devel
mailing list