Skip to content

Commit 078dd32

Browse files
committed
fix #14 : horizontal labels accept more than 1 dimension
1 parent 011cbc7 commit 078dd32

File tree

3 files changed

+109
-64
lines changed

3 files changed

+109
-64
lines changed

larray_editor/arrayadapter.py

+53-35
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"""
@@ -110,24 +123,29 @@ def set_data(self, data, bg_gradient=None, bg_value=None, current_filter=None):
110123
self.bg_gradient = bg_gradient
111124
self.update_filtered_data(current_filter)
112125

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

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

larray_editor/arraymodel.py

+25-13
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,8 @@ class LabelsArrayModel(AbstractArrayModel):
127127
font : QFont, optional
128128
Font. Default is `Calibri` with size 11.
129129
"""
130-
def __init__(self, parent=None, data=None, readonly=False, font=None):
130+
def __init__(self, parent=None, data=None, readonly=False, font=None, orientation=Qt.Horizontal):
131+
self.orientation = orientation
131132
AbstractArrayModel.__init__(self, parent, data, readonly, font)
132133
self.font.setBold(True)
133134

@@ -138,28 +139,39 @@ def _set_data(self, data, changes=None):
138139
QMessageBox.critical(self.dialog, "Error", "Expected list or tuple.")
139140
data = [[]]
140141
self._data = data
141-
self.total_rows = len(data[0])
142-
self.total_cols = len(data) if self.total_rows > 0 else 0
142+
if self.orientation == Qt.Horizontal:
143+
self.total_rows = len(data) if self.total_cols > 0 else 0
144+
self.total_cols = len(data[0])
145+
else:
146+
self.total_rows = len(data[0])
147+
self.total_cols = len(data) if self.total_rows > 0 else 0
143148
self._compute_rows_cols_loaded()
144149

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

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

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

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

larray_editor/arraywidget.py

