Skip to content

Commit 9635f14

Browse files
committed
fix #14 : horizontal labels accept more than 1 dimension
1 parent c22a1a8 commit 9635f14

File tree

3 files changed

+108
-63
lines changed

3 files changed

+108
-63
lines changed

larray_editor/arrayadapter.py

Lines changed: 53 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,15 @@
66

77

88
class LArrayDataAdapter(object):
9-
def __init__(self, axes_model, hlabels_model, vlabels_model, data_model,
10-
data=None, changes=None, current_filter=None, bg_gradient=None, bg_value=None):
9+
def __init__(self, axes_model, hlabels_model, vlabels_model, data_model, data=None,
10+
changes=None, current_filter=None, nb_dims_hlabels=1, bg_gradient=None, bg_value=None):
1111
# set models
1212
self.axes_model = axes_model
1313
self.hlabels_model = hlabels_model
1414
self.vlabels_model = vlabels_model
1515
self.data_model = data_model
16+
# set number of dims of hlabels
17+
self.nb_dims_hlabels = nb_dims_hlabels
1618
# set current filter
1719
if current_filter is None:
1820
current_filter = {}
@@ -31,38 +33,49 @@ def set_changes(self, changes=None):
3133
assert isinstance(changes, dict)
3234
self.changes = changes
3335

36+
def update_nb_dims_hlabels(self, nb_dims_hlabels):
37+
self.nb_dims_hlabels = nb_dims_hlabels
38+
self.update_axes_and_labels()
39+
3440
def get_axes_names(self):
3541
return self.filtered_data.axes.display_names
3642

3743
def get_axes(self):
38-
axes = self.filtered_data.axes
44+
axes_names = self.filtered_data.axes.display_names
3945
# test self.filtered_data.size == 0 is required in case an instance built as LArray([]) is passed
4046
# test len(axes) == 0 is required when a user filters until to get a scalar
41-
if self.filtered_data.size == 0 or len(axes) == 0:
42-
return None
43-
else:
44-
axes_names = axes.display_names
45-
if len(axes_names) >= 2:
46-
axes_names = axes_names[:-2] + [axes_names[-2] + '\\' + axes_names[-1]]
47-
return [[axis_name] for axis_name in axes_names]
48-
49-
def get_hlabels(self):
50-
axes = self.filtered_data.axes
51-
if self.filtered_data.size == 0 or len(axes) == 0:
47+
if self.filtered_data.size == 0 or len(axes_names) == 0:
5248
return None
49+
elif len(axes_names) == 1:
50+
return [axes_names]
5351
else:
54-
return [[label] for label in axes.labels[-1]]
52+
nb_dims_vlabels = len(axes_names) - self.nb_dims_hlabels
53+
# axes corresponding to horizontal labels are set to the last column
54+
res = [['' for c in range(nb_dims_vlabels-1)] + [axis_name] for axis_name in axes_names[nb_dims_vlabels:]]
55+
# axes corresponding to vertical labels are set to the last row
56+
res = res + [[axis_name for axis_name in axes_names[:nb_dims_vlabels]]]
57+
return res
5558

56-
def get_vlabels(self):
57-
axes = self.filtered_data.axes
58-
if self.filtered_data.size == 0 or len(axes) == 0:
59-
return None
60-
elif len(axes) == 1:
61-
return [['']]
59+
def get_labels(self):
60+
if self.filtered_data.size == 0:
61+
vlabels = None
62+
hlabels = None
6263
else:
63-
labels = axes.labels[:-1]
64-
prod = Product(labels)
65-
return [_LazyDimLabels(prod, i) for i in range(len(labels))]
64+
axes = self.filtered_data.axes
65+
nb_dims_vlabels = len(axes) - self.nb_dims_hlabels
66+
def get_labels_product(axes, extra_row=False):
67+
if len(axes) == 0:
68+
return None
69+
else:
70+
# XXX: appends a fake axis instead of using _LazyNone because
71+
# _LazyNone mess up with LabelsArrayModel.get_values (in which slices are used)
72+
if extra_row:
73+
axes.append(la.Axis([' ']))
74+
prod = Product(axes.labels)
75+
return [_LazyDimLabels(prod, i) for i in range(len(axes.labels))]
76+
vlabels = get_labels_product(axes[:nb_dims_vlabels])
77+
hlabels = get_labels_product(axes[nb_dims_vlabels:], nb_dims_vlabels > 0)
78+
return vlabels, hlabels
6679

