diff --git a/dashboard.py b/dashboard.py
index f21b13b..4c24099 100644
--- a/dashboard.py
+++ b/dashboard.py
@@ -18,960 +18,46 @@
To run Coach Dashboard, run the following command:
python3 dashboard.py
"""
-
-from utils import *
import os
-import datetime
-import sys
-import wx
-import random
-import pandas as pd
-from pandas.io.common import EmptyDataError
-import numpy as np
-import colorsys
-from bokeh.palettes import Dark2
-from bokeh.layouts import row, column, widgetbox, Spacer
-from bokeh.models import ColumnDataSource, Range1d, LinearAxis, HoverTool, WheelZoomTool, PanTool, Legend
-from bokeh.models.widgets import RadioButtonGroup, MultiSelect, Button, Select, Slider, Div, CheckboxGroup
-from bokeh.models.glyphs import Patch
-from bokeh.plotting import figure, show, curdoc
-from utils import force_list
-from utils import squeeze_list
-from itertools import cycle
-from os import listdir
-from os.path import isfile, join, isdir, basename
-from enum import Enum
+from dashboard_components.experiment_board import display_directory_group, display_files, averaging_slider_dummy_source
+from dashboard_components.globals import doc
+import dashboard_components.boards # needed for setting the layouts global variable
+from dashboard_components.landing_page import landing_page
-
-class DialogApp(wx.App):
- def getFileDialog(self):
- with wx.FileDialog(None, "Open CSV file", wildcard="CSV files (*.csv)|*.csv",
- style=wx.FD_OPEN | wx.FD_FILE_MUST_EXIST | wx.FD_CHANGE_DIR | wx.FD_MULTIPLE) as fileDialog:
- if fileDialog.ShowModal() == wx.ID_CANCEL:
- return None # the user changed their mind
- else:
- # Proceed loading the file chosen by the user
- return fileDialog.GetPaths()
-
- def getDirDialog(self):
- with wx.DirDialog (None, "Choose input directory", "",
- style=wx.FD_OPEN | wx.FD_FILE_MUST_EXIST | wx.FD_CHANGE_DIR) as dirDialog:
- if dirDialog.ShowModal() == wx.ID_CANCEL:
- return None # the user changed their mind
- else:
- # Proceed loading the dir chosen by the user
- return dirDialog.GetPath()
-class Signal:
- def __init__(self, name, parent):
- self.name = name
- self.full_name = "{}/{}".format(parent.filename, self.name)
- self.selected = False
- self.color = random.choice(Dark2[8])
- self.line = None
- self.bands = None
- self.bokeh_source = parent.bokeh_source
- self.min_val = 0
- self.max_val = 0
- self.axis = 'default'
- self.sub_signals = []
- for name in self.bokeh_source.data.keys():
- if (len(name.split('/')) == 1 and name == self.name) or '/'.join(name.split('/')[:-1]) == self.name:
- self.sub_signals.append(name)
- if len(self.sub_signals) > 1:
- self.mean_signal = squeeze_list([name for name in self.sub_signals if 'Mean' in name.split('/')[-1]])
- self.stdev_signal = squeeze_list([name for name in self.sub_signals if 'Stdev' in name.split('/')[-1]])
- self.min_signal = squeeze_list([name for name in self.sub_signals if 'Min' in name.split('/')[-1]])
- self.max_signal = squeeze_list([name for name in self.sub_signals if 'Max' in name.split('/')[-1]])
- else:
- self.mean_signal = squeeze_list(self.name)
- self.stdev_signal = None
- self.min_signal = None
- self.max_signal = None
- self.has_bollinger_bands = False
- if self.mean_signal and self.stdev_signal and self.min_signal and self.max_signal:
- self.has_bollinger_bands = True
- self.show_bollinger_bands = False
- self.bollinger_bands_source = None
- self.update_range()
-
- def set_color(self, color):
- self.color = color
- if self.line:
- self.line.glyph.line_color = color
- if self.bands:
- self.bands.glyph.fill_color = color
-
- def set_selected(self, val):
- global current_color
- if self.selected != val:
- self.selected = val
- if self.line:
- # self.set_color(Dark2[8][current_color])
- # current_color = (current_color + 1) % len(Dark2[8])
- self.line.visible = self.selected
- if self.bands:
- self.bands.visible = self.selected and self.show_bollinger_bands
- elif self.selected:
- # lazy plotting - plot only when selected for the first time
- show_spinner()
- self.set_color(Dark2[8][current_color])
- current_color = (current_color + 1) % len(Dark2[8])
- if self.has_bollinger_bands:
- self.set_bands_source()
- self.create_bands()
- self.line = plot.line('index', self.mean_signal, source=self.bokeh_source,
- line_color=self.color, line_width=2)
- self.line.visible = True
- hide_spinner()
-
- def set_dash(self, dash):
- self.line.glyph.line_dash = dash
-
- def create_bands(self):
- self.bands = plot.patch(x='band_x', y='band_y', source=self.bollinger_bands_source,
- color=self.color, fill_alpha=0.4, alpha=0.1, line_width=0)
- self.bands.visible = self.show_bollinger_bands
- # self.min_line = plot.line('index', self.min_signal, source=self.bokeh_source,
- # line_color=self.color, line_width=3, line_dash="4 4")
- # self.max_line = plot.line('index', self.max_signal, source=self.bokeh_source,
- # line_color=self.color, line_width=3, line_dash="4 4")
- # self.min_line.visible = self.show_bollinger_bands
- # self.max_line.visible = self.show_bollinger_bands
-
- def set_bands_source(self):
- x_ticks = self.bokeh_source.data['index']
- mean_values = self.bokeh_source.data[self.mean_signal]
- stdev_values = self.bokeh_source.data[self.stdev_signal]
- band_x = np.append(x_ticks, x_ticks[::-1])
- band_y = np.append(mean_values - stdev_values, mean_values[::-1] + stdev_values[::-1])
- source_data = {'band_x': band_x, 'band_y': band_y}
- if self.bollinger_bands_source:
- self.bollinger_bands_source.data = source_data
- else:
- self.bollinger_bands_source = ColumnDataSource(source_data)
-
- def change_bollinger_bands_state(self, new_state):
- self.show_bollinger_bands = new_state
- if self.bands and self.selected:
- self.bands.visible = new_state
- # self.min_line.visible = new_state
- # self.max_line.visible = new_state
-
- def update_range(self):
- self.min_val = np.min(self.bokeh_source.data[self.mean_signal])
- self.max_val = np.max(self.bokeh_source.data[self.mean_signal])
-
- def set_axis(self, axis):
- self.axis = axis
- self.line.y_range_name = axis
-
- def toggle_axis(self):
- if self.axis == 'default':
- self.set_axis('secondary')
- else:
- self.set_axis('default')
-
-
-class SignalsFileBase:
- def __init__(self):
- self.full_csv_path = ""
- self.dir = ""
- self.filename = ""
- self.signals_averaging_window = 1
- self.show_bollinger_bands = False
- self.csv = None
- self.bokeh_source = None
- self.bokeh_source_orig = None
- self.last_modified = None
- self.signals = {}
- self.separate_files = False
-
- def load_csv(self):
- pass
-
- def update_source_and_signals(self):
- # create bokeh data sources
- self.bokeh_source_orig = ColumnDataSource(self.csv)
- self.bokeh_source_orig.data['index'] = self.bokeh_source_orig.data[x_axis]
-
- if self.bokeh_source is None:
- self.bokeh_source = ColumnDataSource(self.csv)
- else:
- # self.bokeh_source.data = self.bokeh_source_orig.data
- # smooth the data if necessary
- self.change_averaging_window(self.signals_averaging_window, force=True)
-
- # create all the signals
- if len(self.signals.keys()) == 0:
- self.signals = {}
- unique_signal_names = []
- for name in self.csv.columns:
- if len(name.split('/')) == 1:
- unique_signal_names.append(name)
- else:
- unique_signal_names.append('/'.join(name.split('/')[:-1]))
- unique_signal_names = list(set(unique_signal_names))
- for signal_name in unique_signal_names:
- self.signals[signal_name] = Signal(signal_name, self)
-
- def load(self):
- self.load_csv()
- self.update_source_and_signals()
-
- def reload_data(self, signals):
- # this function is a workaround to reload the data of all the signals
- # if the data doesn't change, bokeh does not refreshes the line
- self.change_averaging_window(self.signals_averaging_window + 1, force=True)
- self.change_averaging_window(self.signals_averaging_window - 1, force=True)
-
- def change_averaging_window(self, new_size, force=False, signals=None):
- if force or self.signals_averaging_window != new_size:
- self.signals_averaging_window = new_size
- win = np.ones(new_size) / new_size
- temp_data = self.bokeh_source_orig.data.copy()
- for col in self.bokeh_source.data.keys():
- if col == 'index' or col in x_axis_options \
- or (signals and not any(col in signal for signal in signals)):
- temp_data[col] = temp_data[col][:-new_size]
- continue
- temp_data[col] = np.convolve(self.bokeh_source_orig.data[col], win, mode='same')[:-new_size]
- self.bokeh_source.data = temp_data
-
- # smooth bollinger bands
- for signal in self.signals.values():
- if signal.has_bollinger_bands:
- signal.set_bands_source()
-
- def hide_all_signals(self):
- for signal_name in self.signals.keys():
- self.set_signal_selection(signal_name, False)
-
- def set_signal_selection(self, signal_name, val):
- self.signals[signal_name].set_selected(val)
-
- def change_bollinger_bands_state(self, new_state):
- self.show_bollinger_bands = new_state
- for signal in self.signals.values():
- signal.change_bollinger_bands_state(new_state)
-
- def file_was_modified_on_disk(self):
- pass
-
- def get_range_of_selected_signals_on_axis(self, axis, selected_signal=None):
- max_val = -float('inf')
- min_val = float('inf')
- for signal in self.signals.values():
- if (selected_signal and signal.name == selected_signal) or (signal.selected and signal.axis == axis):
- max_val = max(max_val, signal.max_val)
- min_val = min(min_val, signal.min_val)
- return min_val, max_val
-
- def get_selected_signals(self):
- signals = []
- for signal in self.signals.values():
- if signal.selected:
- signals.append(signal)
- return signals
-
- def show_files_separately(self, val):
- pass
-
-
-class SignalsFile(SignalsFileBase):
- def __init__(self, csv_path, load=True):
- SignalsFileBase.__init__(self)
- self.full_csv_path = csv_path
- self.dir, self.filename, _ = break_file_path(csv_path)
- if load:
- self.load()
- # this helps set the correct x axis
- self.change_averaging_window(1, force=True)
-
- def load_csv(self):
- # load csv and fix sparse data.
- # csv can be in the middle of being written so we use try - except
- self.csv = None
- while self.csv is None:
- try:
- self.csv = pd.read_csv(self.full_csv_path)
- break
- except EmptyDataError:
- self.csv = None
- continue
- self.csv = self.csv.interpolate()
- self.csv.fillna(value=0, inplace=True)
-
- self.last_modified = os.path.getmtime(self.full_csv_path)
-
- def file_was_modified_on_disk(self):
- return self.last_modified != os.path.getmtime(self.full_csv_path)
-
-
-class SignalsFilesGroup(SignalsFileBase):
- def __init__(self, csv_paths):
- SignalsFileBase.__init__(self)
- self.full_csv_paths = csv_paths
- self.signals_files = []
- if len(csv_paths) == 1 and os.path.isdir(csv_paths[0]):
- self.signals_files = [SignalsFile(str(file), load=False) for file in add_directory_csv_files(csv_paths[0])]
- else:
- for csv_path in csv_paths:
- if os.path.isdir(csv_path):
- self.signals_files.append(SignalsFilesGroup(add_directory_csv_files(csv_path)))
- else:
- self.signals_files.append(SignalsFile(str(csv_path), load=False))
- if len(csv_paths) == 1:
- # get the parent directory name (since the current directory is the timestamp directory)
- self.dir = os.path.abspath(os.path.join(os.path.dirname(csv_paths[0]), '..'))
- else:
- # get the common directory for all the experiments
- self.dir = os.path.dirname(os.path.commonprefix(csv_paths))
- self.filename = '{} - Group({})'.format(basename(self.dir), len(self.signals_files))
- self.load()
-
- # this helps set the correct x axis
- self.change_averaging_window(1, force=True)
-
- def load_csv(self):
- corrupted_files_idx = []
- for idx, signal_file in enumerate(self.signals_files):
- signal_file.load_csv()
- if not all(option in signal_file.csv.keys() for option in x_axis_options):
- print("Warning: {} file seems to be corrupted and does contain the necessary columns "
- "and will not be rendered".format(signal_file.filename))
- corrupted_files_idx.append(idx)
-
- for file_idx in corrupted_files_idx:
- del self.signals_files[file_idx]
-
- # get the stats of all the columns
- csv_group = pd.concat([signals_file.csv for signals_file in self.signals_files])
- columns_to_remove = [s for s in csv_group.columns if '/Stdev' in s] + \
- [s for s in csv_group.columns if '/Min' in s] + \
- [s for s in csv_group.columns if '/Max' in s]
- for col in columns_to_remove:
- del csv_group[col]
- csv_group = csv_group.groupby(csv_group.index)
- self.csv_mean = csv_group.mean()
- self.csv_mean.columns = [s + '/Mean' for s in self.csv_mean.columns]
- self.csv_stdev = csv_group.std()
- self.csv_stdev.columns = [s + '/Stdev' for s in self.csv_stdev.columns]
- self.csv_min = csv_group.min()
- self.csv_min.columns = [s + '/Min' for s in self.csv_min.columns]
- self.csv_max = csv_group.max()
- self.csv_max.columns = [s + '/Max' for s in self.csv_max.columns]
-
- # get the indices from the file with the least number of indices and which is not an evaluation worker
- file_with_min_indices = self.signals_files[0]
- for signals_file in self.signals_files:
- if signals_file.csv.shape[0] < file_with_min_indices.csv.shape[0] and \
- 'Training reward' in signals_file.csv.keys():
- file_with_min_indices = signals_file
- self.index_columns = file_with_min_indices.csv[x_axis_options]
-
- # concat the stats and the indices columns
- num_rows = file_with_min_indices.csv.shape[0]
- self.csv = pd.concat([self.index_columns, self.csv_mean.head(num_rows), self.csv_stdev.head(num_rows),
- self.csv_min.head(num_rows), self.csv_max.head(num_rows)], axis=1)
-
- # remove the stat columns for the indices columns
- columns_to_remove = [s + '/Mean' for s in x_axis_options] + \
- [s + '/Stdev' for s in x_axis_options] + \
- [s + '/Min' for s in x_axis_options] + \
- [s + '/Max' for s in x_axis_options]
- for col in columns_to_remove:
- del self.csv[col]
-
- # remove NaNs
- self.csv.fillna(value=0, inplace=True) # removing this line will make bollinger bands fail
- for key in self.csv.keys():
- if 'Stdev' in key and 'Evaluation' not in key:
- self.csv[key] = self.csv[key].fillna(value=0)
-
- for signal_file in self.signals_files:
- signal_file.update_source_and_signals()
-
- def change_averaging_window(self, new_size, force=False, signals=None):
- for signal_file in self.signals_files:
- signal_file.change_averaging_window(new_size, force, signals)
- SignalsFileBase.change_averaging_window(self, new_size, force, signals)
-
- def set_signal_selection(self, signal_name, val):
- self.show_files_separately(self.separate_files)
- SignalsFileBase.set_signal_selection(self, signal_name, val)
-
- def file_was_modified_on_disk(self):
- for signal_file in self.signals_files:
- if signal_file.file_was_modified_on_disk():
- return True
- return False
-
- def show_files_separately(self, val):
- self.separate_files = val
- for signal in self.signals.values():
- if signal.selected:
- if val:
- signal.set_dash("4 4")
- else:
- signal.set_dash("")
- for signal_file in self.signals_files:
- try:
- if val:
- signal_file.set_signal_selection(signal.name, signal.selected)
- else:
- signal_file.set_signal_selection(signal.name, False)
- except:
- pass
-
-
-class RunType(Enum):
- SINGLE_FOLDER_SINGLE_FILE = 1
- SINGLE_FOLDER_MULTIPLE_FILES = 2
- MULTIPLE_FOLDERS_SINGLE_FILES = 3
- MULTIPLE_FOLDERS_MULTIPLE_FILES = 4
- UNKNOWN = 0
-
-
-class FolderType(Enum):
- SINGLE_FILE = 1
- MULTIPLE_FILES = 2
- MULTIPLE_FOLDERS = 3
- EMPTY = 4
-
-dialog = DialogApp()
-
-# read data
-patches = {}
-signals_files = {}
-selected_file = None
-x_axis = 'Episode #'
-x_axis_options = ['Episode #', 'Total steps', 'Wall-Clock Time']
-current_color = 0
-
-# spinner
-root_dir = os.path.dirname(os.path.abspath(__file__))
-with open(os.path.join(root_dir, 'spinner.css'), 'r') as f:
- spinner_style = """""".format(f.read())
-spinner_html = """
"""
-spinner = Div(text="""""")
-
-# file refresh time placeholder
-refresh_info = Div(text="""""", width=210)
-
-# create figures
-plot = figure(plot_width=1200, plot_height=800,
- tools='pan,box_zoom,wheel_zoom,crosshair,undo,redo,reset,save',
- toolbar_location='above', x_axis_label='Episodes',
- x_range=Range1d(0, 10000), y_range=Range1d(0, 100000))
-plot.extra_y_ranges = {"secondary": Range1d(start=-100, end=200)}
-plot.add_layout(LinearAxis(y_range_name="secondary"), 'right')
-
-# legend
-div = Div(text="""""")
-legend = widgetbox([div])
-
-bokeh_legend = Legend(
- items=[("12345678901234567890123456789012345678901234567890", [])], # 50 letters
- # items=[(" ", [])], # 50 letters
- location=(-20, 0), orientation="vertical",
- border_line_color="black",
- label_text_font_size={'value': '9pt'},
- margin=30
-)
-plot.add_layout(bokeh_legend, "right")
-
-
-def update_axis_range(name, range_placeholder):
- max_val = -float('inf')
- min_val = float('inf')
- selected_signal = None
- if name in x_axis_options:
- selected_signal = name
- for signals_file in signals_files.values():
- curr_min_val, curr_max_val = signals_file.get_range_of_selected_signals_on_axis(name, selected_signal)
- max_val = max(max_val, curr_max_val)
- min_val = min(min_val, curr_min_val)
- if min_val != float('inf'):
- range = max_val - min_val
- range_placeholder.start = min_val - 0.1 * range
- range_placeholder.end = max_val + 0.1 * range
-
-
-# update axes ranges
-def update_ranges():
- update_axis_range('default', plot.y_range)
- update_axis_range('secondary', plot.extra_y_ranges['secondary'])
-
-
-def get_all_selected_signals():
- signals = []
- for signals_file in signals_files.values():
- signals += signals_file.get_selected_signals()
- return signals
-
-
-# update legend using the legend text dictionary
-def update_legend():
- legend_text = """"""
- selected_signals = get_all_selected_signals()
- items = []
- for signal in selected_signals:
- side_sign = "<" if signal.axis == 'default' else ">"
- legend_text += """{} {}
"""\
- .format(signal.color, side_sign, signal.full_name)
- items.append((signal.full_name, [signal.line]))
- div.text = legend_text
- # the visible=false => visible=true is a hack to make the legend render again
- bokeh_legend.visible = False
- bokeh_legend.items = items
- bokeh_legend.visible = True
-
-
-# select lines to display
-def select_data(args, old, new):
- if selected_file is None:
- return
- show_spinner()
- selected_signals = new
- for signal_name in selected_file.signals.keys():
- is_selected = signal_name in selected_signals
- selected_file.set_signal_selection(signal_name, is_selected)
-
- # update axes ranges
- update_ranges()
- update_axis_range(x_axis, plot.x_range)
-
- # update the legend
- update_legend()
-
- hide_spinner()
-
-
-# add new lines to the plot
-def plot_signals(signals_file, signals):
- for idx, signal in enumerate(signals):
- signal.line = plot.line('index', signal.name, source=signals_file.bokeh_source,
- line_color=signal.color, line_width=2)
-
-
-def open_file_dialog():
- return dialog.getFileDialog()
-
-
-def open_directory_dialog():
- return dialog.getDirDialog()
-
-
-def show_spinner():
- spinner.text = spinner_style + spinner_html
-
-
-def hide_spinner():
- spinner.text = ""
-
-
-# will create a group from the files
-def create_files_group_signal(files):
- global selected_file
- signals_file = SignalsFilesGroup(files)
- signals_files[signals_file.filename] = signals_file
-
- filenames = [signals_file.filename]
- files_selector.options += filenames
- files_selector.value = filenames[0]
- selected_file = signals_file
-
-
-# load files from disk as a group
-def load_files_group():
- show_spinner()
- files = open_file_dialog()
- # no files selected
- if not files or not files[0]:
- hide_spinner()
- return
-
- change_displayed_doc()
-
- if len(files) == 1:
- create_files_signal(files)
- else:
- create_files_group_signal(files)
-
- change_selected_signals_in_data_selector([""])
- hide_spinner()
-
-
-# classify the folder as containing a single file, multiple files or only folders
-def classify_folder(dir_path):
- files = [f for f in listdir(dir_path) if isfile(join(dir_path, f)) and f.endswith('.csv')]
- folders = [d for d in listdir(dir_path) if isdir(join(dir_path, d))]
- if len(files) == 1:
- return FolderType.SINGLE_FILE
- elif len(files) > 1:
- return FolderType.MULTIPLE_FILES
- elif len(folders) >= 1:
- return FolderType.MULTIPLE_FOLDERS
- else:
- return FolderType.EMPTY
-
-
-# finds if this is single-threaded or multi-threaded
-def get_run_type(dir_path):
- folder_type = classify_folder(dir_path)
- if folder_type == FolderType.SINGLE_FILE:
- return RunType.SINGLE_FOLDER_SINGLE_FILE
-
- elif folder_type == FolderType.MULTIPLE_FILES:
- return RunType.SINGLE_FOLDER_MULTIPLE_FILES
-
- elif folder_type == FolderType.MULTIPLE_FOLDERS:
- # folder contains sub dirs -> we assume we can classify the folder using only the first sub dir
- sub_dirs = [d for d in listdir(dir_path) if isdir(join(dir_path, d))]
-
- # checking only the first folder in the root dir for its type, since we assume that all sub dirs will share the
- # same structure (i.e. if one is a result of multi-threaded run, so will all the other).
- folder_type = classify_folder(os.path.join(dir_path, sub_dirs[0]))
- if folder_type == FolderType.SINGLE_FILE:
- folder_type = RunType.MULTIPLE_FOLDERS_SINGLE_FILES
- elif folder_type == FolderType.MULTIPLE_FILES:
- folder_type = RunType.MULTIPLE_FOLDERS_MULTIPLE_FILES
- return folder_type
-
-
-# takes path to dir and recursively adds all it's files to paths
-def add_directory_csv_files(dir_path, paths=None):
- if not paths:
- paths = []
-
- for p in listdir(dir_path):
- path = join(dir_path, p)
- if isdir(path):
- # call recursively for each dir
- paths = add_directory_csv_files(path, paths)
- elif isfile(path) and path.endswith('.csv'):
- # add every file to the list
- paths.append(path)
-
- return paths
-
-
-# create a signal file from the directory path according to the directory underlying structure
-def handle_dir(dir_path, run_type):
- paths = add_directory_csv_files(dir_path)
- if run_type == RunType.SINGLE_FOLDER_SINGLE_FILE:
- create_files_signal(paths)
- elif run_type == RunType.SINGLE_FOLDER_MULTIPLE_FILES:
- create_files_group_signal(paths)
- elif run_type == RunType.MULTIPLE_FOLDERS_SINGLE_FILES:
- create_files_group_signal(paths)
- elif run_type == RunType.MULTIPLE_FOLDERS_MULTIPLE_FILES:
- sub_dirs = [d for d in listdir(dir_path) if isdir(join(dir_path, d))]
- # for d in sub_dirs:
- # paths = add_directory_csv_files(os.path.join(dir_path, d))
- # create_files_group_signal(paths)
- create_files_group_signal([os.path.join(dir_path, d) for d in sub_dirs])
-
-
-# load directory from disk as a group
-def load_directory_group():
- global selected_file
- show_spinner()
- directory = open_directory_dialog()
- # no files selected
- if not directory:
- hide_spinner()
- return
-
- change_displayed_doc()
-
- handle_dir(directory, get_run_type(directory))
-
- change_selected_signals_in_data_selector([""])
- hide_spinner()
-
-
-def create_files_signal(files):
- global selected_file
- new_signal_files = []
- for idx, file_path in enumerate(files):
- signals_file = SignalsFile(str(file_path))
- signals_files[signals_file.filename] = signals_file
- new_signal_files.append(signals_file)
-
- filenames = [f.filename for f in new_signal_files]
-
- files_selector.options += filenames
- files_selector.value = filenames[0]
- selected_file = new_signal_files[0]
-
-
-# load files from disk
-def load_files():
- show_spinner()
- files = open_file_dialog()
-
- # no files selected
- if not files or not files[0]:
- hide_spinner()
- return
-
- create_files_signal(files)
- hide_spinner()
-
- change_selected_signals_in_data_selector([""])
-
-
-def unload_file():
- global selected_file
- global signals_files
- if selected_file is None:
- return
- selected_file.hide_all_signals()
- del signals_files[selected_file.filename]
- data_selector.options = [""]
- filenames = cycle(files_selector.options)
- files_selector.options.remove(selected_file.filename)
- if len(files_selector.options) > 0:
- files_selector.value = next(filenames)
- else:
- files_selector.value = None
- update_legend()
- refresh_info.text = ""
-
-
-# reload the selected csv file
-def reload_all_files(force=False):
- for file_to_load in signals_files.values():
- if force or file_to_load.file_was_modified_on_disk():
- file_to_load.load()
- refresh_info.text = "last update: " + str(datetime.datetime.now()).split(".")[0]
-
-
-# unselect the currently selected signals and then select the requested signals in the data selector
-def change_selected_signals_in_data_selector(selected_signals):
- # the default bokeh way is not working due to a bug since Bokeh 0.12.6 (https://github.com/bokeh/bokeh/issues/6501)
- # this will currently cause the signals to change color
- for value in list(data_selector.value):
- if value in data_selector.options:
- index = data_selector.options.index(value)
- data_selector.options.remove(value)
- data_selector.value.remove(value)
- data_selector.options.insert(index, value)
- data_selector.value = selected_signals
-
-
-# change data options according to the selected file
-def change_data_selector(args, old, new):
- global selected_file
- if new is None:
- selected_file = None
- return
- show_spinner()
- selected_file = signals_files[new]
- data_selector.options = sorted(list(selected_file.signals.keys()))
- selected_signal_names = [s.name for s in selected_file.signals.values() if s.selected]
- if not selected_signal_names:
- selected_signal_names = [""]
- change_selected_signals_in_data_selector(selected_signal_names)
- averaging_slider.value = selected_file.signals_averaging_window
- group_cb.active = [0 if selected_file.show_bollinger_bands else None]
- group_cb.active += [1 if selected_file.separate_files else None]
- hide_spinner()
-
-
-# smooth all the signals of the selected file
-def update_averaging(args, old, new):
- show_spinner()
- selected_file.change_averaging_window(new)
- hide_spinner()
-
-
-def change_x_axis(val):
- global x_axis
- show_spinner()
- x_axis = x_axis_options[val]
- plot.xaxis.axis_label = x_axis
- reload_all_files(force=True)
- update_axis_range(x_axis, plot.x_range)
- hide_spinner()
-
-
-# move the signal between the main and secondary Y axes
-def toggle_second_axis():
- show_spinner()
- signals = selected_file.get_selected_signals()
- for signal in signals:
- signal.toggle_axis()
-
- update_ranges()
- update_legend()
-
- # this is just for redrawing the signals
- selected_file.reload_data([signal.name for signal in signals])
-
- hide_spinner()
-
-
-def toggle_group_property(new):
- # toggle show / hide Bollinger bands
- selected_file.change_bollinger_bands_state(0 in new)
-
- # show a separate signal for each file in a group
- selected_file.show_files_separately(1 in new)
-
-
-def change_displayed_doc():
- if doc.roots[0] == landing_page:
- doc.remove_root(landing_page)
- doc.add_root(layout)
-
-
-# Color selection - most of these functions are taken from bokeh examples (plotting/color_sliders.py)
-def select_color(attr, old, new):
- show_spinner()
- signals = selected_file.get_selected_signals()
- for signal in signals:
- signal.set_color(rgb_to_hex(crRGBs[new['1d']['indices'][0]]))
- hide_spinner()
-
-
-def generate_color_range(N, I):
- HSV_tuples = [(x*1.0/N, 0.5, I) for x in range(N)]
- RGB_tuples = map(lambda x: colorsys.hsv_to_rgb(*x), HSV_tuples)
- for_conversion = []
- for RGB_tuple in RGB_tuples:
- for_conversion.append((int(RGB_tuple[0]*255), int(RGB_tuple[1]*255), int(RGB_tuple[2]*255)))
- hex_colors = [rgb_to_hex(RGB_tuple) for RGB_tuple in for_conversion]
- return hex_colors, for_conversion
-
-
-# convert RGB tuple to hexadecimal code
-def rgb_to_hex(rgb):
- return '#%02x%02x%02x' % rgb
-
-
-# convert hexadecimal to RGB tuple
-def hex_to_dec(hex):
- red = ''.join(hex.strip('#')[0:2])
- green = ''.join(hex.strip('#')[2:4])
- blue = ''.join(hex.strip('#')[4:6])
- return int(red, 16), int(green, 16), int(blue,16)
-
-color_resolution = 1000
-brightness = 0.75 # change to have brighter/darker colors
-crx = list(range(1, color_resolution+1)) # the resolution is 1000 colors
-cry = [5 for i in range(len(crx))]
-crcolor, crRGBs = generate_color_range(color_resolution, brightness) # produce spectrum
-
-
-# ---------------- Build Website Layout -------------------
-
-# select file
-file_selection_button = Button(label="Select Files", button_type="success", width=120)
-file_selection_button.on_click(load_files_group)
-
-files_selector_spacer = Spacer(width=10)
-
-group_selection_button = Button(label="Select Directory", button_type="primary", width=140)
-group_selection_button.on_click(load_directory_group)
-
-unload_file_button = Button(label="Unload", button_type="danger", width=50)
-unload_file_button.on_click(unload_file)
-
-# files selection box
-files_selector = Select(title="Files:", options=[], width=200)
-files_selector.on_change('value', change_data_selector)
-
-# data selection box
-data_selector = MultiSelect(title="Data:", options=[], size=12)
-data_selector.on_change('value', select_data)
-
-# x axis selection box
-x_axis_selector_title = Div(text="""X Axis:""")
-x_axis_selector = RadioButtonGroup(labels=x_axis_options, active=0)
-x_axis_selector.on_click(change_x_axis)
-
-# toggle second axis button
-toggle_second_axis_button = Button(label="Toggle Second Axis", button_type="success")
-toggle_second_axis_button.on_click(toggle_second_axis)
-
-# averaging slider
-averaging_slider = Slider(title="Averaging window", start=1, end=101, step=10)
-averaging_slider.on_change('value', update_averaging)
-
-# group properties checkbox
-group_cb = CheckboxGroup(labels=["Show statistics bands", "Ungroup signals"], active=[])
-group_cb.on_click(toggle_group_property)
-
-# color selector
-color_selector_title = Div(text="""Select Color:""")
-crsource = ColumnDataSource(data=dict(x=crx, y=cry, crcolor=crcolor, RGBs=crRGBs))
-color_selector = figure(x_range=(0, color_resolution), y_range=(0, 10),
- plot_width=300, plot_height=40,
- tools='tap')
-color_selector.axis.visible = False
-color_range = color_selector.rect(x='x', y='y', width=1, height=10,
- color='crcolor', source=crsource)
-crsource.on_change('selected', select_color)
-color_range.nonselection_glyph = color_range.glyph
-color_selector.toolbar.logo = None
-color_selector.toolbar_location = None
-
-# title
-title = Div(text="""Coach Dashboard
""")
-
-# landing page
-landing_page_description = Div(text="""Start by selecting an experiment file or directory to open:
""")
-center = Div(text="""""")
-center_buttons = Div(text="""""", width=0)
-landing_page = column(center,
- title,
- landing_page_description,
- row(center_buttons),
- row(file_selection_button, sizing_mode='scale_width'),
- row(group_selection_button, sizing_mode='scale_width'),
- sizing_mode='scale_width')
-
-# main layout of the document
-layout = row(file_selection_button, files_selector_spacer, group_selection_button, width=300)
-layout = column(layout, files_selector)
-layout = column(layout, row(refresh_info, unload_file_button))
-layout = column(layout, data_selector)
-layout = column(layout, color_selector_title)
-layout = column(layout, color_selector)
-layout = column(layout, x_axis_selector_title)
-layout = column(layout, x_axis_selector)
-layout = column(layout, group_cb)
-layout = column(layout, toggle_second_axis_button)
-layout = column(layout, averaging_slider)
-# layout = column(layout, legend)
-layout = row(layout, plot)
-layout = column(title, layout)
-layout = column(layout, spinner)
-
-doc = curdoc()
doc.add_root(landing_page)
-doc.add_periodic_callback(reload_all_files, 20000)
-plot.y_range = Range1d(0, 100)
-plot.extra_y_ranges['secondary'] = Range1d(0, 100)
+import argparse
+import glob
-# show load file dialog immediately on start
-#doc.add_timeout_callback(load_files, 1000)
+parser = argparse.ArgumentParser()
+parser.add_argument('-d', '--experiment_dir',
+ help="(string) The path of an experiment dir to open",
+ default=None,
+ type=str)
+parser.add_argument('-f', '--experiment_files',
+ help="(string) The path of an experiment file to open",
+ default=None,
+ type=str)
+args = parser.parse_args()
+
+if args.experiment_dir:
+ doc.add_timeout_callback(lambda: display_directory_group(args.experiment_dir), 1000)
+elif args.experiment_files:
+ files = []
+ for file_pattern in args.experiment_files:
+ files.extend(glob.glob(args.experiment_files))
+ doc.add_timeout_callback(lambda: display_files(files), 1000)
if __name__ == "__main__":
- # find an open port and run the server
- import socket
- s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
- port = 12345
- while True:
- try:
- s.bind(("127.0.0.1", port))
- break
- except socket.error as e:
- if e.errno == 98:
- port += 1
- s.close()
- os.system('bokeh serve --show dashboard.py --port {}'.format(port))
+ from utils import get_open_port
+
+ command = 'bokeh serve --show dashboard.py --port {}'.format(get_open_port())
+ if args.experiment_dir or args.experiment_files:
+ command += ' --args'
+ if args.experiment_dir:
+ command += ' --experiment_dir {}'.format(args.experiment_dir)
+ if args.experiment_files:
+ command += ' --experiment_files {}'.format(args.experiment_files)
+
+ os.system(command)
diff --git a/dashboard_components/__init__.py b/dashboard_components/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/dashboard_components/boards.py b/dashboard_components/boards.py
new file mode 100644
index 0000000..dcbb5a3
--- /dev/null
+++ b/dashboard_components/boards.py
@@ -0,0 +1,18 @@
+from bokeh.layouts import column
+from bokeh.models.widgets import Panel, Tabs
+from dashboard_components.experiment_board import experiment_board_layout
+from dashboard_components.globals import spinner, layouts
+from bokeh.models.widgets import Div
+
+# ---------------- Build Website Layout -------------------
+
+# title
+title = Div(text="""Coach Dashboard
""")
+
+tab1 = Panel(child=experiment_board_layout, title='experiment board')
+tabs = Tabs(tabs=[tab1])
+
+layout = column(title, tabs)
+layout = column(layout, spinner)
+
+layouts['boards'] = layout
diff --git a/dashboard_components/experiment_board.py b/dashboard_components/experiment_board.py
new file mode 100644
index 0000000..a37cc87
--- /dev/null
+++ b/dashboard_components/experiment_board.py
@@ -0,0 +1,473 @@
+
+import datetime
+import os
+import sys
+import time
+from itertools import cycle
+from os import listdir
+from os.path import isfile, join, isdir
+
+from bokeh.models.callbacks import CustomJS
+from bokeh.layouts import row, column, Spacer
+from bokeh.models import ColumnDataSource, Range1d, LinearAxis, Legend
+from bokeh.models.widgets import RadioButtonGroup, MultiSelect, Button, Select, Slider, Div, CheckboxGroup
+from bokeh.plotting import figure
+
+from dashboard_components.globals import signals_files, x_axis_labels, x_axis_options, show_spinner, hide_spinner, \
+ x_axis, dialog, FolderType, RunType, add_directory_csv_files, doc, display_boards, layouts, \
+ crcolor, crx, cry, color_resolution, crRGBs, rgb_to_hex
+from dashboard_components.signals_files_group import SignalsFilesGroup
+from dashboard_components.signals_file import SignalsFile
+
+
+def update_axis_range(name, range_placeholder):
+ max_val = -float('inf')
+ min_val = float('inf')
+ selected_signal = None
+ if name in x_axis_options:
+ selected_signal = name
+ for signals_file in signals_files.values():
+ curr_min_val, curr_max_val = signals_file.get_range_of_selected_signals_on_axis(name, selected_signal)
+ max_val = max(max_val, curr_max_val)
+ min_val = min(min_val, curr_min_val)
+ if min_val != float('inf'):
+ range = max_val - min_val
+ range_placeholder.start = min_val - 0.1 * range
+ range_placeholder.end = max_val + 0.1 * range
+
+
+# update axes ranges
+def update_y_axis_ranges():
+ update_axis_range('default', plot.y_range)
+ update_axis_range('secondary', plot.extra_y_ranges['secondary'])
+
+
+def update_x_axis_ranges():
+ update_axis_range(x_axis[0], plot.x_range)
+
+
+def get_all_selected_signals():
+ signals = []
+ for signals_file in signals_files.values():
+ signals += signals_file.get_selected_signals()
+ return signals
+
+
+# update legend using the legend text dictionary
+def update_legend():
+ selected_signals = get_all_selected_signals()
+ items = []
+ for signal in selected_signals:
+ side_sign = "◀" if signal.axis == 'default' else "▶"
+ items.append((side_sign + " " + signal.full_name, [signal.line]))
+ # the visible=false => visible=true is a hack to make the legend render again
+ bokeh_legend.visible = False
+ bokeh_legend.items = items # this step takes a long time because it is redrawing the plot
+ bokeh_legend.visible = True
+
+
+# select lines to display
+def select_data(args, old, new):
+ if selected_file is None:
+ return
+ show_spinner("Updating the signal selection...")
+ selected_signals = new
+ for signal_name in selected_file.signals.keys():
+ is_selected = signal_name in selected_signals
+ selected_file.set_signal_selection(signal_name, is_selected)
+
+ # update axes ranges
+ update_y_axis_ranges()
+ update_x_axis_ranges()
+
+ # update the legend
+ update_legend()
+
+ hide_spinner()
+
+
+# add new lines to the plot
+def plot_signals(signals_file, signals):
+ for idx, signal in enumerate(signals):
+ signal.line = plot.line('index', signal.name, source=signals_file.bokeh_source,
+ line_color=signal.color, line_width=2)
+
+
+def open_file_dialog():
+ return dialog.getFileDialog()
+
+
+def open_directory_dialog():
+ return dialog.getDirDialog()
+
+
+# will create a group from the files
+def create_files_group_signal(files):
+ global selected_file
+ signals_file = SignalsFilesGroup(files, plot)
+ signals_files[signals_file.filename] = signals_file
+
+ filenames = [signals_file.filename]
+ files_selector.options += filenames
+ files_selector.value = filenames[0]
+ selected_file = signals_file
+
+
+# load files from disk as a group
+def load_files_group():
+ show_spinner("Loading files group...")
+ files = open_file_dialog()
+ # no files selected
+ if not files or not files[0]:
+ hide_spinner()
+ return
+
+ display_boards()
+
+ if len(files) == 1:
+ create_files_signal(files)
+ else:
+ create_files_group_signal(files)
+
+ change_selected_signals_in_data_selector([""])
+ hide_spinner()
+
+
+# classify the folder as containing a single file, multiple files or only folders
+def classify_folder(dir_path):
+ files = [f for f in listdir(dir_path) if isfile(join(dir_path, f)) and f.endswith('.csv')]
+ folders = [d for d in listdir(dir_path) if isdir(join(dir_path, d)) and any(f.endswith(".csv") for f in os.listdir(join(dir_path, d)))]
+ if len(files) == 1:
+ return FolderType.SINGLE_FILE
+ elif len(files) > 1:
+ return FolderType.MULTIPLE_FILES
+ elif len(folders) == 1:
+ return classify_folder(join(dir_path, folders[0]))
+ elif len(folders) > 1:
+ return FolderType.MULTIPLE_FOLDERS
+ else:
+ return FolderType.EMPTY
+
+
+# finds if this is single-threaded or multi-threaded
+def get_run_type(dir_path):
+ folder_type = classify_folder(dir_path)
+ if folder_type == FolderType.SINGLE_FILE:
+ folder_type = RunType.SINGLE_FOLDER_SINGLE_FILE
+
+ elif folder_type == FolderType.MULTIPLE_FILES:
+ folder_type = RunType.SINGLE_FOLDER_MULTIPLE_FILES
+
+ elif folder_type == FolderType.MULTIPLE_FOLDERS:
+ # folder contains sub dirs -> we assume we can classify the folder using only the first sub dir
+ sub_dirs = [d for d in listdir(dir_path) if isdir(join(dir_path, d))]
+
+ # checking only the first folder in the root dir for its type, since we assume that all sub dirs will share the
+ # same structure (i.e. if one is a result of multi-threaded run, so will all the other).
+ folder_type = classify_folder(os.path.join(dir_path, sub_dirs[0]))
+ if folder_type == FolderType.SINGLE_FILE:
+ folder_type = RunType.MULTIPLE_FOLDERS_SINGLE_FILES
+ elif folder_type == FolderType.MULTIPLE_FILES:
+ folder_type = RunType.MULTIPLE_FOLDERS_MULTIPLE_FILES
+
+ return folder_type
+
+
+# create a signal file from the directory path according to the directory underlying structure
+def handle_dir(dir_path, run_type):
+ paths = add_directory_csv_files(dir_path)
+ if run_type in [RunType.SINGLE_FOLDER_SINGLE_FILE,
+ RunType.SINGLE_FOLDER_MULTIPLE_FILES,
+ RunType.MULTIPLE_FOLDERS_SINGLE_FILES]:
+ create_files_group_signal(paths)
+ elif run_type == RunType.MULTIPLE_FOLDERS_MULTIPLE_FILES:
+ sub_dirs = [d for d in listdir(dir_path) if isdir(join(dir_path, d))]
+ # for d in sub_dirs:
+ # paths = add_directory_csv_files(os.path.join(dir_path, d))
+ # create_files_group_signal(paths)
+ create_files_group_signal([os.path.join(dir_path, d) for d in sub_dirs])
+
+
+# load directory from disk as a group
+def load_directory_group():
+ show_spinner("Loading directories group...")
+ directory = open_directory_dialog()
+ # no files selected
+ if not directory:
+ hide_spinner()
+ return
+
+ display_directory_group(directory)
+
+
+def display_directory_group(directory):
+ display_boards()
+ show_spinner("Loading directories group...")
+
+ while get_run_type(directory) == FolderType.EMPTY:
+ show_spinner("Waiting for experiment directory to get populated...")
+ sys.stdout.write("Waiting for experiment directory to get populated...\r")
+ time.sleep(10)
+
+ handle_dir(directory, get_run_type(directory))
+
+ change_selected_signals_in_data_selector([""])
+ hide_spinner()
+
+
+def create_files_signal(files):
+ global selected_file
+ new_signal_files = []
+ for idx, file_path in enumerate(files):
+ signals_file = SignalsFile(str(file_path), plot=plot)
+ signals_files[signals_file.filename] = signals_file
+ new_signal_files.append(signals_file)
+
+ filenames = [f.filename for f in new_signal_files]
+
+ files_selector.options += filenames
+ files_selector.value = filenames[0]
+ selected_file = new_signal_files[0]
+
+
+# load files from disk
+def load_files():
+ show_spinner("Loading files...")
+ files = open_file_dialog()
+
+ # no files selected
+ if not files or not files[0]:
+ hide_spinner()
+ return
+
+ display_files(files)
+
+
+def display_files(files):
+ display_boards()
+ show_spinner("Loading files...")
+
+ create_files_signal(files)
+
+ change_selected_signals_in_data_selector([""])
+ hide_spinner()
+
+
+def unload_file():
+ if selected_file is None:
+ return
+ selected_file.hide_all_signals()
+ del signals_files[selected_file.filename]
+ data_selector.options = [""]
+ filenames = cycle(files_selector.options)
+ files_selector.options.remove(selected_file.filename)
+ if len(files_selector.options) > 0:
+ files_selector.value = next(filenames)
+ else:
+ files_selector.value = None
+ update_legend()
+ refresh_info.text = ""
+
+
+# reload the selected csv file
+def reload_all_files(force=False):
+ for file_to_load in signals_files.values():
+ if force or file_to_load.file_was_modified_on_disk():
+ file_to_load.load()
+ refresh_info.text = "last update: " + str(datetime.datetime.now()).split(".")[0]
+
+
+# unselect the currently selected signals and then select the requested signals in the data selector
+def change_selected_signals_in_data_selector(selected_signals):
+ # the default bokeh way is not working due to a bug since Bokeh 0.12.6 (https://github.com/bokeh/bokeh/issues/6501)
+ # remove the data selection callback before updating the selector
+ data_selector.remove_on_change('value', select_data)
+ for value in list(data_selector.value):
+ if value in data_selector.options:
+ index = data_selector.options.index(value)
+ data_selector.options.remove(value)
+ data_selector.value.remove(value)
+ data_selector.options.insert(index, value)
+ data_selector.value = selected_signals
+ # add back the data selection callback
+ data_selector.on_change('value', select_data)
+
+
+# change data options according to the selected file
+def change_data_selector(args, old, new):
+ global selected_file
+ if new is None:
+ selected_file = None
+ return
+ show_spinner("Updating selection...")
+ selected_file = signals_files[new]
+ data_selector.remove_on_change('value', select_data)
+ data_selector.options = sorted(list(selected_file.signals.keys()))
+ data_selector.on_change('value', select_data)
+ selected_signal_names = [s.name for s in selected_file.signals.values() if s.selected]
+ if not selected_signal_names:
+ selected_signal_names = [""]
+ change_selected_signals_in_data_selector(selected_signal_names)
+ averaging_slider.value = selected_file.signals_averaging_window
+ if len(averaging_slider_dummy_source.data['value']) > 0:
+ averaging_slider_dummy_source.data['value'][0] = selected_file.signals_averaging_window
+ group_cb.active = [0 if selected_file.show_bollinger_bands else None]
+ group_cb.active += [1 if selected_file.separate_files else None]
+ hide_spinner()
+
+
+# smooth all the signals of the selected file
+def update_averaging(args, old, new):
+ show_spinner("Smoothing the signals...")
+ # get the actual value from the dummy source
+ new = averaging_slider_dummy_source.data['value'][0]
+ selected_file.change_averaging_window(new)
+ hide_spinner()
+
+
+def change_x_axis(val):
+ global x_axis
+ show_spinner("Updating the X axis...")
+ x_axis[0] = x_axis_options[val]
+ plot.xaxis.axis_label = x_axis_labels[val]
+
+ for file_to_load in signals_files.values():
+ file_to_load.update_x_axis_index()
+
+ update_axis_range(x_axis[0], plot.x_range)
+ hide_spinner()
+
+
+# move the signal between the main and secondary Y axes
+def toggle_second_axis():
+ show_spinner("Switching the Y axis...")
+ plot.yaxis[-1].visible = True
+ selected_file.toggle_y_axis()
+
+ # this is just for redrawing the signals
+ selected_file.reload_data()
+
+ update_y_axis_ranges()
+ update_legend()
+
+ hide_spinner()
+
+
+def toggle_group_property(new):
+ # toggle show / hide Bollinger bands
+ selected_file.change_bollinger_bands_state(0 in new)
+
+ # show a separate signal for each file in a group
+ selected_file.show_files_separately(1 in new)
+
+
+# Color selection - most of these functions are taken from bokeh examples (plotting/color_sliders.py)
+def select_color(attr, old, new):
+ show_spinner("Changing signal color...")
+ signals = selected_file.get_selected_signals()
+ for signal in signals:
+ signal.set_color(rgb_to_hex(crRGBs[new['1d']['indices'][0]]))
+ hide_spinner()
+
+
+doc.add_periodic_callback(reload_all_files, 20000)
+
+# ---------------- Build Website Layout -------------------
+
+# file refresh time placeholder
+refresh_info = Div(text="""""", width=210)
+
+# create figures
+plot = figure(plot_width=1200, plot_height=800,
+ tools='pan,box_zoom,wheel_zoom,crosshair,undo,redo,reset,save',
+ toolbar_location='above', x_axis_label='Episodes',
+ x_range=Range1d(0, 10000), y_range=Range1d(0, 100000))
+plot.extra_y_ranges = {"secondary": Range1d(start=-100, end=200)}
+plot.add_layout(LinearAxis(y_range_name="secondary"), 'right')
+plot.yaxis[-1].visible = False
+
+bokeh_legend = Legend(
+ # items=[("12345678901234567890123456789012345678901234567890", [])], # 50 letters
+ items=[("__________________________________________________", [])], # 50 letters
+ location=(0, 0), orientation="vertical",
+ border_line_color="black",
+ label_text_font_size={'value': '9pt'},
+ margin=30
+)
+plot.add_layout(bokeh_legend, "right")
+plot.y_range = Range1d(0, 100)
+plot.extra_y_ranges['secondary'] = Range1d(0, 100)
+
+# select file
+file_selection_button = Button(label="Select Files", button_type="success", width=120)
+file_selection_button.on_click(load_files_group)
+
+files_selector_spacer = Spacer(width=10)
+
+group_selection_button = Button(label="Select Directory", button_type="primary", width=140)
+group_selection_button.on_click(load_directory_group)
+
+unload_file_button = Button(label="Unload", button_type="danger", width=50)
+unload_file_button.on_click(unload_file)
+
+# files selection box
+files_selector = Select(title="Files:", options=[])
+files_selector.on_change('value', change_data_selector)
+
+# data selection box
+data_selector = MultiSelect(title="Data:", options=[], size=12)
+data_selector.on_change('value', select_data)
+
+# x axis selection box
+x_axis_selector_title = Div(text="""X Axis:""")
+x_axis_selector = RadioButtonGroup(labels=x_axis_options, active=0)
+x_axis_selector.on_click(change_x_axis)
+
+# toggle second axis button
+toggle_second_axis_button = Button(label="Toggle Second Axis", button_type="success")
+toggle_second_axis_button.on_click(toggle_second_axis)
+
+# averaging slider
+# This data source is just used to communicate / trigger the real callback
+averaging_slider_dummy_source = ColumnDataSource(data=dict(value=[]))
+averaging_slider_dummy_source.on_change('data', update_averaging)
+averaging_slider = Slider(title="Averaging window", start=1, end=101, step=10, callback_policy='mouseup')
+averaging_slider.callback = CustomJS(args=dict(source=averaging_slider_dummy_source), code="""
+ source.data = { value: [cb_obj.value] }
+""")
+
+# group properties checkbox
+group_cb = CheckboxGroup(labels=["Show statistics bands", "Ungroup signals"], active=[])
+group_cb.on_click(toggle_group_property)
+
+# color selector
+color_selector_title = Div(text="""Select Color:""")
+crsource = ColumnDataSource(data=dict(x=crx, y=cry, crcolor=crcolor, RGBs=crRGBs))
+color_selector = figure(x_range=(0, color_resolution), y_range=(0, 10),
+ plot_width=300, plot_height=40,
+ tools='tap')
+color_selector.axis.visible = False
+color_range = color_selector.rect(x='x', y='y', width=1, height=10,
+ color='crcolor', source=crsource)
+crsource.on_change('selected', select_color)
+color_range.nonselection_glyph = color_range.glyph
+color_selector.toolbar.logo = None
+color_selector.toolbar_location = None
+
+# main layout of the document
+layout = row(file_selection_button, files_selector_spacer, group_selection_button, width=300)
+layout = column(layout, files_selector)
+layout = column(layout, row(refresh_info, unload_file_button))
+layout = column(layout, data_selector)
+layout = column(layout, color_selector_title)
+layout = column(layout, color_selector)
+layout = column(layout, x_axis_selector_title)
+layout = column(layout, x_axis_selector)
+layout = column(layout, group_cb)
+layout = column(layout, toggle_second_axis_button)
+layout = column(layout, averaging_slider)
+layout = row(layout, plot)
+
+experiment_board_layout = layout
+
+layouts["experiment_board"] = experiment_board_layout
diff --git a/dashboard_components/globals.py b/dashboard_components/globals.py
new file mode 100644
index 0000000..fcad7aa
--- /dev/null
+++ b/dashboard_components/globals.py
@@ -0,0 +1,136 @@
+import os
+from genericpath import isdir, isfile
+from os import listdir
+from os.path import join
+from utils import Enum
+from bokeh.models import Div
+from bokeh.plotting import curdoc
+import wx
+import colorsys
+
+patches = {}
+signals_files = {}
+selected_file = None
+x_axis = ['Episode #']
+x_axis_options = ['Episode #', 'Total steps', 'Wall-Clock Time']
+x_axis_labels = ['Episode #', 'Total steps', 'Wall-Clock Time (minutes)']
+current_color = 0
+
+# spinner
+root_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
+with open(os.path.join(root_dir, 'spinner.css'), 'r') as f:
+ spinner_style = """""".format(f.read())
+ spinner_html = """"""
+spinner = Div(text="""""")
+displayed_doc = "landing_page"
+layouts = {}
+
+
+def generate_color_range(N, I):
+ HSV_tuples = [(x*1.0/N, 0.5, I) for x in range(N)]
+ RGB_tuples = map(lambda x: colorsys.hsv_to_rgb(*x), HSV_tuples)
+ for_conversion = []
+ for RGB_tuple in RGB_tuples:
+ for_conversion.append((int(RGB_tuple[0]*255), int(RGB_tuple[1]*255), int(RGB_tuple[2]*255)))
+ hex_colors = [rgb_to_hex(RGB_tuple) for RGB_tuple in for_conversion]
+ return hex_colors, for_conversion
+
+
+# convert RGB tuple to hexadecimal code
+def rgb_to_hex(rgb):
+ return '#%02x%02x%02x' % rgb
+
+
+# convert hexadecimal to RGB tuple
+def hex_to_dec(hex):
+ red = ''.join(hex.strip('#')[0:2])
+ green = ''.join(hex.strip('#')[2:4])
+ blue = ''.join(hex.strip('#')[4:6])
+ return int(red, 16), int(green, 16), int(blue,16)
+
+
+color_resolution = 1000
+brightness = 0.75 # change to have brighter/darker colors
+crx = list(range(1, color_resolution+1)) # the resolution is 1000 colors
+cry = [5 for i in range(len(crx))]
+crcolor, crRGBs = generate_color_range(color_resolution, brightness) # produce spectrum
+
+
+def display_boards():
+ global displayed_doc
+ if displayed_doc == "landing_page":
+ doc.remove_root(doc.roots[0])
+ doc.add_root(layouts["boards"])
+ displayed_doc = "boards"
+
+
+def show_spinner(text="Loading..."):
+ spinner.text = spinner_style + spinner_html.format(text)
+
+
+def hide_spinner():
+ spinner.text = ""
+
+
+# takes path to dir and recursively adds all it's files to paths
+def add_directory_csv_files(dir_path, paths=None):
+ if not paths:
+ paths = []
+
+ for p in listdir(dir_path):
+ path = join(dir_path, p)
+ if isdir(path):
+ # call recursively for each dir
+ paths = add_directory_csv_files(path, paths)
+ elif isfile(path) and path.endswith('.csv'):
+ # add every file to the list
+ paths.append(path)
+
+ return paths
+
+
+class DialogApp(wx.App):
+ def getFileDialog(self):
+ with wx.FileDialog(None, "Open CSV file", wildcard="CSV files (*.csv)|*.csv",
+ style=wx.FD_OPEN | wx.FD_FILE_MUST_EXIST | wx.FD_CHANGE_DIR | wx.FD_MULTIPLE) as fileDialog:
+ if fileDialog.ShowModal() == wx.ID_CANCEL:
+ return None # the user changed their mind
+ else:
+ # Proceed loading the file chosen by the user
+ return fileDialog.GetPaths()
+
+ def getDirDialog(self):
+ with wx.DirDialog (None, "Choose input directory", "",
+ style=wx.FD_OPEN | wx.FD_FILE_MUST_EXIST | wx.FD_CHANGE_DIR) as dirDialog:
+ if dirDialog.ShowModal() == wx.ID_CANCEL:
+ return None # the user changed their mind
+ else:
+ # Proceed loading the dir chosen by the user
+ return dirDialog.GetPath()
+
+
+class RunType(Enum):
+ SINGLE_FOLDER_SINGLE_FILE = 1
+ SINGLE_FOLDER_MULTIPLE_FILES = 2
+ MULTIPLE_FOLDERS_SINGLE_FILES = 3
+ MULTIPLE_FOLDERS_MULTIPLE_FILES = 4
+ UNKNOWN = 0
+
+
+class FolderType(Enum):
+ SINGLE_FILE = 1
+ MULTIPLE_FILES = 2
+ MULTIPLE_FOLDERS = 3
+ EMPTY = 4
+
+
+dialog = DialogApp()
+
+doc = curdoc()
diff --git a/dashboard_components/landing_page.py b/dashboard_components/landing_page.py
new file mode 100644
index 0000000..12c51f1
--- /dev/null
+++ b/dashboard_components/landing_page.py
@@ -0,0 +1,21 @@
+from bokeh.layouts import row, column
+from bokeh.models.widgets import Div
+from dashboard_components.experiment_board import file_selection_button, group_selection_button
+from dashboard_components.globals import layouts
+
+# title
+title = Div(text="""Coach Dashboard
""")
+
+# landing page
+landing_page_description = Div(text="""Start by selecting an experiment file or directory to open:
""")
+center = Div(text="""""")
+center_buttons = Div(text="""""", width=0)
+landing_page = column(center,
+ title,
+ landing_page_description,
+ row(center_buttons),
+ row(file_selection_button, sizing_mode='scale_width'),
+ row(group_selection_button, sizing_mode='scale_width'),
+ sizing_mode='scale_width')
+
+layouts['landing_page'] = landing_page
diff --git a/dashboard_components/signals.py b/dashboard_components/signals.py
new file mode 100644
index 0000000..19f10b9
--- /dev/null
+++ b/dashboard_components/signals.py
@@ -0,0 +1,123 @@
+import random
+
+import numpy as np
+from bokeh.models import ColumnDataSource
+from bokeh.palettes import Dark2
+from dashboard_components.globals import show_spinner, hide_spinner, current_color
+from utils import squeeze_list
+
+
+class Signal:
+ def __init__(self, name, parent, plot):
+ self.name = name
+ self.full_name = "{}/{}".format(parent.filename, self.name)
+ self.plot = plot
+ self.selected = False
+ self.color = random.choice(Dark2[8])
+ self.line = None
+ self.bands = None
+ self.bokeh_source = parent.bokeh_source
+ self.min_val = 0
+ self.max_val = 0
+ self.axis = 'default'
+ self.sub_signals = []
+ for name in self.bokeh_source.data.keys():
+ if (len(name.split('/')) == 1 and name == self.name) or '/'.join(name.split('/')[:-1]) == self.name:
+ self.sub_signals.append(name)
+ if len(self.sub_signals) > 1:
+ self.mean_signal = squeeze_list([name for name in self.sub_signals if 'Mean' in name.split('/')[-1]])
+ self.stdev_signal = squeeze_list([name for name in self.sub_signals if 'Stdev' in name.split('/')[-1]])
+ self.min_signal = squeeze_list([name for name in self.sub_signals if 'Min' in name.split('/')[-1]])
+ self.max_signal = squeeze_list([name for name in self.sub_signals if 'Max' in name.split('/')[-1]])
+ else:
+ self.mean_signal = squeeze_list(self.name)
+ self.stdev_signal = None
+ self.min_signal = None
+ self.max_signal = None
+ self.has_bollinger_bands = False
+ if self.mean_signal and self.stdev_signal and self.min_signal and self.max_signal:
+ self.has_bollinger_bands = True
+ self.show_bollinger_bands = False
+ self.bollinger_bands_source = None
+ self.update_range()
+
+ def set_color(self, color):
+ self.color = color
+ if self.line:
+ self.line.glyph.line_color = color
+ if self.bands:
+ self.bands.glyph.fill_color = color
+
+ def plot_line(self):
+ global current_color
+ self.set_color(Dark2[8][current_color])
+ current_color = (current_color + 1) % len(Dark2[8])
+ if self.has_bollinger_bands:
+ self.set_bands_source()
+ self.create_bands()
+ self.line = self.plot.line('index', self.mean_signal, source=self.bokeh_source,
+ line_color=self.color, line_width=2)
+ self.line.visible = True
+
+ def set_selected(self, val):
+ if self.selected != val:
+ self.selected = val
+ if self.line:
+ # self.set_color(Dark2[8][current_color])
+ # current_color = (current_color + 1) % len(Dark2[8])
+ self.line.visible = self.selected
+ if self.bands:
+ self.bands.visible = self.selected and self.show_bollinger_bands
+ elif self.selected:
+ # lazy plotting - plot only when selected for the first time
+ self.plot_line()
+
+ def set_dash(self, dash):
+ self.line.glyph.line_dash = dash
+
+ def create_bands(self):
+ self.bands = self.plot.patch(x='band_x', y='band_y', source=self.bollinger_bands_source,
+ color=self.color, fill_alpha=0.4, alpha=0.1, line_width=0)
+ self.bands.visible = self.show_bollinger_bands
+ # self.min_line = plot.line('index', self.min_signal, source=self.bokeh_source,
+ # line_color=self.color, line_width=3, line_dash="4 4")
+ # self.max_line = plot.line('index', self.max_signal, source=self.bokeh_source,
+ # line_color=self.color, line_width=3, line_dash="4 4")
+ # self.min_line.visible = self.show_bollinger_bands
+ # self.max_line.visible = self.show_bollinger_bands
+
+ def set_bands_source(self):
+ x_ticks = self.bokeh_source.data['index']
+ mean_values = self.bokeh_source.data[self.mean_signal]
+ stdev_values = self.bokeh_source.data[self.stdev_signal]
+ band_x = np.append(x_ticks, x_ticks[::-1])
+ band_y = np.append(mean_values - stdev_values, mean_values[::-1] + stdev_values[::-1])
+ source_data = {'band_x': band_x, 'band_y': band_y}
+ if self.bollinger_bands_source:
+ self.bollinger_bands_source.data = source_data
+ else:
+ self.bollinger_bands_source = ColumnDataSource(source_data)
+
+ def change_bollinger_bands_state(self, new_state):
+ self.show_bollinger_bands = new_state
+ if self.bands and self.selected:
+ self.bands.visible = new_state
+ # self.min_line.visible = new_state
+ # self.max_line.visible = new_state
+
+ def update_range(self):
+ self.min_val = np.min(self.bokeh_source.data[self.mean_signal])
+ self.max_val = np.max(self.bokeh_source.data[self.mean_signal])
+
+ def set_axis(self, axis):
+ self.axis = axis
+ if not self.line:
+ self.plot_line()
+ self.line.visible = False
+ self.line.y_range_name = axis
+
+ def toggle_axis(self):
+ if self.axis == 'default':
+ self.set_axis('secondary')
+ else:
+ self.set_axis('default')
diff --git a/dashboard_components/signals_file.py b/dashboard_components/signals_file.py
new file mode 100644
index 0000000..1e89e18
--- /dev/null
+++ b/dashboard_components/signals_file.py
@@ -0,0 +1,39 @@
+import os
+
+import pandas as pd
+from pandas.errors import EmptyDataError
+
+from dashboard_components.signals_file_base import SignalsFileBase
+from utils import break_file_path
+
+
+class SignalsFile(SignalsFileBase):
+ def __init__(self, csv_path, load=True, plot=None):
+ super().__init__(plot)
+ self.full_csv_path = csv_path
+ self.dir, self.filename, _ = break_file_path(csv_path)
+ if load:
+ self.load()
+ # this helps set the correct x axis
+ self.change_averaging_window(1, force=True)
+
+ def load_csv(self):
+ # load csv and fix sparse data.
+ # csv can be in the middle of being written so we use try - except
+ self.csv = None
+ while self.csv is None:
+ try:
+ self.csv = pd.read_csv(self.full_csv_path)
+ break
+ except EmptyDataError:
+ self.csv = None
+ continue
+ self.csv = self.csv.interpolate()
+ self.csv.fillna(value=0, inplace=True)
+
+ self.csv['Wall-Clock Time'] /= 60.
+
+ self.last_modified = os.path.getmtime(self.full_csv_path)
+
+ def file_was_modified_on_disk(self):
+ return self.last_modified != os.path.getmtime(self.full_csv_path)
\ No newline at end of file
diff --git a/dashboard_components/signals_file_base.py b/dashboard_components/signals_file_base.py
new file mode 100644
index 0000000..1d5628a
--- /dev/null
+++ b/dashboard_components/signals_file_base.py
@@ -0,0 +1,132 @@
+import numpy
+from bokeh.models import ColumnDataSource
+from bokeh.palettes import Dark2
+from dashboard_components.globals import x_axis, x_axis_options, show_spinner
+from dashboard_components.signals import Signal
+import numpy as np
+import copy
+
+
+class SignalsFileBase:
+ def __init__(self, plot):
+ self.plot = plot
+ self.full_csv_path = ""
+ self.dir = ""
+ self.filename = ""
+ self.signals_averaging_window = 1
+ self.show_bollinger_bands = False
+ self.csv = None
+ self.bokeh_source = None
+ self.bokeh_source_orig = None
+ self.last_modified = None
+ self.signals = {}
+ self.separate_files = False
+ self.last_reload_data_fix = False
+
+ def load_csv(self):
+ pass
+
+ def update_x_axis_index(self):
+ global x_axis
+ self.bokeh_source_orig.data['index'] = self.bokeh_source_orig.data[x_axis[0]]
+ self.bokeh_source.data['index'] = self.bokeh_source.data[x_axis[0]]
+
+ def toggle_y_axis(self, signal_name=None):
+ if signal_name:
+ self.signals[signal_name].toggle_axis()
+ else:
+ for signal in self.signals.values():
+ if signal.selected:
+ signal.toggle_axis()
+
+ def update_source_and_signals(self):
+ # create bokeh data sources
+ self.bokeh_source_orig = ColumnDataSource(self.csv)
+ self.bokeh_source_orig.data['index'] = self.bokeh_source_orig.data[x_axis[0]]
+
+ if self.bokeh_source is None:
+ self.bokeh_source = ColumnDataSource(self.csv)
+ else:
+ # smooth the data if necessary
+ self.change_averaging_window(self.signals_averaging_window, force=True)
+
+ self.update_x_axis_index()
+
+ # create all the signals
+ if len(self.signals.keys()) == 0:
+ self.signals = {}
+ unique_signal_names = []
+ for name in self.csv.columns:
+ if len(name.split('/')) == 1:
+ unique_signal_names.append(name)
+ else:
+ unique_signal_names.append('/'.join(name.split('/')[:-1]))
+ unique_signal_names = list(set(unique_signal_names))
+ for signal_name in unique_signal_names:
+ self.signals[signal_name] = Signal(signal_name, self, self.plot)
+
+ def load(self):
+ self.load_csv()
+ self.update_source_and_signals()
+
+ def reload_data(self):
+ # this function is a workaround to reload the data of all the signals
+ # if the data doesn't change, bokeh does not refreshes the line
+ temp_data = self.bokeh_source.data.copy()
+ for col in self.bokeh_source.data.keys():
+ if not self.last_reload_data_fix:
+ temp_data[col] = temp_data[col][:-1]
+ self.last_reload_data_fix = not self.last_reload_data_fix
+ self.bokeh_source.data = temp_data
+
+ def change_averaging_window(self, new_size, force=False, signals=None):
+ if force or self.signals_averaging_window != new_size:
+ self.signals_averaging_window = new_size
+ win = np.ones(new_size) / new_size
+ temp_data = self.bokeh_source_orig.data.copy()
+ for col in self.bokeh_source.data.keys():
+ if col == 'index' or col in x_axis_options \
+ or (signals and not any(col in signal for signal in signals)):
+ temp_data[col] = temp_data[col][:-new_size]
+ continue
+ temp_data[col] = np.convolve(self.bokeh_source_orig.data[col], win, mode='same')[:-new_size]
+ self.bokeh_source.data = temp_data
+
+ # smooth bollinger bands
+ for signal in self.signals.values():
+ if signal.has_bollinger_bands:
+ signal.set_bands_source()
+
+ def hide_all_signals(self):
+ for signal_name in self.signals.keys():
+ self.set_signal_selection(signal_name, False)
+
+ def set_signal_selection(self, signal_name, val):
+ self.signals[signal_name].set_selected(val)
+
+ def change_bollinger_bands_state(self, new_state):
+ self.show_bollinger_bands = new_state
+ for signal in self.signals.values():
+ signal.change_bollinger_bands_state(new_state)
+
+ def file_was_modified_on_disk(self):
+ pass
+
+ def get_range_of_selected_signals_on_axis(self, axis, selected_signal=None):
+ max_val = -float('inf')
+ min_val = float('inf')
+ for signal in self.signals.values():
+ if (selected_signal and signal.name == selected_signal) or (signal.selected and signal.axis == axis):
+ max_val = max(max_val, signal.max_val)
+ min_val = min(min_val, signal.min_val)
+ return min_val, max_val
+
+ def get_selected_signals(self):
+ signals = []
+ for signal in self.signals.values():
+ if signal.selected:
+ signals.append(signal)
+ return signals
+
+ def show_files_separately(self, val):
+ pass
\ No newline at end of file
diff --git a/dashboard_components/signals_files_group.py b/dashboard_components/signals_files_group.py
new file mode 100644
index 0000000..579df75
--- /dev/null
+++ b/dashboard_components/signals_files_group.py
@@ -0,0 +1,149 @@
+import os
+from os.path import basename
+
+import pandas as pd
+
+from dashboard_components.globals import x_axis_options, add_directory_csv_files, show_spinner
+from dashboard_components.signals_file import SignalsFile
+from dashboard_components.signals_file_base import SignalsFileBase
+
+
+class SignalsFilesGroup(SignalsFileBase):
+ def __init__(self, csv_paths, plot=None):
+ super().__init__(plot)
+ self.full_csv_paths = csv_paths
+ self.signals_files = []
+ if len(csv_paths) == 1 and os.path.isdir(csv_paths[0]):
+ self.signals_files = [SignalsFile(str(file), load=False, plot=plot) for file in add_directory_csv_files(csv_paths[0])]
+ else:
+ for csv_path in csv_paths:
+ if os.path.isdir(csv_path):
+ self.signals_files.append(SignalsFilesGroup(add_directory_csv_files(csv_path), plot=plot))
+ else:
+ self.signals_files.append(SignalsFile(str(csv_path), load=False, plot=plot))
+ parent_directory_path = os.path.abspath(os.path.join(os.path.dirname(csv_paths[0]), '..'))
+
+ if len(csv_paths) == 1 and len(os.listdir(parent_directory_path)) == 1:
+ # get the parent directory name (since the current directory is the timestamp directory)
+ self.dir = os.path.abspath(os.path.join(os.path.dirname(csv_paths[0]), '..'))
+ else:
+ # get the common directory for all the experiments
+ self.dir = os.path.dirname(os.path.commonprefix(csv_paths))
+
+ self.filename = '{} - Group({})'.format(basename(self.dir), len(self.signals_files))
+
+ self.load()
+
+ def load_csv(self):
+ corrupted_files_idx = []
+ for idx, signal_file in enumerate(self.signals_files):
+ signal_file.load_csv()
+ if not all(option in signal_file.csv.keys() for option in x_axis_options):
+ print("Warning: {} file seems to be corrupted and does contain the necessary columns "
+ "and will not be rendered".format(signal_file.filename))
+ corrupted_files_idx.append(idx)
+
+ for file_idx in corrupted_files_idx:
+ del self.signals_files[file_idx]
+
+ # get the stats of all the columns
+ if len(self.signals_files) > 1:
+ csv_group = pd.concat([signals_file.csv for signals_file in self.signals_files])
+ columns_to_remove = [s for s in csv_group.columns if '/Stdev' in s] + \
+ [s for s in csv_group.columns if '/Min' in s] + \
+ [s for s in csv_group.columns if '/Max' in s]
+ for col in columns_to_remove:
+ del csv_group[col]
+ csv_group = csv_group.groupby(csv_group.index)
+ self.csv_mean = csv_group.mean()
+ self.csv_mean.columns = [s + '/Mean' for s in self.csv_mean.columns]
+ self.csv_stdev = csv_group.std()
+ self.csv_stdev.columns = [s + '/Stdev' for s in self.csv_stdev.columns]
+ self.csv_min = csv_group.min()
+ self.csv_min.columns = [s + '/Min' for s in self.csv_min.columns]
+ self.csv_max = csv_group.max()
+ self.csv_max.columns = [s + '/Max' for s in self.csv_max.columns]
+
+ # get the indices from the file with the least number of indices and which is not an evaluation worker
+ file_with_min_indices = self.signals_files[0]
+ for signals_file in self.signals_files:
+ if signals_file.csv.shape[0] < file_with_min_indices.csv.shape[0] and \
+ 'Training reward' in signals_file.csv.keys():
+ file_with_min_indices = signals_file
+ self.index_columns = file_with_min_indices.csv[x_axis_options]
+
+ # concat the stats and the indices columns
+ num_rows = file_with_min_indices.csv.shape[0]
+ self.csv = pd.concat([self.index_columns, self.csv_mean.head(num_rows), self.csv_stdev.head(num_rows),
+ self.csv_min.head(num_rows), self.csv_max.head(num_rows)], axis=1)
+
+ # remove the stat columns for the indices columns
+ columns_to_remove = [s + '/Mean' for s in x_axis_options] + \
+ [s + '/Stdev' for s in x_axis_options] + \
+ [s + '/Min' for s in x_axis_options] + \
+ [s + '/Max' for s in x_axis_options]
+ for col in columns_to_remove:
+ del self.csv[col]
+ else: # This is a group of a single file
+ self.csv = self.signals_files[0].csv
+
+ # # convert wall clock time to minutes - isn't needed because the sub-signals are already scaled
+ # self.csv['Wall-Clock Time'] /= 60.
+
+ # remove NaNs
+ self.csv.fillna(value=0, inplace=True) # removing this line will make bollinger bands fail
+ for key in self.csv.keys():
+ if 'Stdev' in key and 'Evaluation' not in key:
+ self.csv[key] = self.csv[key].fillna(value=0)
+
+ for signal_file in self.signals_files:
+ signal_file.update_source_and_signals()
+
+ def reload_data(self):
+ for signal_file in self.signals_files:
+ signal_file.reload_data()
+ SignalsFileBase.reload_data(self)
+
+ def update_x_axis_index(self):
+ for signal_file in self.signals_files:
+ signal_file.update_x_axis_index()
+ SignalsFileBase.update_x_axis_index(self)
+
+ def toggle_y_axis(self, signal_name=None):
+ for signal in self.signals.values():
+ if signal.selected:
+ signal.toggle_axis()
+ for signal_file in self.signals_files:
+ signal_file.toggle_y_axis(signal.name)
+
+ def change_averaging_window(self, new_size, force=False, signals=None):
+ for signal_file in self.signals_files:
+ signal_file.change_averaging_window(new_size, force, signals)
+ SignalsFileBase.change_averaging_window(self, new_size, force, signals)
+
+ def set_signal_selection(self, signal_name, val):
+ self.show_files_separately(self.separate_files)
+ SignalsFileBase.set_signal_selection(self, signal_name, val)
+
+ def file_was_modified_on_disk(self):
+ for signal_file in self.signals_files:
+ if signal_file.file_was_modified_on_disk():
+ return True
+ return False
+
+ def show_files_separately(self, val):
+ self.separate_files = val
+ for signal in self.signals.values():
+ if signal.selected:
+ if val:
+ signal.set_dash("4 4")
+ else:
+ signal.set_dash("")
+ for signal_file in self.signals_files:
+ try:
+ if val:
+ signal_file.set_signal_selection(signal.name, signal.selected)
+ else:
+ signal_file.set_signal_selection(signal.name, False)
+ except:
+ pass
\ No newline at end of file