#          Lisa Hsu
 
 import matplotlib, pylab
+from matplotlib.font_manager import FontProperties
 from matplotlib.numerix import array, arange, reshape, shape, transpose, zeros
 from matplotlib.numerix import Float
 
 matplotlib.interactive(False)
 
-class BarChart(object):
-    def __init__(self, **kwargs):
-        self.init(**kwargs)
+from chart import ChartOptions
 
-    def init(self, **kwargs):
-        self.colormap = 'jet'
+class BarChart(ChartOptions):
+    def __init__(self, default=None, **kwargs):
+        super(BarChart, self).__init__(default, **kwargs)
         self.inputdata = None
         self.chartdata = None
-        self.xlabel = None
-        self.ylabel = None
-        self.legend = None
-        self.xticks = None
-        self.yticks = None
-        self.title = None
-
-        for key,value in kwargs.iteritems():
-            self.__setattr__(key, value)
 
     def gen_colors(self, count):
         cmap = matplotlib.cm.get_cmap(self.colormap)
         if self.chartdata is None:
             raise AttributeError, "Data not set for bar chart!"
 
-        self.figure = pylab.figure()
-        self.axes = self.figure.add_subplot(111)
+        self.figure = pylab.figure(figsize=self.chart_size)
+        self.axes = self.figure.add_axes(self.figure_size)
 
         dim = len(shape(self.inputdata))
         cshape = shape(self.chartdata)
 
         bars = []
         for i,stackdata in enumerate(self.chartdata):
-            bottom = array([0] * len(stackdata[0]))
+            bottom = array([0.0] * len(stackdata[0]), Float)
             stack = []
             for j,bardata in enumerate(stackdata):
                 bardata = array(bardata)
             ticks = arange(nticks) / (nticks - 1) * (ymax - ymin)  + ymin
             self.axes.set_yticks(ticks)
             self.axes.set_yticklabels(self.yticks)
+        elif self.ylim is not None:
+            self.axes.set_ylim(self.ylim)
 
         if self.xticks is not None:
             self.axes.set_xticks(arange(cshape[2]) + .5)
                 number = len(bars[0])
                 lbars = [ bars[0][number - j - 1][0] for j in xrange(number)]
 
-            self.axes.legend(lbars, self.legend, loc='best')
+            self.figure.legend(lbars, self.legend, self.legend_loc,
+                               prop=FontProperties(size=self.legend_size))
 
         if self.title is not None:
             self.axes.set_title(self.title)
     def savefig(self, name):
         self.figure.savefig(name)
 
+    def savecsv(self, name):
+        f = file(name, 'w')
+        data = array(self.inputdata)
+        dim = len(data.shape)
+
+        if dim == 1:
+            #if self.xlabel:
+            #    f.write(', '.join(list(self.xlabel)) + '\n')
+            f.write(', '.join([ '%f' % val for val in data]) + '\n')
+        if dim == 2:
+            #if self.xlabel:
+            #    f.write(', '.join([''] + list(self.xlabel)) + '\n')
+            for i,row in enumerate(data):
+                ylabel = []
+                #if self.ylabel:
+                #    ylabel = [ self.ylabel[i] ]
+                f.write(', '.join(ylabel + [ '%f' % val for val in row]) + '\n')
+        if dim == 3:
+            f.write("don't do 3D csv files\n")
+            pass
+
+        f.close()
+
+
 if __name__ == '__main__':
+    from random import randrange
     import random, sys
 
     dim = 3
         chart1.xticks = [ 'xtick%d' % x for x in xrange(myshape[0]) ]
         chart1.title = 'this is the title'
         chart1.graph()
-        #chart1.savefig('/tmp/test1.png')
+        chart1.savefig('/tmp/test1.png')
+        chart1.savefig('/tmp/test1.ps')
+        chart1.savefig('/tmp/test1.eps')
+        chart1.savecsv('/tmp/test1.csv')
 
     if False:
         chart2 = BarChart()
         chart2.data = data
         chart2.colormap = 'gray'
         chart2.graph()