6780
def get_2D_data(self):
6881
"""Returns Numpy 2D ndarray"""
@@ -109,24 +122,29 @@ def set_data(self, data, bg_value=None, current_filter=None):
109122
self.bg_value = la.aslarray(bg_value) if bg_value is not None else None
110123
self.update_filtered_data(current_filter, reset_minmax=True)
111124

125+
def update_axes_and_labels(self):
126+
axes = self.get_axes()
127+
vlabels, hlabels = self.get_labels()
128+
self.axes_model.set_data(axes)
129+
self.hlabels_model.set_data(hlabels)
130+
self.vlabels_model.set_data(vlabels)
131+
132+
def update_data_2D(self, reset_minmax=False):
133+
data_2D = self.get_2D_data()
134+
changes_2D = self.get_changes_2D()
135+
bg_value_2D = self.get_bg_value_2D(data_2D.shape)
136+
self.data_model.set_data(data_2D, changes_2D, reset_minmax=reset_minmax)
137+
self.data_model.set_bg_value(bg_value_2D)
138+
112139
def update_filtered_data(self, current_filter=None, reset_minmax=False):
113140
if current_filter is not None:
114141
assert isinstance(current_filter, dict)
115142
self.current_filter = current_filter
116143
self.filtered_data = self.la_data[self.current_filter]
117144
if np.isscalar(self.filtered_data):
118145
self.filtered_data = la.aslarray(self.filtered_data)
119-
axes = self.get_axes()
120-
hlabels = self.get_hlabels()
121-
vlabels = self.get_vlabels()
122-
data_2D = self.get_2D_data()
123-
changes_2D = self.get_changes_2D()
124-
bg_value_2D = self.get_bg_value_2D(data_2D.shape)
125-
self.axes_model.set_data(axes)
126-
self.hlabels_model.set_data(hlabels)
127-
self.vlabels_model.set_data(vlabels)
128-
self.data_model.set_data(data_2D, changes_2D, reset_minmax=reset_minmax)
129-
self.data_model.set_bg_value(bg_value_2D)
146+
self.update_axes_and_labels()
147+
self.update_data_2D(reset_minmax=reset_minmax)
130148

131149
def get_data(self):
132150
return self.la_data

larray_editor/arraymodel.py

