[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">&nbsp;</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