-        #chart2.savefig('/tmp/test2.png')
+        chart2.savefig('/tmp/test2.png')
+        chart2.savefig('/tmp/test2.ps')
 
-    pylab.show()
+    #pylab.show()
 
--- /dev/null
+# Copyright (c) 2005 The Regents of The University of Michigan
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met: redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer;
+# redistributions in binary form must reproduce the above copyright
+# notice, this list of conditions and the following disclaimer in the
+# documentation and/or other materials provided with the distribution;
+# neither the name of the copyright holders nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+#
+# Authors: Nathan Binkert
+#          Lisa Hsu
+
+class ChartOptions(object):
+    defaults = { 'chart_size' : (8, 4),
+                 'figure_size' : [0.1, 0.1, 0.6, 0.85],
+                 'title' : None,
+                 'legend' : None,
+                 'legend_loc' : 'upper right',
+                 'legend_size' : 6,
+                 'colormap' : 'jet',
+                 'xlabel' : None,
+                 'ylabel' : None,
+                 'xticks' : None,
+                 'yticks' : None,
+                 'ylim' : None,
+                 }
+
+    def __init__(self, options=None, **kwargs):
+        self.init(options, **kwargs)
+
+    def clear(self):
+        self.options = {}
+
+    def init(self, options=None, **kwargs):
+        self.clear()
+        self.update(options, **kwargs)
+
+    def update(self, options=None, **kwargs):
+        if options is not None:
+            if not isinstance(options, ChartOptions):
+                raise AttributeError, \
+                      'attribute options of type %s should be %s' % \
+                      (type(options), ChartOptions)
+            self.options.update(options.options)
+
+        for key,value in kwargs.iteritems():
+            if key not in ChartOptions.defaults:
+                raise AttributeError, \
+                      "%s instance has no attribute '%s'" % (type(self), key)
+            self.options[key] = value
+
+    def __getattr__(self, attr):
+        if attr in self.options:
+            return self.options[attr]
+
+        if attr in ChartOptions.defaults:
+            return ChartOptions.defaults[attr]
+
+        raise AttributeError, \
+              "%s instance has no attribute '%s'" % (type(self), attr)
+
+    def __setattr__(self, attr, value):
+        if attr in ChartOptions.defaults:
+            self.options[attr] = value
+        else:
+            super(ChartOptions, self).__setattr__(attr, value)
+
 
     def get(self, job, stat):
         run = self.allRunNames.get(job.name, None)
         if run is None:
-            print 'run "%s" not found' % job
             return None
 
         from info import scalar, vector, value, values, total, len
 
 #
 # Authors: Nathan Binkert
 
-class StatOutput(object):
-    def __init__(self, name, jobfile, info, stat=None, binstats=None):
-        self.name = name
+from chart import ChartOptions
+
+class StatOutput(ChartOptions):
+    def __init__(self, jobfile, info, stat=None, binstats=None):
+        super(StatOutput, self).__init__()
         self.jobfile = jobfile
         self.stat = stat
         self.binstats = None
-        self.label = self.name
         self.invert = False
         self.info = info
 
-    def printdata(self, bin = None, printmode = 'G'):
+    def printdata(self, name, bin = None, printmode = 'G'):
         import info
 
         if bin:
-            print '%s %s stats' % (self.name, bin)
+            print '%s %s stats' % (name, bin)
 
         if self.binstats:
             for stat in self.binstats:
             valstring = ', '.join([ valformat % val for val in value ])
             print '%-50s    %s' % (job.name + ':', valstring)
 
-    def display(self, binned = False, printmode = 'G'):
+    def display(self, name, binned = False, printmode = 'G'):
         if binned and self.binstats:
-            self.printdata('kernel', printmode)
-            self.printdata('idle', printmode)
-            self.printdata('user', printmode)
-            self.printdata('interrupt', printmode)
+            self.printdata(name, 'kernel', printmode)
+            self.printdata(name, 'idle', printmode)
+            self.printdata(name, 'user', printmode)
+            self.printdata(name, 'interrupt', printmode)
 