Lines changed: 25 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,8 @@ class LabelsArrayModel(AbstractArrayModel):
125125
font : QFont, optional
126126
Font. Default is `Calibri` with size 11.
127127
"""
128-
def __init__(self, parent=None, data=None, readonly=False, font=None):
128+
def __init__(self, parent=None, data=None, readonly=False, font=None, orientation=Qt.Horizontal):
129+
self.orientation = orientation
129130
AbstractArrayModel.__init__(self, parent, data, readonly, font)
130131
self.font.setBold(True)
131132

@@ -136,28 +137,39 @@ def _set_data(self, data, changes=None):
136137
QMessageBox.critical(self.dialog, "Error", "Expected list or tuple.")
137138
data = [[]]
138139
self._data = data
139-
self.total_rows = len(data[0])
140-
self.total_cols = len(data) if self.total_rows > 0 else 0
140+
if self.orientation == Qt.Horizontal:
141+
self.total_rows = len(data) if self.total_cols > 0 else 0
142+
self.total_cols = len(data[0])
143+
else:
144+
self.total_rows = len(data[0])
145+
self.total_cols = len(data) if self.total_rows > 0 else 0
141146
self._compute_rows_cols_loaded()
142147

143148
def flags(self, index):
144149
"""Set editable flag"""
145150
return Qt.ItemIsEnabled
146151

147152
def get_value(self, index):
148-
i = index.row()
149-
j = index.column()
150-
# we need to inverse column and row because of the way vlabels are generated
151-
return str(self._data[j][i])
153+
if self.orientation == Qt.Horizontal:
154+
i, j = index.row(), index.column()
155+
else:
156+
i, j = index.column(), index.row()
157+
return str(self._data[i][j])
152158

153159
# XXX: I wonder if we shouldn't return a 2D Numpy array of strings?
154160
def get_values(self, left=0, top=0, right=None, bottom=None):
155-
if right is None:
156-
right = self.total_rows
157-
if bottom is None:
158-
bottom = self.total_cols
159-
values = [list(line[left:right]) for line in self._data[top:bottom]]
160-
return values
161+
if self.orientation == Qt.Horizontal:
162+
if right is None:
163+
right = self.total_cols
164+
if bottom is None:
165+
bottom = self.total_rows
166+
return [list(line[left:right]) for line in self._data[top:bottom]]
167+
else:
168+
if right is None:
169+
right = self.total_rows
170+
if bottom is None:
171+
bottom = self.total_cols
172+
return [list(line[top:bottom]) for line in self._data[left:right]]
161173

162174
def data(self, index, role=Qt.DisplayRole):
163175
# print('data', index.column(), index.row(), self.rowCount(), self.columnCount(), '\n', self._data)

larray_editor/arraywidget.py

Lines changed: 30 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -539,7 +539,7 @@ def __init__(self, parent, data=None, readonly=False, bg_value=None, bg_gradient
539539
self.model_hlabels = LabelsArrayModel(parent=self, readonly=readonly)
540540
self.view_hlabels = LabelsView(parent=self, model=self.model_hlabels, position=(TOP, RIGHT))
541541

542-
self.model_vlabels = LabelsArrayModel(parent=self, readonly=readonly)
542+
self.model_vlabels = LabelsArrayModel(parent=self, readonly=readonly, orientation=Qt.Vertical)
543543
self.view_vlabels = LabelsView(parent=self, model=self.model_vlabels, position=(BOTTOM, LEFT))
544544

545545
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
662662
btn_layout.addWidget(gradient_chooser)
663663
self.gradient_chooser = gradient_chooser
664664

665+
label = QLabel("Horizontal Dimensions")
666+
btn_layout.addWidget(label)
667+
spin = QSpinBox(self)
668+
spin.valueChanged.connect(self.nb_horizontal_dims_changed)
669+
self.nb_horizontal_dims_spinbox = spin
670+
btn_layout.addWidget(spin)
671+
665672
# Set widget layout
666673
layout = QVBoxLayout()
667674
layout.addLayout(self.filters_layout)
@@ -760,8 +767,9 @@ def set_data(self, data=None, bg_value=None):
760767
axes = la_data.axes
761768
display_names = axes.display_names
762769

763-
# update data format and bgcolor
770+
# update data format and bgcolor + dim spinbox
764771
self._update_digits_scientific(la_data)
772+
self.nb_horizontal_dims_spinbox.setValue(1)
765773

766774
# update filters
767775
filters_layout = self.filters_layout
@@ -781,7 +789,7 @@ def set_data(self, data=None, bg_value=None):
781789
self.view_vlabels.set_default_size()
782790
self.view_data.set_default_size()
783791

784-
def _update_digits_scientific(self, data):
792+
def _update_digits_scientific_dims(self, data):
785793
"""
786794
data : LArray
787795
"""
@@ -810,6 +818,9 @@ def _update_digits_scientific(self, data):
810818

811819
self.gradient_chooser.setEnabled(self.model_data.bgcolor_possible)
812820

821+
self.nb_horizontal_dims_spinbox.setMinimum(1)
822+
self.nb_horizontal_dims_spinbox.setMaximum(max(1, self.data_adapter.ndim - 1))
823+
813824
def choose_scientific(self, data):
814825
# max_digits = self.get_max_digits()
815826
# default width can fit 8 chars
@@ -942,7 +953,7 @@ def dirty(self):
942953
def accept_changes(self):
943954
"""Accept changes"""
944955
la_data = self.data_adapter.accept_changes()
945-
self._update_digits_scientific(la_data)
956+
self._update_digits_scientific_dims(la_data)
946957

947958
def reject_changes(self):
948959
"""Reject changes"""
@@ -967,6 +978,9 @@ def digits_changed(self, value):
967978
self.digits = value
968979
self.model_data.set_format(self.cell_format)
969980

981+
def nb_horizontal_dims_changed(self, value):
982+
self.data_adapter.update_nb_dims_hlabels(value)
983+
970984
def create_filter_combo(self, axis):
971985
def filter_changed(checked_items):
972986
self.data_adapter.change_filter(axis, checked_items)
@@ -1001,15 +1015,15 @@ def _selection_data(self, headers=True, none_selects_all=True):
10011015
if not self.data_adapter.ndim:
10021016
return raw_data
10031017
# FIXME: this is extremely ad-hoc.
1004-
# TODO: in the future (pandas-based branch) we should use to_string(data[self._selection_filter()])
1018+
# TODO: in the future (multi_index supported) we should use to_string(data[self._selection_filter()])
10051019
dim_headers = self.model_axes.get_values()
1006-
hlabels = self.model_hlabels.get_values(top=col_min, bottom=col_max)
1007-
topheaders = [[dim_header[0] for dim_header in dim_headers] + [label[0] for label in hlabels]]
1020+
hlabels = self.model_hlabels.get_values(left=col_min, right=col_max)
1021+
topheaders = [dims + labels for dims, labels in zip(dim_headers, hlabels)]
10081022
if self.data_adapter.ndim == 1:
10091023
return chain(topheaders, [chain([''], row) for row in raw_data])
10101024
else:
10111025
assert self.data_adapter.ndim > 1
1012-
vlabels = self.model_vlabels.get_values(left=row_min, right=row_max)
1026+
vlabels = self.model_vlabels.get_values(top=row_min, bottom=row_max)
10131027
return chain(topheaders,
10141028
[chain([vlabels[j][r] for j in range(len(vlabels))], row)
10151029
for r, row in enumerate(raw_data)])
@@ -1093,12 +1107,13 @@ def plot(self):
10931107
row_min, row_max, col_min, col_max = self.view_data._selection_bounds()
10941108
dim_names = self.data_adapter.get_axes_names()
10951109
# labels
1096-
xlabels = [label[0] for label in self.model_hlabels.get_values(top=col_min, bottom=col_max)]
1097-
ylabels = self.model_vlabels.get_values(left=row_min, right=row_max)
1098-
# transpose ylabels
1099-
ylabels = [[str(ylabels[i][j]) for i in range(len(ylabels))] for j in range(len(ylabels[0]))]
1100-
# if there is only one dimension, ylabels is empty
1101-
if not ylabels:
1110+
xlabels = self.model_hlabels.get_values(left=col_min, right=col_max, bottom=self.data_adapter.nb_dims_hlabels)
1111+
xlabels = [[str(xlabels[i][j]) for i in range(len(xlabels))] for j in range(len(xlabels[0]))]
1112+
if self.data_adapter.ndim > 1:
1113+
ylabels = self.model_vlabels.get_values(top=row_min, bottom=row_max)
1114+
# transpose ylabels
1115+
ylabels = [[str(ylabels[i][j]) for i in range(len(ylabels))] for j in range(len(ylabels[0]))]
1116+
else:
11021117
ylabels = [[]]
11031118

11041119
assert data.ndim == 2
@@ -1118,7 +1133,7 @@ def plot(self):
11181133
else:
11191134
# plot each row as a line
11201135
xlabel = dim_names[-1]
1121-
xticklabels = [str(label) for label in xlabels]
1136+
xticklabels = ['\n'.join(row) for row in xlabels]
11221137
xdata = np.arange(col_max - col_min)
11231138
for row in range(len(data)):
11241139
ax.plot(xdata, data[row], label=' '.join(ylabels[row]))

0 commit comments

Comments
 (0)