From 6ac6df83a7259fbcdaa157d9ceedd4a13558e4d2 Mon Sep 17 00:00:00 2001 From: Dimitar Tasev <dimtasev@gmail.com> Date: Mon, 26 Nov 2018 15:51:24 +0000 Subject: [PATCH] Statistics and plotting, re #24007 --- .../algorithms/StatisticsOfTableWorkspace.py | 90 +++++++----- .../widgets/tableworkspacedisplay/__main__.py | 7 +- .../widgets/tableworkspacedisplay/model.py | 37 +---- .../tableworkspacedisplay/plot_type.py | 7 + .../tableworkspacedisplay/presenter.py | 134 ++++++++++++++---- .../widgets/tableworkspacedisplay/view.py | 74 +++++++--- 6 files changed, 239 insertions(+), 110 deletions(-) create mode 100644 qt/python/mantidqt/widgets/tableworkspacedisplay/plot_type.py diff --git a/Framework/PythonInterface/plugins/algorithms/StatisticsOfTableWorkspace.py b/Framework/PythonInterface/plugins/algorithms/StatisticsOfTableWorkspace.py index f148fc24d9f..35c343bf342 100644 --- a/Framework/PythonInterface/plugins/algorithms/StatisticsOfTableWorkspace.py +++ b/Framework/PythonInterface/plugins/algorithms/StatisticsOfTableWorkspace.py @@ -4,30 +4,33 @@ # NScD Oak Ridge National Laboratory, European Spallation Source # & Institut Laue - Langevin # SPDX - License - Identifier: GPL - 3.0 + -#pylint: disable=no-init +# pylint: disable=no-init from __future__ import (absolute_import, division, print_function) -from mantid.api import PythonAlgorithm, AlgorithmFactory, ITableWorkspaceProperty -from mantid.kernel import Direction, Stats -import mantid.simpleapi as ms -from mantid import mtd, logger -import numpy as np + import collections + +import numpy as np from six import iteritems +import mantid.simpleapi as ms +from mantid import logger, mtd +from mantid.api import AlgorithmFactory, ITableWorkspaceProperty, PythonAlgorithm +from mantid.kernel import Direction, IntArrayBoundedValidator, IntArrayProperty, Stats + def _stats_to_dict(stats): """ - Converts a Statstics object to an ordered dictionary. + Converts a Statistics object to an ordered dictionary. @param stats Statistics object to convertToWaterfall @return Dictionary of statistics """ stat_dict = collections.OrderedDict() - stat_dict['standard_deviation'] = stats.standard_deviation - stat_dict['maximum'] = stats.maximum - stat_dict['minimum'] = stats.minimum - stat_dict['mean'] = stats.mean - stat_dict['median'] = stats.median + stat_dict['StandardDev'] = stats.standard_deviation + stat_dict['Maximum'] = stats.maximum + stat_dict['Minimum'] = stats.minimum + stat_dict['Mean'] = stats.mean + stat_dict['Median'] = stats.median return stat_dict @@ -42,38 +45,61 @@ class StatisticsOfTableWorkspace(PythonAlgorithm): def PyInit(self): self.declareProperty(ITableWorkspaceProperty('InputWorkspace', '', Direction.Input), doc='Input table workspace.') + validator = IntArrayBoundedValidator() + validator.setLower(0) + self.declareProperty( + IntArrayProperty('ColumnIndices', values=[], direction=Direction.Input, validator=validator), + 'Comma separated list of column indices for which statistics will be separated') self.declareProperty(ITableWorkspaceProperty('OutputWorkspace', '', Direction.Output), - doc='Output workspace contatining column statitics.') + doc='Output workspace containing column statistics.') def PyExec(self): in_ws = mtd[self.getPropertyValue('InputWorkspace')] + indices_list = self.getPropertyValue('ColumnIndices') out_ws_name = self.getPropertyValue('OutputWorkspace') + column_names = in_ws.getColumnNames() + + # If column indices are not provided, then default to _ALL_ columns + if len(indices_list) > 0: + indices_list = [int(x) for x in indices_list.split(',')] + else: + indices_list = range(len(column_names)) out_ws = ms.CreateEmptyTableWorkspace(OutputWorkspace=out_ws_name) - out_ws.addColumn('str', 'statistic') + out_ws.addColumn('str', 'Statistic') stats = collections.OrderedDict([ - ('standard_deviation', collections.OrderedDict()), - ('minimum', collections.OrderedDict()), - ('median', collections.OrderedDict()), - ('maximum', collections.OrderedDict()), - ('mean', collections.OrderedDict()), + ('StandardDev', collections.OrderedDict()), + ('Minimum', collections.OrderedDict()), + ('Median', collections.OrderedDict()), + ('Maximum', collections.OrderedDict()), + ('Mean', collections.OrderedDict()), ]) - - for name in in_ws.getColumnNames(): + # PYTHON_ROOT = "c:/users/qbr77747/apps/miniconda3" + # import os + # import sys + # sys.path.append(os.path.join(PYTHON_ROOT, "lib/site-packages")) + # import pydevd + # pydevd.settrace('localhost', port=44444, stdoutToServer=True, stderrToServer=True) + + for index in indices_list: + column_name = column_names[index] try: - col_stats = _stats_to_dict(Stats.getStatistics(np.array([float(v) for v in in_ws.column(name)]))) - for statname in stats: - stats[statname][name] = col_stats[statname] - out_ws.addColumn('float', name) - except ValueError: - logger.notice('Column \'%s\' is not numerical, skipping' % name) - - for name, stat in iteritems(stats): - stat1 = collections.OrderedDict(stat) - stat1['statistic'] = name - out_ws.addRow(stat1) + column_data = np.array([float(v) for v in in_ws.column(index)]) + col_stats = _stats_to_dict(Stats.getStatistics(column_data)) + for stat_name in stats: + stats[stat_name][column_name] = col_stats[stat_name] + out_ws.addColumn('float', column_name) + except RuntimeError: + logger.notice('Column \'%s\' is not numerical, skipping' % column_name) + except: + logger.notice('Column \'%s\' is not numerical, skipping' % column_name) + + for index, stat_name in iteritems(stats): + stat = collections.OrderedDict(stat_name) + stat['Statistic'] = index + out_ws.addRow(stat) self.setProperty('OutputWorkspace', out_ws) diff --git a/qt/python/mantidqt/widgets/tableworkspacedisplay/__main__.py b/qt/python/mantidqt/widgets/tableworkspacedisplay/__main__.py index e42ed294991..f17a54bf05b 100644 --- a/qt/python/mantidqt/widgets/tableworkspacedisplay/__main__.py +++ b/qt/python/mantidqt/widgets/tableworkspacedisplay/__main__.py @@ -18,11 +18,12 @@ from qtpy.QtWidgets import QApplication # noqa: F402 from mantid.simpleapi import Load # noqa: F402 from mantidqt.widgets.tableworkspacedisplay.presenter import TableWorkspaceDisplay # noqa: F402 -from workbench.plotting.functions import plot # noqa: F402 +# from workbench.plotting.functions import plot # noqa: F402 +import matplotlib.pyplot as plt app = QApplication([]) # DEEE_WS_MON = Load("SavedTableWorkspace.nxs") # DEEE_WS_MON = Load("TOPAZ_3007.peaks.nxs") -DEEE_WS_MON = Load("SmallPeakWS10.nxs") -window = TableWorkspaceDisplay(DEEE_WS_MON, plot) +DEEE_WS_MON = Load("SmallPeakWS10_vals.nxs") +window = TableWorkspaceDisplay(DEEE_WS_MON, plt) app.exec_() diff --git a/qt/python/mantidqt/widgets/tableworkspacedisplay/model.py b/qt/python/mantidqt/widgets/tableworkspacedisplay/model.py index 3414cb726b5..68cbe49331b 100644 --- a/qt/python/mantidqt/widgets/tableworkspacedisplay/model.py +++ b/qt/python/mantidqt/widgets/tableworkspacedisplay/model.py @@ -21,6 +21,9 @@ class ITableDisplayModel(object): def get_column_headers(self): raise NotImplementedError("This is an interface and should be implemented.") + def get_column_header(self, index): + raise NotImplementedError("This is an interface and should be implemented.") + def get_column(self, index): raise NotImplementedError("This is an interface and should be implemented.") @@ -44,8 +47,6 @@ class TableWorkspaceDisplayModel(ITableDisplayModel): SPECTRUM_PLOT_LEGEND_STRING = '{}-{}' BIN_PLOT_LEGEND_STRING = '{}-bin-{}' - # PEAKS_WORKSPACE_EDITABLE_COLUMNS = ["RunNumber", "h", "k", "l"] - def __init__(self, ws): if not isinstance(ws, TableWorkspace) and not isinstance(ws, PeaksWorkspace): raise ValueError("The workspace type is not supported: {0}".format(type(ws))) @@ -82,33 +83,5 @@ class TableWorkspaceDisplayModel(ITableDisplayModel): def get_number_of_columns(self): return self.ws_num_cols -# class PeaksWorkspaceDisplayModel(TableWorkspaceDisplayModel): -# def __init__(self, ws): -# super(PeaksWorkspaceDisplayModel, self).__init__(ws) -# self.sigma_col_index = None -# -# def get_column_headers(self): -# column_names = self.ws.getColumnNames() -# self.sigma_col_index = column_names.index("SigInt") + 1 -# # insert the intensity/sigma after the sigma column -# column_names.insert(self.sigma_col_index, "I/σ") -# # update the number of columns -# self.ws_num_cols = len(column_names) -# return column_names -# -# def get_column(self, index): -# """ -# Get data for a column from the PeaksWorkspace. -# -# Handles the index for the additional column Intensity/Sigma column correctly, -# as the column itself is only added -# :param index: -# :return: -# """ -# if index < self.sigma_col_index: -# return self.ws.column(index) -# elif index > self.sigma_col_index: -# return self.ws.column(index - 1) -# else: -# num_rows = self.get_number_of_rows() -# return [self.ws.getPeak(i).getIntensityOverSigma() for i in range(num_rows)] + def get_column_header(self, index): + return self.get_column_headers()[index] diff --git a/qt/python/mantidqt/widgets/tableworkspacedisplay/plot_type.py b/qt/python/mantidqt/widgets/tableworkspacedisplay/plot_type.py new file mode 100644 index 00000000000..9388850c7a0 --- /dev/null +++ b/qt/python/mantidqt/widgets/tableworkspacedisplay/plot_type.py @@ -0,0 +1,7 @@ +from mantid.py3compat import Enum + + +class PlotType(Enum): + LINEAR = 1 + SCATTER = 2 + LINE_AND_SYMBOL = 3 diff --git a/qt/python/mantidqt/widgets/tableworkspacedisplay/presenter.py b/qt/python/mantidqt/widgets/tableworkspacedisplay/presenter.py index d548cb95a30..2408dc31b7c 100644 --- a/qt/python/mantidqt/widgets/tableworkspacedisplay/presenter.py +++ b/qt/python/mantidqt/widgets/tableworkspacedisplay/presenter.py @@ -9,31 +9,55 @@ # from __future__ import absolute_import, division, print_function +from functools import partial + from qtpy.QtWidgets import QTableWidgetItem -from mantid.simpleapi import DeleteTableRows -from mantidqt.widgets.common.table_copying import copy_cells, show_no_selection_to_copy_toast +from mantid.simpleapi import DeleteTableRows, StatisticsOfTableWorkspace +from mantidqt.widgets.common.table_copying import copy_cells, show_mouse_toast, show_no_selection_to_copy_toast +from mantidqt.widgets.tableworkspacedisplay.plot_type import PlotType from .model import TableWorkspaceDisplayModel from .view import TableWorkspaceDisplayView class TableWorkspaceDisplay(object): A_LOT_OF_THINGS_TO_PLOT_MESSAGE = "You selected {} spectra to plot. Are you sure you want to plot that many?" + TOO_MANY_SELECTED_FOR_X = "Too many columns are selected to set as X. Please select only 1." + TOO_MANY_SELECTED_FOR_PLOT = "Too many columns are selected to plot. Please select only 1." NUM_SELECTED_FOR_CONFIRMATION = 10 + NO_COLUMN_MARKED_AS_X = "No column marked as X." - def __init__(self, ws, plot=None, parent=None, model=None, view=None): + def __init__(self, ws, plot=None, parent=None, model=None, view=None, name=None): # Create model and view, or accept mocked versions self.model = model if model else TableWorkspaceDisplayModel(ws) - self.view = view if view else TableWorkspaceDisplayView(self, parent, self.model.get_name()) + self.name = self.model.get_name() if name is None else name + self.view = view if view else TableWorkspaceDisplayView(self, parent, self.name) + self.parent = parent self.plot = plot self.view.set_context_menu_actions(self.view) - column_headers = self.model.get_column_headers() - # self.editable_columns = self.model.get_editable_columns(column_headers) - self.view.setColumnCount(len(column_headers)) - self.view.setHorizontalHeaderLabels(column_headers) + self.update_column_headers() # self.view.setHorizontalHeaderLabels(["{}[Y]".format(x) for x in column_headers]) self.load_data(self.view) + self.column_marked_as_x = None + + def update_column_headers(self, extra_labels=None): + """ + :param extra_labels: Extra labels to be appended to the column headers. + Expected format: [(id, label), (2, "X"),...] + :type extra_labels: List[Tuple[int, str]] + :return: + """ + column_headers = self.model.get_column_headers() + num_headers = len(column_headers) + self.view.setColumnCount(num_headers) + + if extra_labels: + for index, label in extra_labels: + column_headers[index] += label + + self.view.setHorizontalHeaderLabels(column_headers) + def load_data(self, table): num_rows = self.model.get_number_of_rows() table.setRowCount(num_rows) @@ -41,26 +65,21 @@ class TableWorkspaceDisplay(object): num_cols = self.model.get_number_of_columns() for col in range(num_cols): column_data = self.model.get_column(col) - # editable = False - # if peaks_workspace and col in self.editable_columns: - # editable = True for row in range(num_rows): item = QTableWidgetItem(str(column_data[row])) - # if not editable: - # item.setFlags(item.flags() & ~Qt.ItemIsEditable) table.setItem(row, col, item) - def action_copy_cells(self, table): - copy_cells(table) + def action_copy_cells(self): + copy_cells(self.view) - def action_copy_bin_values(self, table): - copy_cells(table) + def action_copy_bin_values(self): + copy_cells(self.view) - def action_copy_spectrum_values(self, table): - copy_cells(table) + def action_copy_spectrum_values(self): + copy_cells(self.view) - def action_keypress_copy(self, table): - copy_cells(table) + def action_keypress_copy(self): + copy_cells(self.view) def action_delete_row(self): selection_model = self.view.selectionModel() @@ -78,15 +97,80 @@ class TableWorkspaceDisplay(object): for row in reversed(selected_rows_list): self.view.removeRow(row) - def action_statistics_on_rows(self): + def action_statistics_on_columns(self): selection_model = self.view.selectionModel() if not selection_model.hasSelection(): show_no_selection_to_copy_toast() return - selected_rows = selection_model.selectedRows() - selected_rows_list = [index.row() for index in selected_rows] - num_cols = self.model.get_number_of_columns() + selected_columns = selection_model.selectedColumns() + selected_columns_list = [index.column() for index in selected_columns] + stats = StatisticsOfTableWorkspace(self.model.ws, selected_columns_list) + TableWorkspaceDisplay(stats, parent=self.parent, name="Column Statistics of {}".format(self.name)) + + def action_hide_column(self): + selection_model = self.view.selectionModel() + if not selection_model.hasSelection(): + show_no_selection_to_copy_toast() + return + + selected_columns = selection_model.selectedColumns() + selected_columns_list = [index.column() for index in selected_columns] + for column_index in selected_columns_list: + self.view.hideColumn(column_index) + + def action_show_all_columns(self): + for column_index in range(self.view.columnCount()): + self.view.showColumn(column_index) + + def action_set_as_x(self): + selection_model = self.view.selectionModel() + if not selection_model.hasSelection(): + show_no_selection_to_copy_toast() + return + + selected_columns = selection_model.selectedColumns() + if len(selected_columns) > 1: + show_mouse_toast(self.TOO_MANY_SELECTED_FOR_X) + return + self.column_marked_as_x = selected_columns[0].column() + + self.update_column_headers([(self.column_marked_as_x, "[X]")]) + + def action_plot(self, type): + if self.column_marked_as_x is None: + show_mouse_toast(self.NO_COLUMN_MARKED_AS_X) + return + + selection_model = self.view.selectionModel() + if not selection_model.hasSelection(): + show_no_selection_to_copy_toast() + return + + x = self.model.get_column(self.column_marked_as_x) + fig, ax = self.plot.subplots(subplot_kw={'projection': 'mantid'}) + ax.set_xlabel(self.model.get_column_header(self.column_marked_as_x)) + + if type == PlotType.LINEAR: + plot_func = ax.plot + elif type == PlotType.SCATTER: + plot_func = ax.scatter + elif type == PlotType.LINE_AND_SYMBOL: + plot_func = partial(ax.plot, marker='o') + else: + raise ValueError("Plot Type: {} not currently supported!".format(type)) + + selected_columns = [col_index.column() for col_index in selection_model.selectedColumns()] + for column in selected_columns: + if column == self.column_marked_as_x: + # TODO log that `column` has been skipped as it's the same as X + pass + y = self.model.get_column(column) + column_label = self.model.get_column_header(column) + plot_func(x, y, label='Column {}'.format(column_label)) + ax.set_ylabel(column_label) + ax.legend() + fig.show() # def _do_action_plot(self, table, axis, get_index, plot_errors=False): # if self.plot is None: diff --git a/qt/python/mantidqt/widgets/tableworkspacedisplay/view.py b/qt/python/mantidqt/widgets/tableworkspacedisplay/view.py index d00626bac1a..c9080fb5459 100644 --- a/qt/python/mantidqt/widgets/tableworkspacedisplay/view.py +++ b/qt/python/mantidqt/widgets/tableworkspacedisplay/view.py @@ -14,9 +14,10 @@ from functools import partial from qtpy import QtGui from qtpy.QtCore import Qt from qtpy.QtGui import QKeySequence -from qtpy.QtWidgets import (QAction, QHeaderView, QMessageBox, QTableView, QTableWidget) +from qtpy.QtWidgets import (QAction, QHeaderView, QMenu, QMessageBox, QTableView, QTableWidget) import mantidqt.icons +from mantidqt.widgets.tableworkspacedisplay.plot_type import PlotType class TableWorkspaceDisplayView(QTableWidget): @@ -27,6 +28,7 @@ class TableWorkspaceDisplayView(QTableWidget): self.COPY_ICON = mantidqt.icons.get_icon("fa.files-o") self.DELETE_ROW = mantidqt.icons.get_icon("fa.minus-square-o") self.STATISTICS_ON_ROW = mantidqt.icons.get_icon('fa.fighter-jet') + self.GRAPH_ICON = mantidqt.icons.get_icon('fa.line-chart') # change the default color of the rows - makes them light blue # monitors and masked rows are colored in the table's custom model @@ -64,13 +66,9 @@ class TableWorkspaceDisplayView(QTableWidget): table.addAction(copy_action) horizontalHeader = table.horizontalHeader() - horizontalHeader.setContextMenuPolicy(Qt.ActionsContextMenu) - horizontalHeader.setSectionResizeMode(QHeaderView.Fixed) - - copy_bin_values = QAction(self.COPY_ICON, "Copy", horizontalHeader) - copy_bin_values.triggered.connect(self.presenter.action_copy_bin_values) - - horizontalHeader.addAction(copy_bin_values) + horizontalHeader.setContextMenuPolicy(Qt.CustomContextMenu) + horizontalHeader.customContextMenuRequested.connect(self.custom_context_menu) + # horizontalHeader.setSectionResizeMode(QHeaderView.Fixed) verticalHeader = table.verticalHeader() verticalHeader.setContextMenuPolicy(Qt.ActionsContextMenu) @@ -82,19 +80,59 @@ class TableWorkspaceDisplayView(QTableWidget): delete_row = QAction(self.DELETE_ROW, "Delete Row", verticalHeader) delete_row.triggered.connect(self.presenter.action_delete_row) - statistics_on_rows = QAction(self.STATISTICS_ON_ROW, "Statistics on Rows", verticalHeader) - statistics_on_rows.triggered.connect(self.presenter.action_statistics_on_rows) - - separator1 = QAction(verticalHeader) - separator1.setSeparator(True) - separator2 = QAction(verticalHeader) - separator2.setSeparator(True) + separator2 = self.make_separator(verticalHeader) verticalHeader.addAction(copy_spectrum_values) - verticalHeader.addAction(separator1) - verticalHeader.addAction(delete_row) verticalHeader.addAction(separator2) - verticalHeader.addAction(statistics_on_rows) + verticalHeader.addAction(delete_row) + + def custom_context_menu(self, position): + main_menu = QMenu() + plot = QMenu("Plot...", main_menu) + plot_line = QAction(self.GRAPH_ICON, "Line", plot) + plot_line.triggered.connect(partial(self.presenter.action_plot, PlotType.LINEAR)) + + plot_scatter = QAction(self.GRAPH_ICON, "Scatter", plot) + plot_scatter.triggered.connect(partial(self.presenter.action_plot, PlotType.SCATTER)) + + plot_line_and_points = QAction(self.GRAPH_ICON, "Line + Symbol", plot) + plot_line_and_points.triggered.connect(partial(self.presenter.action_plot, PlotType.LINE_AND_SYMBOL)) + + plot.addAction(plot_line) + plot.addAction(plot_scatter) + plot.addAction(plot_line_and_points) + main_menu.addMenu(plot) + + copy_bin_values = QAction(self.COPY_ICON, "Copy", main_menu) + copy_bin_values.triggered.connect(self.presenter.action_copy_bin_values) + + set_as_x = QAction(self.COPY_ICON, "Set as X", main_menu) + set_as_x.triggered.connect(self.presenter.action_set_as_x) + + statistics_on_columns = QAction(self.STATISTICS_ON_ROW, "Statistics on Columns", main_menu) + statistics_on_columns.triggered.connect(self.presenter.action_statistics_on_columns) + + hide_column = QAction(self.STATISTICS_ON_ROW, "Hide Column", main_menu) + hide_column.triggered.connect(self.presenter.action_hide_column) + + show_all_columns = QAction(self.STATISTICS_ON_ROW, "Show All Columns", main_menu) + show_all_columns.triggered.connect(self.presenter.action_show_all_columns) + + main_menu.addAction(copy_bin_values) + main_menu.addAction(self.make_separator(main_menu)) + main_menu.addAction(set_as_x) + main_menu.addAction(self.make_separator(main_menu)) + main_menu.addAction(statistics_on_columns) + main_menu.addAction(self.make_separator(main_menu)) + main_menu.addAction(hide_column) + main_menu.addAction(show_all_columns) + + main_menu.exec_(self.mapToGlobal(position)) + + def make_separator(self, horizontalHeader): + separator1 = QAction(horizontalHeader) + separator1.setSeparator(True) + return separator1 @staticmethod def copy_to_clipboard(data): -- GitLab