-            print '%s total stats' % self.name
-        self.printdata(printmode=printmode)
+            print '%s total stats' % name
+        self.printdata(name, printmode=printmode)
 
-    def graph(self, graphdir):
+    def graph(self, name, graphdir, proxy=None):
         from os.path import expanduser, isdir, join as joinpath
         from barchart import BarChart
         from matplotlib.numerix import Float, array, zeros
-        import os, re
+        import os, re, urllib
+        from jobfile import crossproduct
 
         confgroups = self.jobfile.groups()
         ngroups = len(confgroups)
         skiplist = [ False ] * ngroups
-        groupopts = None
-        baropts = None
+        groupopts = []
+        baropts = []
         groups = []
         for i,group in enumerate(confgroups):
             if group.flags.graph_group:
-                if groupopts is not None:
-                    raise AttributeError, \
-                          'Two groups selected for graph group'
-                groupopts = group.subopts()
+                groupopts.append(group.subopts())
                 skiplist[i] = True
             elif group.flags.graph_bars:
-                if baropts is not None:
-                    raise AttributeError, \
-                          'Two groups selected for graph bars'
-                baropts = group.subopts()
+                baropts.append(group.subopts())
                 skiplist[i] = True
             else:
                 groups.append(group)
 
-        if groupopts is None:
+        if not groupopts:
             raise AttributeError, 'No group selected for graph group'
 
-        if baropts is None:
+        if not baropts:
             raise AttributeError, 'No group selected for graph bars'
 
+        groupopts = [ group for group in crossproduct(groupopts) ]
+        baropts = [ bar for bar in crossproduct(baropts) ]
+
         directory = expanduser(graphdir)
         if not isdir(directory):
             os.mkdir(directory)
-        html = file(joinpath(directory, '%s.html' % self.name), 'w')
+        html = file(joinpath(directory, '%s.html' % name), 'w')
         print >>html, '<html>'
-        print >>html, '<title>Graphs for %s</title>' % self.name
+        print >>html, '<title>Graphs for %s</title>' % name
         print >>html, '<body>'
+        html.flush()
 
         for options in self.jobfile.options(groups):
+            chart = BarChart(self)
+
             data = zeros((len(groupopts), len(baropts)), Float)
             data = [ [ None ] * len(baropts) for i in xrange(len(groupopts)) ]
             enabled = False
             stacked = 0
             for g,gopt in enumerate(groupopts):
                 for b,bopt in enumerate(baropts):
-                    job = self.jobfile.job(options + [ gopt, bopt ])
+                    job = self.jobfile.job(options + gopt + bopt)
                     if not job:
                         continue
 
+                    if proxy:
+                        import db
+                        proxy.dict['system'] = self.info[job.system]
                     val = self.info.get(job, self.stat)
+                    if val is None:
+                        print 'stat "%s" for job "%s" not found' % \
+                              (self.stat, job)
+
                     if isinstance(val, (list, tuple)):
                         if len(val) == 1:
                             val = val[0]
                     for j in xrange(len(baropts)):
                         val = data[i][j]
                         if val is None:
-                            data[i][j] = [] * stacked
+                            data[i][j] = [ 0.0 ] * stacked
                         elif len(val) != stacked:
                             raise ValueError, "some stats stacked, some not"
 
             if data.sum() == 0:
                 continue
 
