thg / contrib / plot-annotate-colors.py

#!/usr/bin/env python
# plot-annotate-colors.py - Display how annotation colors are assigned
#
# Copyright 2010 Yuya Nishihara <yuya@tcha.org>
#
# This software may be used and distributed according to the terms of the
# GNU General Public License version 2, incorporated herein by reference.
import os, sys, math

sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..'))

from mercurial import hg, ui, cmdutil, util
from tortoisehg.util import colormap, hglib

from PyQt4.QtCore import *
from PyQt4.QtGui import *

SECS_PER_DAY = 24 * 60 * 60

class ColorPlot(QWidget):
    """Display annotation colors as x-y graph

    ::
        +-------------------------------------------------> age
        |
        |   o  ... filled by its color
        |
        |           O  ... size of circle describes number of lines
        |
        |                                        *
        |                                        ... missing ctx is
        |                                            filled by yellow
        |                                            (error marker)
        v
        committer
    """

    def __init__(self, parent=None):
        super(ColorPlot, self).__init__(parent)
        self.setSizePolicy(QSizePolicy(QSizePolicy.Expanding,
                                       QSizePolicy.Expanding))
        pal = QPalette()
        pal.setColor(self.backgroundRole(), QColor('black'))
        self.setPalette(pal)
        self.setAutoFillBackground(True)

        self._updatetimer = QTimer(self, singleShot=True)
        self._updatetimer.timeout.connect(self.update)

        self._maxhues = None
        self._maxsaturations = None
        self._mindate = None
        self._maxcolors = 0

        self._latestdate = None
        self._fctxs = None

    def paintEvent(self, event):
        if self._fctxs is None:
            return

        mindate = None
        if self._mindate:
            mindate = self._latestdate - self._mindate * SECS_PER_DAY
        palette, cm = colormap._makeannotatepalette(
            list(sorted(set(self._fctxs), key=lambda fctx: -fctx.date()[0])),
            now=self._latestdate, maxcolors=self._maxcolors,
            maxhues=self._maxhues, maxsaturations=self._maxsaturations,
            mindate=mindate)

        weights = {}
        for fctx in self._fctxs:
            try:
                weights[fctx] += 1
            except KeyError:
                weights[fctx] = 1

        p = QPainter(self)
        p.setRenderHint(QPainter.Antialiasing)
        for fctx, w in weights.iteritems():
            i = abs(hash(fctx.user()))
            j = self._latestdate - fctx.date()[0]
            c = cm.get_color(fctx, self._latestdate)
            if c not in palette:
                b = QBrush(QColor('yellow'), Qt.Dense4Pattern)  # error marker
            else:
                b = QBrush(QColor(c))
            p.setPen(QPen(b, math.sqrt(w) * 2, Qt.SolidLine, Qt.RoundCap))
            p.drawPoint(j / SECS_PER_DAY + 2,
                        i / (sys.maxint / (self.height() - 4)) + 2)

        p.setPen(QColor('yellow'))
        p.setFont(QFont('monospace', 8))
        p.drawText(2, self.height() - 8,
                   'maxsaturations: %d' % cm._maxsaturations)

    def sizeHint(self):
        return QSize(365, 200)

    @pyqtSlot(int)
    def setMaxHues(self, value):
        self._maxhues = value or None
        self._updatetimer.start(100)

    @pyqtSlot(int)
    def setMaxSaturations(self, value):
        self._maxsaturations = value or None
        self._updatetimer.start(100)

    @pyqtSlot(int)
    def setMinDate(self, value):
        self._mindate = value or None
        self._updatetimer.start(100)

    @pyqtSlot(int)
    def setMaxColors(self, value):
        self._maxcolors = value
        self._updatetimer.start(100)

    def plot(self, repo, canonpath, rev='.'):
        latestfctx = repo[rev][canonpath]
        self._fctxs = [fctx for fctx, line in latestfctx.annotate(follow=True)]
        self._latestdate = latestfctx.date()[0]
        self._updatetimer.start(100)

class ColorPlotter(QWidget):
    def __init__(self, parent=None):
        super(ColorPlotter, self).__init__(parent)
        self.setLayout(QVBoxLayout(self))
        self._plot = ColorPlot(self)
        self.layout().addWidget(self._plot)

        form = QFormLayout()
        self.layout().addLayout(form)

        self._maxhues_edit = QSpinBox(self, minimum=0, maximum=360,
                                      specialValueText='inf')
        self._maxhues_edit.valueChanged.connect(self._plot.setMaxHues)
        self._maxhues_edit.setValue(8)
        form.addRow('Max Hues (committers):', self._maxhues_edit)

        self._maxsats_edit = QSpinBox(self, minimum=0, maximum=255,
                                      specialValueText='inf')
        self._maxsats_edit.valueChanged.connect(self._plot.setMaxSaturations)
        self._maxsats_edit.setValue(16)
        form.addRow('Max Saturations (ages):', self._maxsats_edit)

        self._mindate_edit = QSpinBox(self, minimum=0, maximum=720,
                                      specialValueText='n/a')
        self._mindate_edit.valueChanged.connect(self._plot.setMinDate)
        self._mindate_edit.setValue(365)
        form.addRow('Minimal revision age to include', self._mindate_edit)

        self._maxcols_edit = QSpinBox(self, minimum=0, maximum=255)
        self._maxcols_edit.valueChanged.connect(self._plot.setMaxColors)
        self._maxcols_edit.setValue(32)
        form.addRow('Max Colors (palette size):', self._maxcols_edit)

    @pyqtSlot(unicode)
    def setPath(self, path):
        spath = hglib.fromunicode(path)
        reporoot = cmdutil.findrepo(os.path.abspath(spath))
        if not reporoot:
            QMessageBox.warning(self, 'Repository Not Found',
                                'Repository not found for path: %s' % path)
        repo = hg.repository(ui.ui(), reporoot)
        canonpath = util.canonpath(repo.root, os.getcwd(), spath)
        self._plot.plot(repo, canonpath)

class ColorPlotterWindow(QMainWindow):
    def __init__(self, parent=None):
        super(ColorPlotterWindow, self).__init__(parent)
        self._plotter = ColorPlotter(self)
        self.setCentralWidget(self._plotter)

        self.setMenuBar(QMenuBar())
        filemenu = self.menuBar().addMenu('&File')
        filemenu.addAction('&Open', self._openFile, QKeySequence.Open)

    @pyqtSlot()
    def _openFile(self):
        path = QFileDialog.getOpenFileName(self, 'File to Annotate')
        if path:
            self.setPath(path)

    @pyqtSlot(unicode)
    def setPath(self, path):
        self.setWindowFilePath(path)
        self._plotter.setPath(path)

def main(args=sys.argv):
    app = QApplication(args)
    w = ColorPlotterWindow()
    w.show()
    if len(args) > 1:
        QTimer.singleShot(100, lambda: w.setPath(args[1]))
    return app.exec_()

if __name__ == '__main__':
    sys.exit(main())
Tip: Filter by directory path e.g. /media app.js to search for public/media/app.js.
Tip: Use camelCasing e.g. ProjME to search for ProjectModifiedEvent.java.
Tip: Filter by extension type e.g. /repo .js to search for all .js files in the /repo directory.
Tip: Separate your search with spaces e.g. /ssh pom.xml to search for src/ssh/pom.xml.
Tip: Use ↑ and ↓ arrow keys to navigate and return to view the file.
Tip: You can also navigate files with Ctrl+j (next) and Ctrl+k (previous) and view the file with Ctrl+o.
Tip: You can also navigate files with Alt+j (next) and Alt+k (previous) and view the file with Alt+o.