Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 50 additions & 0 deletions MAVProxy/modules/lib/graph_ui.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,3 +78,53 @@ def set_xlim(self, xlim):
return False
self.xlim = xlim
return True


class Histogram_UI(object):
'''UI class for displaying histograms from log data'''

def __init__(self, mestate):
self.mestate = mestate
self.xlim = None
self.xlim_pipe = multiproc.Pipe()

def display_histogram(self, graphdef, bins=50, show_stats=True):
'''display a histogram'''
if 'mestate' in globals():
self.mestate.console.write("Histogram: %s\n" % ' '.join(graphdef.expression.split()))
else:
self.mestate.child_pipe_send_console.send("Histogram: %s\n" % ' '.join(graphdef.expression.split()))

mh = grapher.MavHistogram()
if self.mestate.settings.title is not None:
mh.set_title(self.mestate.settings.title)
else:
mh.set_title(graphdef.name)
mh.set_bins(bins)
mh.set_show_stats(show_stats)
mh.set_condition(self.mestate.settings.condition)
mh.set_legend(self.mestate.settings.legend)
mh.add_mav(copy.copy(self.mestate.mlog))
for f in graphdef.expression.split():
mh.add_field(f)
mh.process(self.mestate.flightmode_selections, self.mestate.mlog._flightmodes)
lenmavlist = len(mh.mav_list)
mh.mav_list = []
child = multiproc.Process(target=mh.show, args=[lenmavlist], kwargs={'xlim_pipe': self.xlim_pipe})
child.start()
self.xlim_pipe[1].close()
self.mestate.mlog.rewind()

def check_xlim_change(self):
'''histogram never drives xlim changes in other graphs'''
return None

def set_xlim(self, xlim):
'''forward a time-range update to the histogram child process'''
if self.xlim_pipe is not None and self.xlim != xlim:
try:
self.xlim_pipe[0].send(xlim)
except IOError:
return False
self.xlim = xlim
return True
124 changes: 124 additions & 0 deletions MAVProxy/modules/lib/grapher.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
from math import *
from pymavlink.mavextra import *
import matplotlib.pyplot as plt
import matplotlib.ticker
from pymavlink import mavutil
import threading
import numpy as np
Expand Down Expand Up @@ -798,6 +799,129 @@ def show(self, lenmavlist, block=True, xlim_pipe=None, output=None):
else:
plt.savefig(output, bbox_inches='tight', dpi=200)


class MavHistogram(MavGraph):
'''Histogram of a single log data field with live time-range filtering.'''

def __init__(self, flightmode_colourmap=None):
super().__init__(flightmode_colourmap)
self.bins = 50
self.show_stats = True
self._all_x = None
self._all_y = None

def set_bins(self, bins):
'''set number of histogram bins'''
self.bins = bins

def set_show_stats(self, show_stats):
'''set whether to overlay median and std dev lines'''
self.show_stats = show_stats

def _draw_histogram(self, y_data):
'''redraw histogram axes with the supplied data'''
self.ax1.cla()
label = self.fields[0] if self.fields else ''
title = self.title if self.title else 'Histogram: ' + label
self.ax1.set_title(title)
self.ax1.hist(y_data, bins=self.bins, color=colors[0], alpha=0.7, label=label,
weights=np.ones(len(y_data)) / len(y_data) * 100.0)
self.ax1.set_xlabel(label)
self.ax1.set_ylabel('Frequency (%)')
self.ax1.yaxis.set_major_formatter(matplotlib.ticker.FormatStrFormatter('%.1f%%'))
self.ax1.xaxis.set_major_locator(matplotlib.ticker.MaxNLocator(nbins=15))
self.ax1.xaxis.set_minor_locator(matplotlib.ticker.AutoMinorLocator())
self.ax1.tick_params(axis='x', which='major', labelrotation=45)

if self.show_stats and len(y_data) > 0:
median = np.median(y_data)
mean = np.mean(y_data)
std = np.std(y_data)

stat_styles = [
(median, 'black', '-', 2.0, 'Median %.3g' % median),
(mean - std, 'orange', '--', 1.5, u'\u00b11\u03c3 (%.3g)' % std),
(mean + std, 'orange', '--', 1.5, None),
(mean - 2*std, 'red', ':', 1.5, u'\u00b12\u03c3 (%.3g)' % (2*std)),
(mean + 2*std, 'red', ':', 1.5, None),
(mean - 3*std, 'purple', '-.', 1.2, u'\u00b13\u03c3 (%.3g)' % (3*std)),
(mean + 3*std, 'purple', '-.', 1.2, None),
]
for (xval, col, ls, lw, lbl) in stat_styles:
self.ax1.axvline(x=xval, color=col, linestyle=ls,
linewidth=lw, label=lbl)

self.ax1.legend(loc=self.legend)
self.fig.tight_layout()
self.fig.canvas.draw_idle()

def _hist_xlim_timer(self):
'''called every 100 ms to check for time-range updates from other graphs'''
if self.closing or self.xlim_pipe is None:
return
try:
if not self.xlim_pipe[1].poll():
return
xlim = self.xlim_pipe[1].recv()
except Exception:
return
if xlim == self.xlim or xlim is None:
return
self.xlim = xlim
mask = (self._all_x >= xlim[0]) & (self._all_x <= xlim[1])
y_filtered = self._all_y[mask]
if len(y_filtered) == 0:
return
self._draw_histogram(y_filtered)

def show(self, lenmavlist, block=True, xlim_pipe=None, output=None):
'''show histogram plot in a new figure'''
if xlim_pipe is not None:
xlim_pipe[0].close()
self.xlim_pipe = xlim_pipe