-            bar_descs = [ opt.desc for opt in baropts ]
-            group_descs = [ opt.desc for opt in groupopts ]
-            if stacked:
-                try:
-                    legend = self.info.rcategories
-                except:
-                    legend = [ str(i) for i in xrange(stacked) ]
-            else:
-                legend = bar_descs
-
-            chart = BarChart(data=data, xlabel='Benchmark', ylabel=self.label,
-                             legend=legend, xticks=group_descs)
+            x = data.shape[0]
+            y = data.shape[1]
+            xkeep = [ i for i in xrange(x) if data[i].sum() != 0 ]
+            ykeep = [ i for i in xrange(y) if data[:,i].sum() != 0 ]
+            data = data.take(xkeep, axis=0)
+            data = data.take(ykeep, axis=1)
+            chart.data = data
+
+            gopts = [ groupopts[i] for i in xkeep ]
+            bopts = [ baropts[i] for i in ykeep ]
+
+            bdescs = [ ' '.join([o.desc for o in opt]) for opt in bopts]
+            gdescs = [ ' '.join([o.desc for o in opt]) for opt in gopts]
+
+            if chart.legend is None:
+                if stacked:
+                    try:
+                        chart.legend = self.info.rcategories
+                    except:
+                        chart.legend = [ str(i) for i in xrange(stacked) ]
+                else:
+                    chart.legend = bdescs
+
+            if chart.xticks is None:
+                chart.xticks = gdescs
             chart.graph()
 
             names = [ opt.name for opt in options ]
             descs = [ opt.desc for opt in options ]
 
-            filename =  '%s-%s.png' % (self.name, ':'.join(names))
+            if names[0] == 'run':
+                names = names[1:]
+                descs = descs[1:]
+
+            basename = '%s-%s' % (name, ':'.join(names))
             desc = ' '.join(descs)
-            filepath = joinpath(directory, filename)
-            chart.savefig(filepath)
-            filename = re.sub(':', '%3A', filename)
-            print >>html, '''%s<br><img src="%s"><br>''' % (desc, filename)
+
+            pngname = '%s.png' % basename
+            psname = '%s.eps' % re.sub(':', '-', basename)
+            epsname = '%s.ps' % re.sub(':', '-', basename)
+            chart.savefig(joinpath(directory, pngname))
+            chart.savefig(joinpath(directory, epsname))
+            chart.savefig(joinpath(directory, psname))
+            html_name = urllib.quote(pngname)
+            print >>html, '''%s<br><img src="%s"><br>''' % (desc, html_name)
+            html.flush()
 
         print >>html, '</body>'
         print >>html, '</html>'
 
 from orderdict import orderdict
 import output
 
+class FileData(dict):
+    def __init__(self, filename):
+        self.filename = filename
+        fd = file(filename)
+        current = []
+        for line in fd:
+            line = line.strip()
+            if line.startswith('>>>'):
+                current = []
+                self[line[3:]] = current
+            else:
+                current.append(line)
+        fd.close()
+
 class RunData(dict):
-    def __init__(self, filename=None):
+    def __init__(self, filename):
         self.filename = filename
 
-    def __getattr__(self, attr):
+    def __getattribute__(self, attr):
         if attr == 'total':
             total = 0.0
             for value in self.itervalues():
                 total += value
             return total
+
+        if attr == 'filedata':
+            return FileData(self.filename)
+
         if attr == 'maxsymlen':
             return max([ len(sym) for sym in self.iterkeys() ])
 
+        return super(RunData, self).__getattribute__(attr)
+
     def display(self, output=None, limit=None, maxsymlen=None):
         if not output:
             import sys
         for number,name in symbols:
             print >>output, symbolf % (name, 100.0 * (float(number) / total))
 
-
-
 class PCData(RunData):
     def __init__(self, filename=None, categorize=None, showidle=True):
         super(PCData, self).__init__(self, filename)
-        if filename is None:
-            return
-
-        fd = file(filename)
-
-        for line in fd:
-            if line.strip() == '>>>PC data':
-                break
-
-        for line in fd:
-            if line.startswith('>>>'):
-                break
 
+        filedata = self.filedata['PC data']
+        for line in filedata:
             (symbol, count) = line.split()
             if symbol == "0x0":
                 continue
 
                 self[category] = count
 
-        fd.close()
-
 class FuncNode(object):
-    def __new__(cls, filename = None):
-        if filename is None:
+    def __new__(cls, filedata=None):
+        if filedata is None:
             return super(FuncNode, cls).__new__(cls)
 
-        fd = file(filename, 'r')
-        fditer = iter(fd)
         nodes = {}