+31-16
Original file line numberDiff line numberDiff line change
@@ -517,7 +517,7 @@ def __init__(self, parent, data=None, readonly=False, bg_value=None, bg_gradient
517517
self.model_hlabels = LabelsArrayModel(parent=self, readonly=readonly)
518518
self.view_hlabels = LabelsView(parent=self, model=self.model_hlabels, position=(TOP, RIGHT))
519519

520-
self.model_vlabels = LabelsArrayModel(parent=self, readonly=readonly)
520+
self.model_vlabels = LabelsArrayModel(parent=self, readonly=readonly, orientation=Qt.Vertical)
521521
self.view_vlabels = LabelsView(parent=self, model=self.model_vlabels, position=(BOTTOM, LEFT))
522522

523523
self.model_data = DataArrayModel(parent=self, readonly=readonly, minvalue=minvalue, maxvalue=maxvalue)
@@ -617,6 +617,13 @@ def __init__(self, parent, data=None, readonly=False, bg_value=None, bg_gradient
617617
self.bgcolor_checkbox = bgcolor
618618
btn_layout.addWidget(bgcolor)
619619

620+
label = QLabel("Horizontal Dimensions")
621+
btn_layout.addWidget(label)
622+
spin = QSpinBox(self)
623+
spin.valueChanged.connect(self.nb_horizontal_dims_changed)
624+
self.nb_horizontal_dims_spinbox = spin
625+
btn_layout.addWidget(spin)
626+
620627
# Set widget layout
621628
layout = QVBoxLayout()
622629
layout.addLayout(self.filters_layout)
@@ -707,8 +714,9 @@ def set_data(self, data=None, bg_gradient=None, bg_value=None):
707714
axes = la_data.axes
708715
display_names = axes.display_names
709716

710-
# update data format and bgcolor
717+
# update data format and bgcolor + dim spinbox
711718
self._update_digits_scientific(la_data)
719+
self.nb_horizontal_dims_spinbox.setValue(1)
712720

713721
# update filters
714722
filters_layout = self.filters_layout
@@ -727,7 +735,7 @@ def set_data(self, data=None, bg_gradient=None, bg_value=None):
727735
self.view_vlabels.set_default_size()
728736
self.view_data.set_default_size()
729737

730-
def _update_digits_scientific(self, data):
738+
def _update_digits_scientific_dims(self, data):
731739
"""
732740
data : LArray
733741
"""
@@ -757,6 +765,9 @@ def _update_digits_scientific(self, data):
757765
self.bgcolor_checkbox.setChecked(self.model_data.bgcolor_enabled)
758766
self.bgcolor_checkbox.setEnabled(self.model_data.bgcolor_enabled)
759767

768+
self.nb_horizontal_dims_spinbox.setMinimum(1)
769+
self.nb_horizontal_dims_spinbox.setMaximum(max(1, self.data_adapter.ndim - 1))
770+
760771
def choose_scientific(self, data):
761772
# max_digits = self.get_max_digits()
762773
# default width can fit 8 chars
@@ -889,7 +900,7 @@ def dirty(self):
889900
def accept_changes(self):
890901
"""Accept changes"""
891902
la_data = self.data_adapter.accept_changes()
892-
self._update_digits_scientific(la_data)
903+
self._update_digits_scientific_dims(la_data)
893904

894905
def reject_changes(self):
895906
"""Reject changes"""
@@ -914,10 +925,13 @@ def digits_changed(self, value):
914925
self.digits = value
915926
self.model_data.set_format(self.cell_format)
916927

928+
def nb_horizontal_dims_changed(self, value):
929+
self.data_adapter.update_nb_dims_hlabels(value)
930+
917931
def create_filter_combo(self, axis):
918932
def filter_changed(checked_items):
919933
filtered = self.data_adapter.change_filter(axis, checked_items)
920-
self._update_digits_scientific(filtered)
934+
self._update_digits_scientific_dims(filtered)
921935
combo = FilterComboBox(self)
922936
combo.addItems([str(l) for l in axis.labels])
923937
combo.checkedItemsChanged.connect(filter_changed)
@@ -949,15 +963,15 @@ def _selection_data(self, headers=True, none_selects_all=True):
949963
if not self.data_adapter.ndim:
950964
return raw_data
951965
# FIXME: this is extremely ad-hoc.
952-
# TODO: in the future (pandas-based branch) we should use to_string(data[self._selection_filter()])
966+
# TODO: in the future (multi_index supported) we should use to_string(data[self._selection_filter()])
953967
dim_headers = self.model_axes.get_values()
954-
hlabels = self.model_hlabels.get_values(top=col_min, bottom=col_max)
955-
topheaders = [[dim_header[0] for dim_header in dim_headers] + [label[0] for label in hlabels]]
968+
hlabels = self.model_hlabels.get_values(left=col_min, right=col_max)
969+
topheaders = [dims + labels for dims, labels in zip(dim_headers, hlabels)]
956970
if self.data_adapter.ndim == 1:
957971
return chain(topheaders, [chain([''], row) for row in raw_data])
958972
else:
959973
assert self.data_adapter.ndim > 1
960-
vlabels = self.model_vlabels.get_values(left=row_min, right=row_max)
974+
vlabels = self.model_vlabels.get_values(top=row_min, bottom=row_max)
961975
return chain(topheaders,
962976
[chain([vlabels[j][r] for j in range(len(vlabels))], row)
963977
for r, row in enumerate(raw_data)])
@@ -1041,12 +1055,13 @@ def plot(self):
10411055
row_min, row_max, col_min, col_max = self.view_data._selection_bounds()
10421056
dim_names = self.data_adapter.get_axes_names()
10431057
# labels
1044-
xlabels = [label[0] for label in self.model_hlabels.get_values(top=col_min, bottom=col_max)]
1045-
ylabels = self.model_vlabels.get_values(left=row_min, right=row_max)
1046-
# transpose ylabels
1047-
ylabels = [[str(ylabels[i][j]) for i in range(len(ylabels))] for j in range(len(ylabels[0]))]
1048-
# if there is only one dimension, ylabels is empty
1049-
if not ylabels:
1058+
xlabels = self.model_hlabels.get_values(left=col_min, right=col_max, bottom=self.data_adapter.nb_dims_hlabels)
1059+
xlabels = [[str(xlabels[i][j]) for i in range(len(xlabels))] for j in range(len(xlabels[0]))]
1060+
if self.data_adapter.ndim > 1:
1061+
ylabels = self.model_vlabels.get_values(top=row_min, bottom=row_max)
1062+
# transpose ylabels
1063+
ylabels = [[str(ylabels[i][j]) for i in range(len(ylabels))] for j in range(len(ylabels[0]))]
1064+
else:
10501065
ylabels = [[]]
10511066

10521067
assert data.ndim == 2
@@ -1066,7 +1081,7 @@ def plot(self):
10661081
else:
10671082
# plot each row as a line
10681083
xlabel = dim_names[-1]
1069-
xticklabels = [str(label) for label in xlabels]
1084+
xticklabels = ['\n'.join(row) for row in xlabels]
10701085
xdata = np.arange(col_max - col_min)
10711086
for row in range(len(data)):
10721087
ax.plot(xdata, data[row], label=' '.join(ylabels[row]))

0 commit comments

Comments
 (0)