if not self.x or len(self.x[0]) == 0:
print("No data for histogram")
return

self._all_x = np.array(self.x[0])
self._all_y = np.array(self.y[0])

label = self.fields[0] if self.fields else ''
title = self.title if self.title else 'Histogram: ' + label

interactive = output is None
if interactive:
plt.ion()

self.fig, ax = plt.subplots(figsize=(10, 6))
self.ax1 = ax

self._draw_histogram(self._all_y)

self.fig.canvas.get_default_filename = lambda: ''.join(
'histogram' if self.title is None else
(c if c.isalnum() else '_' for c in title)) + '.png'
if self.fig.canvas.manager is not None:
self.fig.canvas.manager.set_window_title(title)
self.fig.canvas.mpl_connect('close_event', self.close_event)

if output is None:
if xlim_pipe is not None:
self.xlim_t = self.fig.canvas.new_timer(interval=100)
self.xlim_t.add_callback(self._hist_xlim_timer)
self.xlim_t.start()
plt.draw()
plt.show(block=block)
elif output.endswith('.html'):
import mpld3
html = mpld3.fig_to_html(self.fig)
with open(output, 'w') as f_out:
f_out.write(html)
else:
plt.savefig(output, bbox_inches='tight', dpi=200)


if __name__ == "__main__":
from argparse import ArgumentParser
parser = ArgumentParser(description=__doc__)
Expand Down
47 changes: 47 additions & 0 deletions MAVProxy/tools/MAVExplorer.py
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,8 @@ def __init__(self):
MPSetting('sync_xmap', bool, True, 'sync X-axis zoom for map'),
MPSetting('legend', str, 'upper left', 'legend position'),
MPSetting('legend2', str, 'upper right', 'legend2 position'),
MPSetting('hist_bins', int, 50, 'histogram bin count', tab='Graph'),
MPSetting('hist_show_stats', bool, True, 'show median/std dev on histogram', tab='Graph'),
MPSetting('axis_mode', str, 'auto', 'y-axis layout mode',
choice=['auto', 'dual', 'multi']),
MPSetting('title', str, None, 'Graph title'),
Expand All @@ -131,6 +133,7 @@ def __init__(self):
"condition" : ["(VARIABLE)"],
"graph" : ['(VARIABLE) (VARIABLE) (VARIABLE) (VARIABLE) (VARIABLE) (VARIABLE) (VARIABLE) (VARIABLE) (VARIABLE) (VARIABLE) (VARIABLE) (VARIABLE)'],
"graphs" : ['(PREDEFINED_GRAPH)'],
"histogram" : ['(VARIABLE) (VARIABLE) (VARIABLE) (VARIABLE)', '--bins (VARIABLE) (VARIABLE)'],
"dump" : ['(MESSAGETYPE)', '--verbose (MESSAGETYPE)'],
"map" : ['(VARIABLE) (VARIABLE) (VARIABLE) (VARIABLE) (VARIABLE)'],
"param" : ['download', 'check', 'help (PARAMETER)', 'save', 'savechanged', 'diff', 'show', 'check'],
Expand Down Expand Up @@ -260,6 +263,7 @@ def setup_menus():
items=[MPMenuItem('MagFit', 'MagFit', '# magfit'),
MPMenuItem('Stats', 'Stats', '# stats'),
MPMenuItem('FFT', 'FFT', '# fft'),
MPMenuItem('Histogram', 'Histogram', '# histogram'),
MPMenuItem('Location Analysis', 'Location', '# locationAnalysis')]))

mestate.console.set_menu(TopMenu, menu_callback)
Expand Down Expand Up @@ -579,6 +583,48 @@ def cmd_graph(args):
#print("initial: ", xlimits.last_xlim)
grui[-1].set_xlim(xlimits.last_xlim)

def cmd_histogram(args):
'''plot histogram of a single log data field'''
usage = "usage: histogram [--bins N] <FIELD>"
if len(args) < 1:
print(usage)
return

bins = mestate.settings.hist_bins
fields = []
i = 0
while i < len(args):
if args[i] == '--bins':
if i + 1 < len(args):
try:
bins = int(args[i+1])
i += 2
continue
except ValueError:
print("Invalid bins value: %s" % args[i+1])
return
fields.append(args[i])
i += 1

if not fields:
print(usage)
return
if len(fields) > 1:
print("histogram only accepts one field at a time (got: %s)" % ', '.join(fields))
return

check_vehicle_type()
from MAVProxy.modules.lib.graph_ui import Histogram_UI
expression = fields[0]
graphdef = GraphDefinition(mestate.settings.title, expression, '', [expression], None)
hui = Histogram_UI(mestate)
hui.display_histogram(graphdef, bins=bins, show_stats=mestate.settings.hist_show_stats)
grui.append(hui)
global xlimits
if xlimits.last_xlim is not None and mestate.settings.sync_xzoom:
grui[-1].set_xlim(xlimits.last_xlim)


def cmd_graphs(args):
'''graphs command'''
usage = "usage: graphs <PREDEFINED_GRAPH_NAME>"
Expand Down Expand Up @@ -1743,6 +1789,7 @@ def main_loop():
command_map = {
'graph' : (cmd_graph, 'display a graph'),
'graphs' : (cmd_graphs, 'display a predefined graph'),
'histogram' : (cmd_histogram, 'plot histogram of log data fields'),
'set' : (cmd_set, 'control settings'),
'reload' : (cmd_reload, 'reload graphs'),
'save' : (cmd_save, 'save a graph'),
Expand Down