-        for line in fditer:
-            if line.strip() == '>>>function data':
-                break
-
-        for line in fditer:
-            if line.startswith('>>>'):
-                break
-
-            data = line.split()
-            node_id = int(data[0], 16)
+        for line in filedata['function data']:
+            data = line.split(' ')
+            node_id = long(data[0], 16)
             node = FuncNode()
             node.symbol = data[1]
-            node.count = int(data[2])
-            node.children = [ int(child, 16) for child in data[3:] ]
+            if node.symbol == '':
+                node.symbol = 'unknown'
+            node.count = long(data[2])
+            node.children = [ long(child, 16) for child in data[3:] ]
             nodes[node_id] = node
 
         for node in nodes.itervalues():
                 child.parent = node
             node.children = tuple(children)
         if not nodes:
-            print filename
+            print filedata.filename
             print nodes
         return nodes[0]
 
-    def __init__(self, filename=None):
-        pass
-
     def total(self):
         total = self.count
         for child in self.children:
 class FuncData(RunData):
     def __init__(self, filename, categorize=None):
         super(FuncData, self).__init__(filename)
-        self.tree = FuncNode(filename)
-        self.tree.aggregate(self, categorize, incategory=False)
-        self.total = self.tree.total()
+        tree = self.tree
+        tree.aggregate(self, categorize, incategory=False)
+        self.total = tree.total()
+
+    def __getattribute__(self, attr):
+        if attr == 'tree':
+            return FuncNode(self.filedata)
+        return super(FuncData, self).__getattribute__(attr)
 
     def displayx(self, output=None, maxcount=None):
         if output is None:
         try:
             return self.data[run][cpu]
         except KeyError:
+            print run, cpu
             return None
 
     def alldata(self):
         cpu = '%s.run%d' % (job.system, self.cpu)
         data = self.getdata(run, cpu)
         if not data:
-            return [ 0.0 for c in self.categories ]
+            return None
 
         values = []
         for category in self.categories:
-            values.append(data.get(category, 0.0))
-        return values
+            val = float(data.get(category, 0.0))
+            if val < 0.0:
+                raise ValueError, 'value is %f' % val
+            values.append(val)
+        total = sum(values)
+        return [ v / total * 100.0 for v in values ]
 
     def dump(self):
         for run,cpu,data in self.alldata():
     import getopt, re, sys
     from os.path import expanduser
     from output import StatOutput
-    from jobfile import JobFile
 
     # default option values
     numsyms = 10
     funcdata = True
     jobfilename = 'Test.py'
     dodot = False
-    dotformat = 'raw'
+    dotfile = None
     textout = False
     threshold = 0.01
     inputfile = None
         elif o == '-c':
             categorize = True
         elif o == '-D':
-            dotformat = a
+            dotfile = a
         elif o == '-d':
             dodot = True
         elif o == '-f':
         usage(1)
 
     if inputfile:
-        data = FuncData(inputfile)
+        catfunc = None
+        if categorize:
+            catfunc = func_categorize
+        data = FuncData(inputfile, categorize=catfunc)
 
         if dodot:
             import pydot
             dot = pydot.Dot()
-            data.dot(dot, threshold=threshold)
+            data.tree.dot(dot, threshold=threshold)
             #dot.orientation = 'landscape'
             #dot.ranksep='equally'
             #dot.rank='samerank'
-            dot.write(dotfile, format=dotformat)
+            dot.write(dotfile, format='png')
         else:
             data.display(limit=numsyms)
 
     else:
+        from jobfile import JobFile
         jobfile = JobFile(jobfilename)
 
         if funcdata:
                     name = 'funcstacks%d' % cpu
                 else:
                     name = 'stacks%d' % cpu
-                output = StatOutput(name, jobfile, info=profile)
-                output.graph(graph)
+                output = StatOutput(jobfile, info=profile)
+                output.xlabel = 'System Configuration'
+                output.ylabel = '% CPU utilization'
+                output.stat = name
+                output.graph(name, graph)
 
         if dodot:
             for cpu in cpus:
 
             print 'only displaying sample %s' % options.ticks
         source.ticks = [ int(x) for x in options.ticks.split() ]
 
