Commit 71cc9221 authored by Mario Hock's avatar Mario Hock

Nice output in presentation mode

- Large font for good readability on presentation slides.
- Human readable axis labels
parent eb29e46e
......@@ -3,15 +3,15 @@
import sys
import matplotlib
import matplotlib.ticker as ticker
from cnl_library import CNLParser, calc_ema, merge_lists, pretty_json, human_readable_from_seconds
from plot_cpu import plot_top_cpus
## matplotlib.use('QT4Agg') # override matplotlibrc
matplotlib.use('QT4Agg') # override matplotlibrc (optional)
import matplotlib.pyplot as plt
from cnl_library import CNLParser, calc_ema, merge_lists, pretty_json
from plot_cpu import plot_top_cpus
import plot_ticks
import plot_layout
def append_twice(base_list, extend_list):
if ( isinstance(extend_list, list) ):
......@@ -23,121 +23,16 @@ def append_twice(base_list, extend_list):
base_list.append(extend_list)
#def get_cpu_label(cnl_file, col_name):
#"""
#Return the label to be displayed in the legend for a given CPU-col name.
## TODO Maybe this class should be moved to another file..
class TimeLocator(matplotlib.ticker.Locator):
"""
Place the ticks to be nice seconds/minutes/hours values.
"""
def __init__(self, numticks=5):
#self._base = Base(base)
self.numticks = numticks
def __call__(self):
'Return the locations of the ticks'
vmin, vmax = self.axis.get_view_interval()
return self.tick_values(vmin, vmax)
def _shrink_to_a_multiple_of(self, origin, divisor, maxdiff=0):
diff = origin % divisor
# BRANCH: external max-diff
if ( maxdiff > 0 ):
if ( diff <= maxdiff ):
origin -= diff
# BRANCH: automatic max-diff
elif ( diff < origin * 0.2 ):
origin -= diff
return origin
def _make_nice(self, value, maxdiff=0):
## TODO Find out if this approach gets the desired results...
## TODO quit loop after it worked?, actually make a loop
value = self._shrink_to_a_multiple_of(value, 60*60, maxdiff)
value = self._shrink_to_a_multiple_of(value, 60*30, maxdiff)
value = self._shrink_to_a_multiple_of(value, 60*15, maxdiff)
value = self._shrink_to_a_multiple_of(value, 60*10, maxdiff)
value = self._shrink_to_a_multiple_of(value, 60*5, maxdiff)
value = self._shrink_to_a_multiple_of(value, 60, maxdiff)
value = self._shrink_to_a_multiple_of(value, 30, maxdiff)
value = self._shrink_to_a_multiple_of(value, 15, maxdiff)
value = self._shrink_to_a_multiple_of(value, 10, maxdiff)
value = self._shrink_to_a_multiple_of(value, 5, maxdiff)
return value
def tick_values(self, vmin, vmax):
if vmax < vmin:
vmin, vmax = vmax, vmin
## Find a nice stepping for the ticks.
diff = vmax-vmin
# If the scale starts almost with 0, concentrate on the positive side.
if ( vmin <= 0 and 0 < vmax and vmin*-1 < diff / (2*self.numticks)):
diff = vmax - 0
step = diff / self.numticks
nice_step = self._make_nice(step)
## Place the ticks.
locs = list()
# Tick »0« if it is in range
if ( vmin <= 0 and 0 < vmax ):
base = 0
else:
base = self._make_nice(vmin) ## TODO make nice with +, instead - ?
# ticks to the right
pos = base
while ( pos <= vmax ):
locs.append(pos)
pos += nice_step
# ticks to the left
pos = base - nice_step
while ( pos >= vmin ):
locs.append(pos)
pos -= nice_step
## Add an additional (still nice) max label, if appropriate.
additional_max_tick = self._make_nice(vmax, 0.25 * nice_step)
if ( additional_max_tick - max(locs) >= 0.5 * nice_step ):
locs.append(additional_max_tick)
return self.raise_if_exceeds(locs)
#def view_limits(self, dmin, dmax):
#"""
#Set the view limits to the nearest multiples of base that
#contain the data
#"""
#vmin = self._base.le(dmin)
#vmax = self._base.ge(dmax)
#if vmin == vmax:
#vmin -= 1
#vmax += 1
#return mtransforms.nonsingular(vmin, vmax)
#This function actually just removes the ".util" suffix.
#"""
#if ( col_name.ends_with(".util") ):
#return col_name[0:-5]
#return col_name
......@@ -155,7 +50,7 @@ def parse_cnl_file(filename, nic_fields = [".send", ".receive"]):
net_cols.append( nic_name + nic_field )
cpu_cols = [ cpu_name + ".util" for cpu_name in cnl_file.get_cpus() ]
#cpu_cols = [ cpu_name + ".irq" for cpu_name in cnl_file.get_cpus() ] ## XXX
cpu_col_labels = [ cpu_name for cpu_name in cnl_file.get_cpus() ]
cols = cnl_file.get_csv_columns()
#x_values = cols["end"]
......@@ -166,8 +61,12 @@ def parse_cnl_file(filename, nic_fields = [".send", ".receive"]):
cnl_file.cols = cols
cnl_file.net_col_names = net_cols
cnl_file.cpu_col_names = cpu_cols
cnl_file.cpu_col_labels = cpu_col_labels
#cnl_file.x_values = x_values
#cnl_file.get_cpu_label = get_cpu_label
return cnl_file
......@@ -175,26 +74,29 @@ def get_min_max_x(cnl_file):
return ( cnl_file.cols["begin"][0], cnl_file.cols["end"][-1] )
def plot(ax, x_values, cols, active_cols, alpha, **kwargs):
def plot(ax, x_values, cols, active_cols, col_labels, alpha, **kwargs):
#use_ema = kwargs.get("use_ema")
ema_only = kwargs.get("ema_only")
smooth = kwargs.get("smooth")
for col_name in active_cols:
for col_name, col_label in zip(active_cols, col_labels):
data = cols[col_name]
if ( len(x_values) == len(data)*2 ):
data = merge_lists( data, data )
# * plot *
if ( not ema_only ):
ax.plot(x_values , data, label=col_name, alpha=alpha)
ax.plot(x_values , data, label=col_label, alpha=alpha)
## plot ema
if ( ema_only and smooth ):
ax.plot(x_values , calc_ema(data, smooth), label=col_name)
ax.plot(x_values , calc_ema(data, smooth), label=col_label)
def plot_net(ax, cnl_file, args):
def plot_net(ax, cnl_file, args, layout):
# parameters
legend_outside = True
alpha = args.opacity if args.transparent_net else 1.0
......@@ -202,9 +104,9 @@ def plot_net(ax, cnl_file, args):
# axes
ax.set_ylim(0,10**10)
ax.set_ylabel('Throughput (Bit/s)', fontsize=args.axis_fontsize)
ax.set_ylabel('Throughput (Bit/s)', fontsize=layout.fontsize.axis_labels)
plot(ax, cnl_file.x_values, cnl_file.cols, cnl_file.net_col_names, alpha,
plot(ax, cnl_file.x_values, cnl_file.cols, cnl_file.net_col_names, cnl_file.net_col_names, alpha,
ema_only=True if smooth else False, smooth=smooth)
# Legend
......@@ -214,12 +116,12 @@ def plot_net(ax, cnl_file, args):
l = ax.legend( loc='upper left', bbox_to_anchor=(0, 0), ncol=int(len(cnl_file.net_col_names)/2),
bbox_transform = trans,
fancybox=False, shadow=False, fontsize=args.legend_fontsize)
fancybox=False, shadow=False, fontsize=layout.fontsize.legend)
else:
l = ax.legend(loc=0)
def plot_cpu(ax, cnl_file, args):
def plot_cpu(ax, cnl_file, args, layout):
# parameters
legend_outside = True
alpha = args.opacity if args.transparent_cpu else 1.0
......@@ -227,10 +129,10 @@ def plot_cpu(ax, cnl_file, args):
# axes
ax.set_ylim(0,100)
ax.set_ylabel('CPU util (%)', fontsize=args.axis_fontsize)
ax.set_ylabel('CPU util (%)', fontsize=layout.fontsize.axis_labels)
# * plot *
plot(ax, cnl_file.x_values, cnl_file.cols, cnl_file.cpu_col_names, alpha,
plot(ax, cnl_file.x_values, cnl_file.cols, cnl_file.cpu_col_names, cnl_file.cpu_col_labels, alpha,
ema_only=True if smooth else False, smooth=smooth)
# Legend
......@@ -240,16 +142,15 @@ def plot_cpu(ax, cnl_file, args):
l = ax.legend( loc='upper left', bbox_to_anchor=(0, 0), ncol=int(len(cnl_file.cpu_col_names)/2),
bbox_transform = trans,
fancybox=False, shadow=False, fontsize=args.legend_fontsize)
fancybox=False, shadow=False, fontsize=layout.fontsize.legend)
else:
l = ax.legend(loc=0)
#ax.set_label("Testlabel")
l.draggable(True)
class NameSuggestor:
def __init__(self):
self.date = list()
......@@ -260,7 +161,6 @@ class NameSuggestor:
self.hostname.append( cnl_file.get_hostname() )
def suggest_filename(self):
print( "{}_{}".format( self.date[0], "_".join(self.hostname) ) ) ## XXX
return "{}_{}".format( self.date[0], "_".join(self.hostname) )
......@@ -291,7 +191,6 @@ if __name__ == "__main__":
metavar="ALPHA",
help = "Smooth CPU values with exponential moving average. (Disabled by default. When specified without parameter: ALPHA=0.1)" )
## XXX experimental..
parser.add_argument("-sn", "--smooth-net", nargs='?', const=DEFAULT_ALPHA, type=float,
metavar="ALPHA",
help = "Smooth transmission rates with exponential moving average. (Disabled by default. When specified without parameter: ALPHA=0.1)" )
......@@ -303,17 +202,19 @@ if __name__ == "__main__":
parser.add_argument("-rs", "--receive-send", action="store_true",
help="Like --send-receive, but the other way round.")
## TODO currently this is always a PDF file -> make dependent from file ending
## TODO make it look the same as saving from GUI..
parser.add_argument("-o", "--output", type=str,
help="Plot directly into a file. [TESTING] Note: This function is still under development..")
## TODO implement (maybe set as default)
#parser.add_argument("-a", "--all-matches", action="store_true",
#help="Finds all matches current directory (or in --files, if specified) and plots them pairwise.")
args = parser.parse_args()
## font size (there is no cmd-line option for this, [yet?])
args.axis_fontsize = 12
args.legend_fontsize = 12
layout = plot_layout.Layout("default")
## set implicated options
# --transparent
......@@ -324,8 +225,13 @@ if __name__ == "__main__":
# --publication
if ( args.publication ):
args.no_comment = True
args.axis_fontsize = 20
args.legend_fontsize = 16
layout = plot_layout.Layout("presentation")
# axes
args.x_minutes = True
args.y_10G = True
num_files = len(args.files)
name_suggestor = NameSuggestor()
......@@ -388,6 +294,10 @@ if __name__ == "__main__":
#ax_net = fig.add_subplot(111) ## twin axis
#ax_cpu = ax_net.twinx() ## twin axis
# set tick size
layout.set_tick_fontsize(plt, ax_net, ax_cpu)
## Prepare x_values
plateau = True ## XXX
......@@ -396,14 +306,14 @@ if __name__ == "__main__":
else:
cnl_file.x_values = cnl_file.cols["end"]
# shift x-values ## TODO find a single base-time for all files?
# shift x-values ## TODO FIXME [IMPORTANT] find a single base-time for all files?
#base_time = min_max[0]
base_time = cnl_file.get_machine_readable_date()
cnl_file.x_values = [ x - base_time for x in cnl_file.x_values ]
## Plot
plot_net(ax_net, cnl_file, args)
plot_cpu(ax_cpu, cnl_file, args)
plot_net(ax_net, cnl_file, args, layout)
plot_cpu(ax_cpu, cnl_file, args, layout)
old_ax_net = ax_net
old_ax_cpu = ax_cpu
......@@ -413,46 +323,30 @@ if __name__ == "__main__":
if ( num_files == 1 ):
ax1 = fig.add_subplot(2, num_cols, 2, sharex=old_ax_net, sharey=old_ax_cpu)
ax2 = fig.add_subplot(2, num_cols, 4, sharex=ax_net, sharey=old_ax_cpu)
layout.set_tick_fontsize(plt, ax1, ax2)
plot_top_cpus( cnl_file, args, (ax1, ax2), (0,1) )
plot_top_cpus( cnl_file, args, layout, (ax1, ax2), (0,1) )
## Subplot-Layout (margins)
# NOTE: This actually works great with a screen resolution of 1920x1200.
# Since all space here are in relative size, this might have to be adjusted for other screen resolutions.
if ( args.publication ):
## TODO What about the fontsize? Only increase for slides, or for publication in general..?
# Narrow layout for publications and presentations
if ( num_files == 1 ):
# CPU area charts
#fig.subplots_adjust(left=0.03, wspace=0.15, right=0.93, top=0.97, hspace=0.3, bottom=0.08) ## small font size
fig.subplots_adjust(left=0.04, wspace=0.15, right=0.92, top=0.97, hspace=0.3, bottom=0.09) ## large font, legend on the right
#fig.subplots_adjust(left=0.04, wspace=0.15, right=0.99, top=0.97, hspace=0.3, bottom=0.09) ## large font, legend below
else:
# double plot
#fig.subplots_adjust(left=0.03, wspace=0.15, right=0.99, top=0.97, hspace=0.3, bottom=0.08) ## small font size
fig.subplots_adjust(left=0.04, wspace=0.15, right=0.99, top=0.97, hspace=0.3, bottom=0.09)
else:
# Regular layout (for good readability on screen)
fig.subplots_adjust(left=0.1, wspace=0.2, right=0.9, top=0.92, hspace=0.4, bottom=0.12)
## Set window margins
has_area_plot = (num_files == 1)
layout.set_margins(fig, has_area_plot)
## TODO, maybe the TimeLocator can do this better? (see TimeLocator.view_limits)
## set min/max (remember: The x-axis is shared.)
margin = max( (max_x - min_x) * 0.03, 10 )
ax_net.set_xlim(min_x - margin - base_time, max_x + margin - base_time) ## XXX Like that, it's the base-time from the latest file...
in_plot_margin = max( (max_x - min_x) * 0.03, 10 )
ax_net.set_xlim(min_x - in_plot_margin - base_time, max_x + in_plot_margin - base_time) ## XXX Like that, the base-time from the latest file is used...
## Format tick labels TESTING
def format_ticks(x, pos=None):
return human_readable_from_seconds(float(x))
ax_net.xaxis.set_major_formatter( ticker.FuncFormatter(format_ticks) )
ax_net.xaxis.set_major_locator( TimeLocator() )
## Format tick labels
if ( args.x_minutes ):
ax_net.xaxis.set_major_locator( plot_ticks.TimeLocator() )
ax_net.xaxis.set_major_formatter( matplotlib.ticker.FuncFormatter(plot_ticks.format_xticks_minutes) )
if ( args.y_10G ):
ax_net.yaxis.set_major_formatter( matplotlib.ticker.FuncFormatter(plot_ticks.format_yticks_10G) )
## Set the default format for the save-botton to PDF.
## Set the default format for the save-button to "PDF".
try:
fig.canvas.get_default_filetype = lambda: "pdf"
suggested_name = "{}-plot.{}".format( name_suggestor.suggest_filename(), fig.canvas.get_default_filetype() )
......@@ -473,5 +367,21 @@ if __name__ == "__main__":
matplotlib.rc('pdf', fonttype=42)
# show plot
plt.show()
# ## XXX hack...
# class EventHandler:
# def __init__(self):
# fig.canvas.mpl_connect('button_press_event', self.onpress)
# #fig.canvas.mpl_connect('button_release_event', self.onrelease)
# #fig.canvas.mpl_connect('motion_notify_event', self.onmove)
#
# def onpress(self, event):
# ax_net.xaxis.set_major_formatter( ticker.FuncFormatter(format_xticks_time) )
# fig.canvas.draw()
# handler = EventHandler()
# Show / hardcopy plot
if ( args.output ):
plt.savefig(args.output, format="pdf")
else:
plt.show()
......@@ -63,7 +63,7 @@ def _create_cpu_cols_by_util(cnl_file):
def plot_area_chart(ax, cnl_file, args, cols, legend_outside, legend_title):
def plot_area_chart(ax, cnl_file, args, layout, cols, legend_outside, legend_title):
"""
Plots an area chart of the CPU utilization (usr, sys, ...).
"""
......@@ -75,7 +75,7 @@ def plot_area_chart(ax, cnl_file, args, cols, legend_outside, legend_title):
# Axes
ax.set_ylim(0,100)
#ax.set_ylabel('CPU ' + "/".join(cpu_fields) + ' (%)')
ax.set_ylabel('CPU util (%)', fontsize=args.axis_fontsize)
ax.set_ylabel('CPU util (%)', fontsize=layout.fontsize.axis_labels)
# Plot
y_offsets = numpy.array([0.0] * len(cnl_file.cols["begin"]))
......@@ -111,12 +111,12 @@ def plot_area_chart(ax, cnl_file, args, cols, legend_outside, legend_title):
# Legend
if ( legend_outside ):
## Legend on the right
if ( args.legend_fontsize == 12 or True ): ## XXX
if ( True ): ## XXX
offset = transforms.ScaledTranslation(20, 0, transforms.IdentityTransform())
trans = ax.transAxes + offset
l = ax.legend(loc='upper left', bbox_to_anchor=(1.0, 1.02),fancybox=True, shadow=True, title=legend_title,
fontsize=args.legend_fontsize)
fontsize=layout.fontsize.legend)
## Legend below
else:
......@@ -128,7 +128,7 @@ def plot_area_chart(ax, cnl_file, args, cols, legend_outside, legend_title):
#ncol=math.ceil(len(cpu_fields)/2),
ncol=len(cpu_fields),
bbox_transform = trans,
fontsize=args.legend_fontsize)
fontsize=layout.fontsize.legend)
else:
l = ax.legend(loc=0)
......@@ -136,7 +136,7 @@ def plot_area_chart(ax, cnl_file, args, cols, legend_outside, legend_title):
l.draggable(True)
def plot_top_cpus(cnl_file, args, axes, indices=[0]):
def plot_top_cpus(cnl_file, args, layout, axes, indices=[0]):
"""
This function creates "virtual top-cpus" and plots the utilization fields (usr, system, ...)
......@@ -158,5 +158,5 @@ def plot_top_cpus(cnl_file, args, axes, indices=[0]):
for ax, i in zip(axes, indices):
label = "Top #{} CPU".format(i+1)
cols = top_cpus[i]
plot_area_chart(ax, cnl_file, args, cols, True, label)
plot_area_chart(ax, cnl_file, args, layout, cols, True, label)
# -*- coding:utf-8 -*-
class Layout:
def __init__(self, name):
self.fontsize = fontsize[name]
self.margin_double_plot = margin_double[name]
self.margin_area_plot = margin_area[name]
def set_margins(self, fig, area_plot=False):
if ( area_plot ):
self.margin_area_plot.set_margins(fig)
else:
self.margin_double_plot.set_margins(fig)
def set_tick_fontsize(self, plt, *axes):
for ax in axes:
plt.setp(ax.get_xticklabels(), fontsize=self.fontsize.ticks)
plt.setp(ax.get_yticklabels(), fontsize=self.fontsize.ticks)
## TODO Maybe "named tuples" would be more appropriate than classes for Margin and Fontsize?
class Margin:
def __init__(self, left, wspace, right, top, hspace, bottom):
self.bottom = float(bottom)
self.hspace = float(hspace)
self.left = float(left)
self.right = float(right)
self.top = float(top)
self.wspace = float(wspace)
def set_margins(self, fig):
fig.subplots_adjust(left=self.left, wspace=self.wspace, right=self.right,
top=self.top, hspace=self.hspace, bottom=self.bottom)
class Fontsize:
def __init__(self, axis_labels, legend, ticks):
self.axis_labels = axis_labels
self.legend = legend
self.ticks = ticks
## Presets: Fontsize
fontsize = dict()
fontsize["default"] = Fontsize(axis_labels = 18, legend = 14, ticks = 14)
fontsize["presentation"] = Fontsize(axis_labels = 28, legend = 22, ticks = 18)
fontsize["publication"] = fontsize["default"]
## Presets: Subplot-Layout (margins)
# NOTE: These values actually work great with a screen resolution of 1920x1200.
# Since all spaces here are in relative size, this might have to be adjusted for other screen resolutions.
margin_double = dict()
margin_area = dict()
# "default": good readability on screen
margin_double["default"] = Margin(left=0.1, wspace=0.2, right=0.9, top=0.92, hspace=0.4, bottom=0.12)
margin_area["default"] = Margin(left=0.1, wspace=0.2, right=0.9, top=0.92, hspace=0.4, bottom=0.12)
# "presentation": large font, small margins
margin_double["presentation"] = Margin(left=0.05, wspace=0.15, right=0.98, top=0.97, hspace=0.33, bottom=0.11)
margin_area["presentation"] = Margin(left=0.05, wspace=0.15, right=0.90, top=0.97, hspace=0.33, bottom=0.11) # legend on the right
margin_area["presentationX"] = Margin(left=0.04, wspace=0.15, right=0.99, top=0.97, hspace=0.3, bottom=0.09) # legend below
# "publication": regular font, small margins
margin_double["publication"] = Margin(left=0.03, wspace=0.15, right=0.99, top=0.97, hspace=0.3, bottom=0.08) # small font size
margin_area["publication"] = Margin(left=0.03, wspace=0.15, right=0.93, top=0.97, hspace=0.3, bottom=0.08) # small font size
# -*- coding:utf-8 -*-
import matplotlib.ticker as ticker
from cnl_library import human_readable_from_seconds
## tick positions ##
class TimeLocator(ticker.Locator):
"""
Place the x-axis ticks to be nice seconds/minutes/hours values.
"""
def __init__(self, numticks=5):
#self._base = Base(base)
self.numticks = numticks
def __call__(self):
'Return the locations of the ticks'
vmin, vmax = self.axis.get_view_interval()
return self.tick_values(vmin, vmax)
def _shrink_to_a_multiple_of(self, origin, divisor, maxdiff=0):
diff = origin % divisor
# BRANCH: external max-diff
if ( maxdiff > 0 ):
if ( diff <= maxdiff ):
origin -= diff
# BRANCH: automatic max-diff
elif ( diff < origin * 0.2 ):
origin -= diff
return origin
def _make_nice(self, value, maxdiff=0):
## TODO Find out if this approach gets the desired results...
steps = (60*60, 60*30, 60*15, 60*10, 60*5,
60, 30, 15, 10, 5)
nice_value = value
for step in steps:
nice_value = self._shrink_to_a_multiple_of(value, step, maxdiff)
if ( nice_value != value ):
break
return nice_value
def tick_values(self, vmin, vmax):
if vmax < vmin:
vmin, vmax = vmax, vmin
## Find a nice stepping for the ticks.
diff = vmax-vmin
# If the scale starts almost with 0, concentrate on the positive side.
if ( vmin <= 0 and 0 < vmax and vmin*-1 < diff / (2*self.numticks)):
diff = vmax - 0
step = diff / self.numticks
nice_step = self._make_nice(step)
## Place the ticks.
locs = list()
# Tick »0« if it is in range
if ( vmin <= 0 and 0 < vmax ):
base = 0
else:
## TODO make nice with +, instead - ?
base = self._make_nice(vmin)
# ticks to the right
pos = base
while ( pos <= vmax ):
locs.append(pos)
pos += nice_step
# ticks to the left