From ade13e0c9e84a28c76a522d435c26c15d2c42572 Mon Sep 17 00:00:00 2001 From: Alix Damman Date: Fri, 1 Sep 2017 14:28:18 +0200 Subject: [PATCH 1/2] renamed xlabels and ylabels as hlabels and vlabels --- larray_editor/arrayadapter.py | 18 +++---- larray_editor/arraymodel.py | 2 +- larray_editor/arraywidget.py | 98 +++++++++++++++++------------------ 3 files changed, 59 insertions(+), 59 deletions(-) diff --git a/larray_editor/arrayadapter.py b/larray_editor/arrayadapter.py index 8cfbae9..1a57d81 100644 --- a/larray_editor/arrayadapter.py +++ b/larray_editor/arrayadapter.py @@ -6,12 +6,12 @@ class LArrayDataAdapter(object): - def __init__(self, axes_model, xlabels_model, ylabels_model, data_model, + def __init__(self, axes_model, hlabels_model, vlabels_model, data_model, data=None, changes=None, current_filter=None, bg_gradient=None, bg_value=None): # set models self.axes_model = axes_model - self.xlabels_model = xlabels_model - self.ylabels_model = ylabels_model + self.hlabels_model = hlabels_model + self.vlabels_model = vlabels_model self.data_model = data_model # set current filter if current_filter is None: @@ -46,14 +46,14 @@ def get_axes(self): axes_names = axes_names[:-2] + [axes_names[-2] + '\\' + axes_names[-1]] return [[axis_name] for axis_name in axes_names] - def get_xlabels(self): + def get_hlabels(self): axes = self.filtered_data.axes if self.filtered_data.size == 0 or len(axes) == 0: return None else: return [[label] for label in axes.labels[-1]] - def get_ylabels(self): + def get_vlabels(self): axes = self.filtered_data.axes if self.filtered_data.size == 0 or len(axes) == 0: return None @@ -117,14 +117,14 @@ def update_filtered_data(self, current_filter=None, reset_minmax=False): if np.isscalar(self.filtered_data): self.filtered_data = la.aslarray(self.filtered_data) axes = self.get_axes() - xlabels = self.get_xlabels() - ylabels = self.get_ylabels() + hlabels = self.get_hlabels() + vlabels = self.get_vlabels() data_2D = self.get_2D_data() changes_2D = self.get_changes_2D() bg_value_2D = self.get_bg_value_2D(data_2D.shape) self.axes_model.set_data(axes) - self.xlabels_model.set_data(xlabels) - self.ylabels_model.set_data(ylabels) + self.hlabels_model.set_data(hlabels) + self.vlabels_model.set_data(vlabels) self.data_model.set_data(data_2D, changes_2D, reset_minmax=reset_minmax) self.data_model.set_bg_value(bg_value_2D) diff --git a/larray_editor/arraymodel.py b/larray_editor/arraymodel.py index 96a49f8..3273675 100644 --- a/larray_editor/arraymodel.py +++ b/larray_editor/arraymodel.py @@ -147,7 +147,7 @@ def flags(self, index): def get_value(self, index): i = index.row() j = index.column() - # we need to inverse column and row because of the way ylabels are generated + # we need to inverse column and row because of the way vlabels are generated return str(self._data[j][i]) # XXX: I wonder if we shouldn't return a 2D Numpy array of strings? diff --git a/larray_editor/arraywidget.py b/larray_editor/arraywidget.py index 5b8d7d1..40e6dfd 100644 --- a/larray_editor/arraywidget.py +++ b/larray_editor/arraywidget.py @@ -536,17 +536,17 @@ def __init__(self, parent, data=None, readonly=False, bg_value=None, bg_gradient self.model_axes = LabelsArrayModel(parent=self, readonly=readonly) self.view_axes = LabelsView(parent=self, model=self.model_axes, position=(TOP, LEFT)) - self.model_xlabels = LabelsArrayModel(parent=self, readonly=readonly) - self.view_xlabels = LabelsView(parent=self, model=self.model_xlabels, position=(TOP, RIGHT)) + self.model_hlabels = LabelsArrayModel(parent=self, readonly=readonly) + self.view_hlabels = LabelsView(parent=self, model=self.model_hlabels, position=(TOP, RIGHT)) - self.model_ylabels = LabelsArrayModel(parent=self, readonly=readonly) - self.view_ylabels = LabelsView(parent=self, model=self.model_ylabels, position=(BOTTOM, LEFT)) + self.model_vlabels = LabelsArrayModel(parent=self, readonly=readonly) + self.view_vlabels = LabelsView(parent=self, model=self.model_vlabels, position=(BOTTOM, LEFT)) self.model_data = DataArrayModel(parent=self, readonly=readonly, minvalue=minvalue, maxvalue=maxvalue) self.view_data = DataView(parent=self, model=self.model_data, dtype=data.dtype, shape=data.shape) - self.data_adapter = LArrayDataAdapter(axes_model=self.model_axes, xlabels_model=self.model_xlabels, - ylabels_model=self.model_ylabels, data_model=self.model_data, data=data, + self.data_adapter = LArrayDataAdapter(axes_model=self.model_axes, hlabels_model=self.model_hlabels, + vlabels_model=self.model_vlabels, data_model=self.model_data, data=data, bg_value=bg_value, bg_gradient=bg_gradient) # Create vertical and horizontal scrollbars @@ -554,15 +554,15 @@ def __init__(self, parent, data=None, readonly=False, bg_value=None, bg_gradient self.hscrollbar = ScrollBar(self, self.view_data.horizontalScrollBar()) # Synchronize resizing - self.view_axes.horizontalHeader().sectionResized.connect(self.view_ylabels.updateSectionWidth) - self.view_axes.verticalHeader().sectionResized.connect(self.view_xlabels.updateSectionHeight) - self.view_xlabels.horizontalHeader().sectionResized.connect(self.view_data.updateSectionWidth) - self.view_ylabels.verticalHeader().sectionResized.connect(self.view_data.updateSectionHeight) + self.view_axes.horizontalHeader().sectionResized.connect(self.view_vlabels.updateSectionWidth) + self.view_axes.verticalHeader().sectionResized.connect(self.view_hlabels.updateSectionHeight) + self.view_hlabels.horizontalHeader().sectionResized.connect(self.view_data.updateSectionWidth) + self.view_vlabels.verticalHeader().sectionResized.connect(self.view_data.updateSectionHeight) # Synchronize auto-resizing self.view_axes.horizontalHeader().sectionHandleDoubleClicked.connect(self.resize_axes_column_to_contents) - self.view_xlabels.horizontalHeader().sectionHandleDoubleClicked.connect(self.resize_xlabels_column_to_contents) + self.view_hlabels.horizontalHeader().sectionHandleDoubleClicked.connect(self.resize_hlabels_column_to_contents) self.view_axes.verticalHeader().sectionHandleDoubleClicked.connect(self.resize_axes_row_to_contents) - self.view_ylabels.verticalHeader().sectionHandleDoubleClicked.connect(self.resize_ylabels_row_to_contents) + self.view_vlabels.verticalHeader().sectionHandleDoubleClicked.connect(self.resize_vlabels_row_to_contents) # synchronize specific methods self.view_axes.allSelected.connect(self.view_data.selectAll) @@ -572,18 +572,18 @@ def __init__(self, parent, data=None, readonly=False, bg_value=None, bg_gradient self.view_data.signal_plot.connect(self.plot) # Synchronize scrolling - # data <--> xlabels - self.view_data.horizontalScrollBar().valueChanged.connect(self.view_xlabels.horizontalScrollBar().setValue) - self.view_xlabels.horizontalScrollBar().valueChanged.connect(self.view_data.horizontalScrollBar().setValue) - # data <--> ylabels - self.view_data.verticalScrollBar().valueChanged.connect(self.view_ylabels.verticalScrollBar().setValue) - self.view_ylabels.verticalScrollBar().valueChanged.connect(self.view_data.verticalScrollBar().setValue) + # data <--> hlabels + self.view_data.horizontalScrollBar().valueChanged.connect(self.view_hlabels.horizontalScrollBar().setValue) + self.view_hlabels.horizontalScrollBar().valueChanged.connect(self.view_data.horizontalScrollBar().setValue) + # data <--> vlabels + self.view_data.verticalScrollBar().valueChanged.connect(self.view_vlabels.verticalScrollBar().setValue) + self.view_vlabels.verticalScrollBar().valueChanged.connect(self.view_data.verticalScrollBar().setValue) # Synchronize selecting columns(rows) via hor.(vert.) header of x(y)labels view - self.view_xlabels.horizontalHeader().sectionPressed.connect(self.view_data.selectColumn) - self.view_xlabels.horizontalHeader().sectionEntered.connect(self.view_data.selectNewColumn) - self.view_ylabels.verticalHeader().sectionPressed.connect(self.view_data.selectRow) - self.view_ylabels.verticalHeader().sectionEntered.connect(self.view_data.selectNewRow) + self.view_hlabels.horizontalHeader().sectionPressed.connect(self.view_data.selectColumn) + self.view_hlabels.horizontalHeader().sectionEntered.connect(self.view_data.selectNewColumn) + self.view_vlabels.verticalHeader().sectionPressed.connect(self.view_data.selectRow) + self.view_vlabels.verticalHeader().sectionEntered.connect(self.view_data.selectNewRow) # following lines are required to keep usual selection color # when selecting rows/columns via headers of label views. @@ -598,17 +598,17 @@ def __init__(self, parent, data=None, readonly=False, bg_value=None, bg_gradient array_frame.setFrameStyle(QFrame.StyledPanel) # remove borders of internal tables self.view_axes.setFrameStyle(QFrame.NoFrame) - self.view_xlabels.setFrameStyle(QFrame.NoFrame) - self.view_ylabels.setFrameStyle(QFrame.NoFrame) + self.view_hlabels.setFrameStyle(QFrame.NoFrame) + self.view_vlabels.setFrameStyle(QFrame.NoFrame) self.view_data.setFrameStyle(QFrame.NoFrame) # Set layout of table views: - # [ axes ][xlabels]|V| - # [ylabels][ data ]|s| + # [ axes ][vlabels]|V| + # [hlabels][ data ]|s| # | H. scrollbar | array_layout = QGridLayout() array_layout.addWidget(self.view_axes, 0, 0) - array_layout.addWidget(self.view_xlabels, 0, 1) - array_layout.addWidget(self.view_ylabels, 1, 0) + array_layout.addWidget(self.view_hlabels, 0, 1) + array_layout.addWidget(self.view_vlabels, 1, 0) self.view_data.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) array_layout.addWidget(self.view_data, 1, 1) array_layout.addWidget(self.vscrollbar, 0, 2, 2, 1) @@ -777,8 +777,8 @@ def set_data(self, data=None, bg_value=None): # reset default size self.view_axes.set_default_size() - self.view_ylabels.set_default_size() - self.view_xlabels.set_default_size() + self.view_hlabels.set_default_size() + self.view_vlabels.set_default_size() self.view_data.set_default_size() def _update_digits_scientific(self, data): @@ -902,37 +902,37 @@ def autofit_columns(self): self.view_axes.autofit_columns() for column in range(self.model_axes.columnCount()): self.resize_axes_column_to_contents(column) - self.view_xlabels.autofit_columns() - for column in range(self.model_xlabels.columnCount()): - self.resize_xlabels_column_to_contents(column) + self.view_hlabels.autofit_columns() + for column in range(self.model_hlabels.columnCount()): + self.resize_hlabels_column_to_contents(column) def resize_axes_column_to_contents(self, column): # must be connected to view_axes.horizontalHeader().sectionHandleDoubleClicked signal width = max(self.view_axes.horizontalHeader().sectionSize(column), - self.view_ylabels.sizeHintForColumn(column)) - # no need to call resizeSection on view_ylabels (see synchronization lines in init) + self.view_vlabels.sizeHintForColumn(column)) + # no need to call resizeSection on view_vlabels (see synchronization lines in init) self.view_axes.horizontalHeader().resizeSection(column, width) - def resize_xlabels_column_to_contents(self, column): + def resize_hlabels_column_to_contents(self, column): # must be connected to view_labels.horizontalHeader().sectionHandleDoubleClicked signal - width = max(self.view_xlabels.horizontalHeader().sectionSize(column), + width = max(self.view_hlabels.horizontalHeader().sectionSize(column), self.view_data.sizeHintForColumn(column)) # no need to call resizeSection on view_data (see synchronization lines in init) - self.view_xlabels.horizontalHeader().resizeSection(column, width) + self.view_hlabels.horizontalHeader().resizeSection(column, width) def resize_axes_row_to_contents(self, row): # must be connected to view_axes.verticalHeader().sectionHandleDoubleClicked height = max(self.view_axes.verticalHeader().sectionSize(row), - self.view_xlabels.sizeHintForRow(row)) - # no need to call resizeSection on view_xlabels (see synchronization lines in init) + self.view_hlabels.sizeHintForRow(row)) + # no need to call resizeSection on view_hlabels (see synchronization lines in init) self.view_axes.verticalHeader().resizeSection(row, height) - def resize_ylabels_row_to_contents(self, row): + def resize_vlabels_row_to_contents(self, row): # must be connected to view_labels.verticalHeader().sectionHandleDoubleClicked - height = max(self.view_ylabels.verticalHeader().sectionSize(row), + height = max(self.view_vlabels.verticalHeader().sectionSize(row), self.view_data.sizeHintForRow(row)) # no need to call resizeSection on view_data (see synchronization lines in init) - self.view_ylabels.verticalHeader().resizeSection(row, height) + self.view_vlabels.verticalHeader().resizeSection(row, height) @property def dirty(self): @@ -1003,15 +1003,15 @@ def _selection_data(self, headers=True, none_selects_all=True): # FIXME: this is extremely ad-hoc. # TODO: in the future (pandas-based branch) we should use to_string(data[self._selection_filter()]) dim_headers = self.model_axes.get_values() - xlabels = self.model_xlabels.get_values(top=col_min, bottom=col_max) - topheaders = [[dim_header[0] for dim_header in dim_headers] + [label[0] for label in xlabels]] + hlabels = self.model_hlabels.get_values(top=col_min, bottom=col_max) + topheaders = [[dim_header[0] for dim_header in dim_headers] + [label[0] for label in hlabels]] if self.data_adapter.ndim == 1: return chain(topheaders, [chain([''], row) for row in raw_data]) else: assert self.data_adapter.ndim > 1 - ylabels = self.model_ylabels.get_values(left=row_min, right=row_max) + vlabels = self.model_vlabels.get_values(left=row_min, right=row_max) return chain(topheaders, - [chain([ylabels[j][r] for j in range(len(ylabels))], row) + [chain([vlabels[j][r] for j in range(len(vlabels))], row) for r, row in enumerate(raw_data)]) else: return raw_data @@ -1093,8 +1093,8 @@ def plot(self): row_min, row_max, col_min, col_max = self.view_data._selection_bounds() dim_names = self.data_adapter.get_axes_names() # labels - xlabels = [label[0] for label in self.model_xlabels.get_values(top=col_min, bottom=col_max)] - ylabels = self.model_ylabels.get_values(left=row_min, right=row_max) + xlabels = [label[0] for label in self.model_hlabels.get_values(top=col_min, bottom=col_max)] + ylabels = self.model_vlabels.get_values(left=row_min, right=row_max) # transpose ylabels ylabels = [[str(ylabels[i][j]) for i in range(len(ylabels))] for j in range(len(ylabels[0]))] # if there is only one dimension, ylabels is empty From 3d441b143d18c6c456735e1184dbd95c4401c5f2 Mon Sep 17 00:00:00 2001 From: Alix Damman Date: Fri, 1 Sep 2017 15:10:24 +0200 Subject: [PATCH 2/2] fix #14 : horizontal labels accept more than 1 dimension --- larray_editor/arrayadapter.py | 84 ++++++++++++++++++++-------------- larray_editor/arraymodel.py | 38 ++++++++++------ larray_editor/arraywidget.py | 86 ++++++++++++++++++++++------------- 3 files changed, 130 insertions(+), 78 deletions(-) diff --git a/larray_editor/arrayadapter.py b/larray_editor/arrayadapter.py index 1a57d81..d04e9a3 100644 --- a/larray_editor/arrayadapter.py +++ b/larray_editor/arrayadapter.py @@ -6,13 +6,15 @@ class LArrayDataAdapter(object): - def __init__(self, axes_model, hlabels_model, vlabels_model, data_model, - data=None, changes=None, current_filter=None, bg_gradient=None, bg_value=None): + def __init__(self, axes_model, hlabels_model, vlabels_model, data_model, data=None, + changes=None, current_filter=None, nb_dims_hlabels=1, bg_gradient=None, bg_value=None): # set models self.axes_model = axes_model self.hlabels_model = hlabels_model self.vlabels_model = vlabels_model self.data_model = data_model + # set number of dims of hlabels + self.nb_dims_hlabels = nb_dims_hlabels # set current filter if current_filter is None: current_filter = {} @@ -31,38 +33,49 @@ def set_changes(self, changes=None): assert isinstance(changes, dict) self.changes = changes + def update_nb_dims_hlabels(self, nb_dims_hlabels): + self.nb_dims_hlabels = nb_dims_hlabels + self.update_axes_and_labels() + def get_axes_names(self): return self.filtered_data.axes.display_names def get_axes(self): - axes = self.filtered_data.axes + axes_names = self.filtered_data.axes.display_names # test self.filtered_data.size == 0 is required in case an instance built as LArray([]) is passed # test len(axes) == 0 is required when a user filters until to get a scalar - if self.filtered_data.size == 0 or len(axes) == 0: + if self.filtered_data.size == 0 or len(axes_names) == 0: return None + elif len(axes_names) == 1: + return [axes_names] else: - axes_names = axes.display_names - if len(axes_names) >= 2: - axes_names = axes_names[:-2] + [axes_names[-2] + '\\' + axes_names[-1]] - return [[axis_name] for axis_name in axes_names] + nb_dims_vlabels = len(axes_names) - self.nb_dims_hlabels + # axes corresponding to horizontal labels are set to the last column + res = [['' for c in range(nb_dims_vlabels-1)] + [axis_name] for axis_name in axes_names[nb_dims_vlabels:]] + # axes corresponding to vertical labels are set to the last row + res = res + [[axis_name for axis_name in axes_names[:nb_dims_vlabels]]] + return res - def get_hlabels(self): + def get_labels(self): axes = self.filtered_data.axes if self.filtered_data.size == 0 or len(axes) == 0: - return None + vlabels = None + hlabels = None else: - return [[label] for label in axes.labels[-1]] - - def get_vlabels(self): - axes = self.filtered_data.axes - if self.filtered_data.size == 0 or len(axes) == 0: - return None - elif len(axes) == 1: - return [['']] - else: - labels = axes.labels[:-1] - prod = Product(labels) - return [_LazyDimLabels(prod, i) for i in range(len(labels))] + nb_dims_vlabels = len(axes) - self.nb_dims_hlabels + def get_labels_product(axes, extra_row=False): + if len(axes) == 0: + return [[' ']] + else: + # XXX: appends a fake axis instead of using _LazyNone because + # _LazyNone mess up with LabelsArrayModel.get_values (in which slices are used) + if extra_row: + axes.append(la.Axis([' '])) + prod = Product(axes.labels) + return [_LazyDimLabels(prod, i) for i in range(len(axes.labels))] + vlabels = get_labels_product(axes[:nb_dims_vlabels]) + hlabels = get_labels_product(axes[nb_dims_vlabels:], nb_dims_vlabels > 0) + return vlabels, hlabels def get_2D_data(self): """Returns Numpy 2D ndarray""" @@ -109,6 +122,20 @@ def set_data(self, data, bg_value=None, current_filter=None): self.bg_value = la.aslarray(bg_value) if bg_value is not None else None self.update_filtered_data(current_filter, reset_minmax=True) + def update_axes_and_labels(self): + axes = self.get_axes() + vlabels, hlabels = self.get_labels() + self.axes_model.set_data(axes) + self.hlabels_model.set_data(hlabels) + self.vlabels_model.set_data(vlabels) + + def update_data_2D(self, reset_minmax=False): + data_2D = self.get_2D_data() + changes_2D = self.get_changes_2D() + bg_value_2D = self.get_bg_value_2D(data_2D.shape) + self.data_model.set_data(data_2D, changes_2D, reset_minmax=reset_minmax) + self.data_model.set_bg_value(bg_value_2D) + def update_filtered_data(self, current_filter=None, reset_minmax=False): if current_filter is not None: assert isinstance(current_filter, dict) @@ -116,17 +143,8 @@ def update_filtered_data(self, current_filter=None, reset_minmax=False): self.filtered_data = self.la_data[self.current_filter] if np.isscalar(self.filtered_data): self.filtered_data = la.aslarray(self.filtered_data) - axes = self.get_axes() - hlabels = self.get_hlabels() - vlabels = self.get_vlabels() - data_2D = self.get_2D_data() - changes_2D = self.get_changes_2D() - bg_value_2D = self.get_bg_value_2D(data_2D.shape) - self.axes_model.set_data(axes) - self.hlabels_model.set_data(hlabels) - self.vlabels_model.set_data(vlabels) - self.data_model.set_data(data_2D, changes_2D, reset_minmax=reset_minmax) - self.data_model.set_bg_value(bg_value_2D) + self.update_axes_and_labels() + self.update_data_2D(reset_minmax=reset_minmax) def get_data(self): return self.la_data diff --git a/larray_editor/arraymodel.py b/larray_editor/arraymodel.py index 3273675..393720f 100644 --- a/larray_editor/arraymodel.py +++ b/larray_editor/arraymodel.py @@ -125,7 +125,8 @@ class LabelsArrayModel(AbstractArrayModel): font : QFont, optional Font. Default is `Calibri` with size 11. """ - def __init__(self, parent=None, data=None, readonly=False, font=None): + def __init__(self, parent=None, data=None, readonly=False, font=None, orientation=Qt.Horizontal): + self.orientation = orientation AbstractArrayModel.__init__(self, parent, data, readonly, font) self.font.setBold(True) @@ -136,8 +137,12 @@ def _set_data(self, data, changes=None): QMessageBox.critical(self.dialog, "Error", "Expected list or tuple.") data = [[]] self._data = data - self.total_rows = len(data[0]) - self.total_cols = len(data) if self.total_rows > 0 else 0 + if self.orientation == Qt.Horizontal: + self.total_rows = len(data) if self.total_cols > 0 else 0 + self.total_cols = len(data[0]) + else: + self.total_rows = len(data[0]) + self.total_cols = len(data) if self.total_rows > 0 else 0 self._compute_rows_cols_loaded() def flags(self, index): @@ -145,19 +150,26 @@ def flags(self, index): return Qt.ItemIsEnabled def get_value(self, index): - i = index.row() - j = index.column() - # we need to inverse column and row because of the way vlabels are generated - return str(self._data[j][i]) + if self.orientation == Qt.Horizontal: + i, j = index.row(), index.column() + else: + i, j = index.column(), index.row() + return str(self._data[i][j]) # XXX: I wonder if we shouldn't return a 2D Numpy array of strings? def get_values(self, left=0, top=0, right=None, bottom=None): - if right is None: - right = self.total_rows - if bottom is None: - bottom = self.total_cols - values = [list(line[left:right]) for line in self._data[top:bottom]] - return values + if self.orientation == Qt.Horizontal: + if right is None: + right = self.total_cols + if bottom is None: + bottom = self.total_rows + return [list(line[left:right]) for line in self._data[top:bottom]] + else: + if right is None: + right = self.total_rows + if bottom is None: + bottom = self.total_cols + return [list(line[top:bottom]) for line in self._data[left:right]] def data(self, index, role=Qt.DisplayRole): # print('data', index.column(), index.row(), self.rowCount(), self.columnCount(), '\n', self._data) diff --git a/larray_editor/arraywidget.py b/larray_editor/arraywidget.py index 40e6dfd..291bd70 100644 --- a/larray_editor/arraywidget.py +++ b/larray_editor/arraywidget.py @@ -539,7 +539,7 @@ def __init__(self, parent, data=None, readonly=False, bg_value=None, bg_gradient self.model_hlabels = LabelsArrayModel(parent=self, readonly=readonly) self.view_hlabels = LabelsView(parent=self, model=self.model_hlabels, position=(TOP, RIGHT)) - self.model_vlabels = LabelsArrayModel(parent=self, readonly=readonly) + self.model_vlabels = LabelsArrayModel(parent=self, readonly=readonly, orientation=Qt.Vertical) self.view_vlabels = LabelsView(parent=self, model=self.model_vlabels, position=(BOTTOM, LEFT)) self.model_data = DataArrayModel(parent=self, readonly=readonly, minvalue=minvalue, maxvalue=maxvalue) @@ -662,6 +662,13 @@ def __init__(self, parent, data=None, readonly=False, bg_value=None, bg_gradient btn_layout.addWidget(gradient_chooser) self.gradient_chooser = gradient_chooser + label = QLabel("Horizontal Dimensions") + btn_layout.addWidget(label) + spin = QSpinBox(self) + spin.valueChanged.connect(self.nb_horizontal_dims_changed) + self.nb_horizontal_dims_spinbox = spin + btn_layout.addWidget(spin) + # Set widget layout layout = QVBoxLayout() layout.addLayout(self.filters_layout) @@ -675,6 +682,8 @@ def __init__(self, parent, data=None, readonly=False, bg_value=None, bg_gradient # See http://doc.qt.io/qt-4.8/qt-draganddrop-fridgemagnets-dragwidget-cpp.html for an example self.setAcceptDrops(True) + self._raw_data_selection = None + def gradient_changed(self, index): gradient = self.gradient_chooser.itemData(index) if index > 0 else None self.model_data.set_bg_gradient(gradient) @@ -760,8 +769,9 @@ def set_data(self, data=None, bg_value=None): axes = la_data.axes display_names = axes.display_names - # update data format and bgcolor - self._update_digits_scientific(la_data) + # update data format and bgcolor + dim spinbox + self._update_digits_scientific_dims(la_data) + self.nb_horizontal_dims_spinbox.setValue(1) # update filters filters_layout = self.filters_layout @@ -781,7 +791,7 @@ def set_data(self, data=None, bg_value=None): self.view_vlabels.set_default_size() self.view_data.set_default_size() - def _update_digits_scientific(self, data): + def _update_digits_scientific_dims(self, data): """ data : LArray """ @@ -810,6 +820,9 @@ def _update_digits_scientific(self, data): self.gradient_chooser.setEnabled(self.model_data.bgcolor_possible) + self.nb_horizontal_dims_spinbox.setMinimum(1) + self.nb_horizontal_dims_spinbox.setMaximum(max(1, self.data_adapter.ndim - 1)) + def choose_scientific(self, data): # max_digits = self.get_max_digits() # default width can fit 8 chars @@ -942,7 +955,7 @@ def dirty(self): def accept_changes(self): """Accept changes""" la_data = self.data_adapter.accept_changes() - self._update_digits_scientific(la_data) + self._update_digits_scientific_dims(la_data) def reject_changes(self): """Reject changes""" @@ -967,6 +980,9 @@ def digits_changed(self, value): self.digits = value self.model_data.set_format(self.cell_format) + def nb_horizontal_dims_changed(self, value): + self.data_adapter.update_nb_dims_hlabels(value) + def create_filter_combo(self, axis): def filter_changed(checked_items): self.data_adapter.change_filter(axis, checked_items) @@ -1001,15 +1017,15 @@ def _selection_data(self, headers=True, none_selects_all=True): if not self.data_adapter.ndim: return raw_data # FIXME: this is extremely ad-hoc. - # TODO: in the future (pandas-based branch) we should use to_string(data[self._selection_filter()]) + # TODO: in the future (multi_index supported) we should use to_string(data[self._selection_filter()]) dim_headers = self.model_axes.get_values() - hlabels = self.model_hlabels.get_values(top=col_min, bottom=col_max) - topheaders = [[dim_header[0] for dim_header in dim_headers] + [label[0] for label in hlabels]] + hlabels = self.model_hlabels.get_values(left=col_min, right=col_max) + topheaders = [dims + labels for dims, labels in zip(dim_headers, hlabels)] if self.data_adapter.ndim == 1: return chain(topheaders, [chain([''], row) for row in raw_data]) else: assert self.data_adapter.ndim > 1 - vlabels = self.model_vlabels.get_values(left=row_min, right=row_max) + vlabels = self.model_vlabels.get_values(top=row_min, bottom=row_max) return chain(topheaders, [chain([vlabels[j][r] for j in range(len(vlabels))], row) for r, row in enumerate(raw_data)]) @@ -1033,6 +1049,8 @@ def vrepr(v): clipboard = QApplication.clipboard() clipboard.setText(text) + self._raw_data_selection = self._selection_data(headers=False) + def to_excel(self): """View selection in Excel""" if xw is None: @@ -1052,22 +1070,25 @@ def paste(self): if bounds is None: return row_min, row_max, col_min, col_max = bounds - clipboard = QApplication.clipboard() - text = str(clipboard.text()) - list_data = [line.split('\t') for line in text.splitlines()] - try: - # take the first cell which contains '\' - pos_last = next(i for i, v in enumerate(list_data[0]) if '\\' in v) - except StopIteration: - # if there isn't any, assume 1d array - pos_last = 0 - if pos_last: - # ndim > 1 - list_data = [line[pos_last + 1:] for line in list_data[1:]] - elif len(list_data) == 2 and list_data[1][0] == '': - # ndim == 1 - list_data = [list_data[1][1:]] - new_data = np.array(list_data) + # clipboard = QApplication.clipboard() + # text = str(clipboard.text()) + # list_data = [line.split('\t') for line in text.splitlines()] + # try: + # # take the first cell which contains '\' + # pos_last = next(i for i, v in enumerate(list_data[0]) if '\\' in v) + # except StopIteration: + # # if there isn't any, assume 1d array + # pos_last = 0 + # if pos_last: + # # ndim > 1 + # list_data = [line[pos_last + 1:] for line in list_data[1:]] + # elif len(list_data) == 2 and list_data[1][0] == '': + # # ndim == 1 + # list_data = [list_data[1][1:]] + # new_data = np.array(list_data) + if self._raw_data_selection is None: + return + new_data = np.array(self._raw_data_selection) if new_data.shape[0] > 1: row_max = row_min + new_data.shape[0] if new_data.shape[1] > 1: @@ -1093,12 +1114,13 @@ def plot(self): row_min, row_max, col_min, col_max = self.view_data._selection_bounds() dim_names = self.data_adapter.get_axes_names() # labels - xlabels = [label[0] for label in self.model_hlabels.get_values(top=col_min, bottom=col_max)] - ylabels = self.model_vlabels.get_values(left=row_min, right=row_max) - # transpose ylabels - ylabels = [[str(ylabels[i][j]) for i in range(len(ylabels))] for j in range(len(ylabels[0]))] - # if there is only one dimension, ylabels is empty - if not ylabels: + xlabels = self.model_hlabels.get_values(left=col_min, right=col_max, bottom=self.data_adapter.nb_dims_hlabels) + xlabels = [[str(xlabels[i][j]) for i in range(len(xlabels))] for j in range(len(xlabels[0]))] + if self.data_adapter.ndim > 1: + ylabels = self.model_vlabels.get_values(top=row_min, bottom=row_max) + # transpose ylabels + ylabels = [[str(ylabels[i][j]) for i in range(len(ylabels))] for j in range(len(ylabels[0]))] + else: ylabels = [[]] assert data.ndim == 2 @@ -1118,7 +1140,7 @@ def plot(self): else: # plot each row as a line xlabel = dim_names[-1] - xticklabels = [str(label) for label in xlabels] + xticklabels = ['\n'.join(row) for row in xlabels] xdata = np.arange(col_max - col_min) for row in range(len(data)): ax.plot(xdata, data[row], label=' '.join(ylabels[row]))