-    import output
-
-    def display():
-        if options.graph:
-            output.graph(options.graphdir)
-        else:
-            output.display(options.binned, options.printmode)
-
+    from output import StatOutput
+    output = StatOutput(options.jobfile, source)
+    output.xlabel = 'System Configuration'
 
     if command == 'stat' or command == 'formula':
         if len(args) != 1:
             stats = eval(args[0])
 
         for stat in stats:
-            output = output.StatOutput(stat.name, options.jobfile, source)
             output.stat = stat
-            output.label = stat.name
-            display()
+            output.ylabel = stat.name
+            if options.graph:
+                output.graph(stat.name, options.graphdir)
+            else:
+                output.display(stat.name, options.binned, options.printmode)
 
         return
 
     if len(args):
         raise CommandException
 
-    system = source.__dict__[options.system]
     from info import ProxyGroup
     sim_seconds = source['sim_seconds']
     proxy = ProxyGroup(system = source[options.system])
     packets = etherdev.rxPackets + etherdev.txPackets
     bps = etherdev.rxBandwidth + etherdev.txBandwidth
 
-    output = output.StatOutput(command, options.jobfile, source)
+    def display():
+        if options.graph:
+            output.graph(command, options.graphdir, proxy)
+        else:
+            output.display(command, options.binned, options.printmode)
 
     if command == 'usertime':
         import copy
         user.bins = 'user'
 
         output.stat = user / system.run0.numCycles
-        output.label = 'User Fraction'
+        output.ylabel = 'User Fraction'
 
         display()
         return
 
     if command == 'pps':
         output.stat = packets / sim_seconds
-        output.label = 'Packets/s'
+        output.ylabel = 'Packets/s'
         display()
         return
 
     if command == 'bpt' or command == 'tpb':
         output.stat = bytes / system.run0.numCycles * 8
-        output.label = 'bps / Hz'
+        output.ylabel = 'bps / Hz'
         output.invert = command == 'tpb'
         display()
         return
         if command == 'bps':
             output.stat = bps / 1e9
 
-        output.label = 'Bandwidth (Gbps)'
+        output.ylabel = 'Bandwidth (Gbps)'
+        output.ylim = [ 0.0, 10.0 ]
         display()
         return
 
     if command == 'bpp':
         output.stat = bytes / packets
-        output.label = 'Bytes / Packet'
+        output.ylabel = 'Bytes / Packet'
         display()
         return
 
     if command == 'rxbpp':
         output.stat = etherdev.rxBytes / etherdev.rxPackets
-        output.label = 'Receive Bytes / Packet'
+        output.ylabel = 'Receive Bytes / Packet'
         display()
         return
 
     if command == 'txbpp':
         output.stat = etherdev.txBytes / etherdev.txPackets
-        output.label = 'Transmit Bytes / Packet'
+        output.ylabel = 'Transmit Bytes / Packet'
         display()
         return
 
     if command == 'rtp':
         output.stat = etherdev.rxPackets / etherdev.txPackets
-        output.label = 'rxPackets / txPackets'
+        output.ylabel = 'rxPackets / txPackets'
         display()
         return
 
     if command == 'rtb':
         output.stat = etherdev.rxBytes / etherdev.txBytes
-        output.label = 'rxBytes / txBytes'
+        output.ylabel = 'rxBytes / txBytes'
         display()
         return
 
 
     if command == 'misses':
         output.stat = misses
-        output.label = 'Overall MSHR Misses'
+        output.ylabel = 'Overall MSHR Misses'
         display()
         return
 
     if command == 'mpkb':
         output.stat = misses / (bytes / 1024)
         output.binstats = [ misses ]
-        output.label = 'Misses / KB'
+        output.ylabel = 'Misses / KB'
         display()
         return
 
         interrupts = system.run0.kern.faults[4]
         output.stat = interrupts / kbytes
         output.binstats = [ interrupts ]
-        output.label = 'Interrupts / KB'
+        output.ylabel = 'Interrupts / KB'
         display()
         return