Skip to content

Commit 3a17f64

Browse files
committed
FEAT: re-activate sort for hlabels
* also implemented sort on DirectoryPath, Polars DF and LF and Narwhals LF * reset sort when changing the filter. There are cases where we could keep it but this will have to wait later
1 parent ee34256 commit 3a17f64

File tree

2 files changed

+89
-28
lines changed

2 files changed

+89
-28
lines changed

larray_editor/arrayadapter.py

Lines changed: 76 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -970,38 +970,66 @@ def __init__(self, data, attributes):
970970
path_objs = list(data.iterdir())
971971
# sort by type then name
972972
path_objs.sort(key=lambda p: (not p.is_dir(), p.name))
973+
973974
parent_dir = data.parent
974975
if parent_dir != data:
975976
path_objs.insert(0, parent_dir)
976-
self._path_objs = path_objs
977+
self._sorted_path_objs = path_objs
978+
979+
self._colnames = ['Name', 'Time Modified', 'Size']
977980

981+
def shape2d(self):
982+
return len(self._sorted_path_objs), len(self._colnames)
983+
984+
def get_hlabels_values(self, start, stop):
985+
return [self._colnames[start:stop]]
986+
987+
def get_values(self, h_start, v_start, h_stop, v_stop):
978988
def file_mtime_as_str(p) -> str:
979989
try:
980990
mt_time = datetime.fromtimestamp(p.stat().st_mtime)
981991
return mt_time.strftime('%d/%m/%Y %H:%M')
982992
except Exception:
983993
return ''
984-
985-
self._list = [(
994+
parent_dir = self.data.parent
995+
return [(
986996
p.name if p != parent_dir else '..',
987997
# give the mtime of the "current" directory
988-
file_mtime_as_str(p if p != parent_dir else data),
989-
'<directory>' if p.is_dir() else p.stat().st_size
990-
) for p in path_objs]
991-
self._colnames = ['Name', 'Time Modified', 'Size']
998+
file_mtime_as_str(p if p != parent_dir else self.data),
999+
'<directory>' if p.is_dir() else p.stat().st_size
1000+
)[h_start:h_stop] for p in self._sorted_path_objs[v_start:v_stop]]
9921001

993-
def shape2d(self):
994-
return len(self._list), len(self._colnames)
1002+
def can_sort_hlabel(self, row_idx, col_idx):
1003+
return True
9951004

996-
def get_hlabels_values(self, start, stop):
997-
return [self._colnames[start:stop]]
1005+
def sort_hlabel(self, row_idx, col_idx, ascending):
1006+
assert row_idx == 0
1007+
assert col_idx in {0, 1, 2}
1008+
self._current_sort = [(self.num_v_axes() + row_idx, col_idx, ascending)]
1009+
# strip ".." before sorting
1010+
path_objs = self._sorted_path_objs[1:]
1011+
1012+
def get_sort_key(p):
1013+
# name
1014+
if col_idx == 0:
1015+
return not p.is_dir(), p.name
1016+
# time
1017+
elif col_idx == 1:
1018+
return not p.is_dir(), p.stat().st_mtime
1019+
# size
1020+
else:
1021+
return not p.is_dir(), p.stat().st_size
9981022

999-
def get_values(self, h_start, v_start, h_stop, v_stop):
1000-
return [row[h_start:h_stop]
1001-
for row in self._list[v_start:v_stop]]
1023+
path_objs.sort(key=get_sort_key, reverse=not ascending)
1024+
1025+
# re-add ".."
1026+
parent_dir = self.data.parent
1027+
if parent_dir != self.data:
1028+
path_objs.insert(0, parent_dir)
1029+
self._sorted_path_objs = path_objs
10021030

10031031
def cell_activated(self, row_idx, column_idx):
1004-
return self._path_objs[row_idx].absolute()
1032+
return self._sorted_path_objs[row_idx].absolute()
10051033

10061034

10071035
@adapter_for('pathlib.Path')
@@ -2096,6 +2124,10 @@ def get_values(self, h_start, v_start, h_stop, v_stop):
20962124
@adapter_for('polars.DataFrame')
20972125
@adapter_for('narwhals.DataFrame')
20982126
class PolarsDataFrameAdapter(AbstractColumnarAdapter):
2127+
def __init__(self, data, attributes):
2128+
super().__init__(data, attributes=attributes)
2129+
self.sorted_data = data
2130+
20992131
def shape2d(self):
21002132
return self.data.shape
21012133

@@ -2107,7 +2139,15 @@ def get_values(self, h_start, v_start, h_stop, v_stop):
21072139
# has a better behavior for datetime columns (e.g. pl_df3).
21082140
# Otherwise, Polars converts datetimes to floats instead using a numpy
21092141
# object array
2110-
return self.data[v_start:v_stop, h_start:h_stop].to_pandas().values
2142+
return self.sorted_data[v_start:v_stop, h_start:h_stop].to_pandas().values
2143+
2144+
def can_sort_hlabel(self, row_idx, col_idx):
2145+
return True
2146+
2147+
def sort_hlabel(self, row_idx, col_idx, ascending):
2148+
self._current_sort = [(self.num_v_axes() + row_idx, col_idx, ascending)]
2149+
self.sorted_data = self.data.sort(self.data.columns[col_idx],
2150+
descending=not ascending)
21112151

21122152

21132153
@adapter_for('polars.LazyFrame')
@@ -2123,6 +2163,7 @@ def __init__(self, data, attributes):
21232163
# so we could try to use a temporary value and
21242164
# fill the real height as we go like for CSV files
21252165
self._height = data.select(pl.len()).collect(engine='streaming').item()
2166+
self.sorted_data = data
21262167

21272168
def shape2d(self):
21282169
return self._height, len(self._schema)
@@ -2131,14 +2172,23 @@ def get_hlabels_values(self, start, stop):
21312172
return [self._columns[start:stop]]
21322173

21332174
def get_values(self, h_start, v_start, h_stop, v_stop):
2134-
subset = self.data[v_start:v_stop].select(self._columns[h_start:h_stop])
2175+
lf = self.sorted_data
2176+
subset = lf[v_start:v_stop].select(self._columns[h_start:h_stop])
21352177
df = subset.collect(engine='streaming')
21362178
# Going via Pandas instead of directly using to_numpy() because this
21372179
# has a better behavior for datetime columns (e.g. pl_df3).
21382180
# Otherwise, Polars converts datetimes to floats instead using a numpy
21392181
# object array
21402182
return df.to_pandas().values
21412183

2184+
def can_sort_hlabel(self, row_idx, col_idx):
2185+
return True
2186+
2187+
def sort_hlabel(self, row_idx, col_idx, ascending):
2188+
self._current_sort = [(self.num_v_axes() + row_idx, col_idx, ascending)]
2189+
self.sorted_data = self.data.sort(self._columns[col_idx],
2190+
descending=not ascending)
2191+
21422192

21432193
@adapter_for('narwhals.LazyFrame')
21442194
class NarwhalsLazyFrameAdapter(AbstractColumnarAdapter):
@@ -2157,6 +2207,7 @@ def __init__(self, data, attributes):
21572207
# Polars but probably not other engines)
21582208
self._height = data.select(nw.len()).collect(engine='streaming').item()
21592209
self._wh_index = data.with_row_index('_index')
2210+
self.sorted_data = data
21602211

21612212
def shape2d(self):
21622213
return self._height, len(self._schema)
@@ -2174,6 +2225,14 @@ def get_values(self, h_start, v_start, h_stop, v_stop):
21742225
lazy_sub_df = self._wh_index.filter(filter_).select(self._columns[h_start:h_stop])
21752226
return lazy_sub_df.collect(engine='streaming').to_numpy()
21762227

2228+
def can_sort_hlabel(self, row_idx, col_idx):
2229+
return True
2230+
2231+
def sort_hlabel(self, row_idx, col_idx, ascending):
2232+
self._current_sort = [(self.num_v_axes() + row_idx, col_idx, ascending)]
2233+
self.sorted_data = self.data.sort(self._columns[col_idx],
2234+
descending=not ascending)
2235+
21772236

21782237
@adapter_for('iode.Variables')
21792238
class IodeVariablesAdapter(AbstractAdapter):

larray_editor/arraywidget.py

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,7 @@ def change_filter(self, filter_idx, filter_name, indices):
171171
old_h_pos = hscrollbar.value()
172172
old_nrows, old_ncols = data_adapter.shape2d()
173173
data_adapter.update_filter(filter_idx, filter_name, indices)
174+
data_adapter._current_sort = []
174175
# TODO: this does too much work (it sets the adapters even
175176
# if those do not change and sets v_offset/h_offset to 0 when we
176177
# do not *always* want to do so) and maybe too little
@@ -186,10 +187,8 @@ def change_filter(self, filter_idx, filter_name, indices):
186187
# if the old values were already 0, visible_v/hscroll_changed will
187188
# not be triggered and update_*_column_widths has no chance to run
188189
# unless we call them explicitly
189-
array_widget._update_axes_column_widths_from_content()
190-
array_widget._update_hlabels_column_widths_from_content()
191-
array_widget._update_axes_row_heights_from_content()
192-
array_widget._update_vlabels_row_heights_from_content()
190+
assert isinstance(array_widget, ArrayEditorWidget)
191+
array_widget.update_cell_sizes_from_content()
193192
else:
194193
# TODO: would be nice to implement some clever positioning algorithm
195194
# here when new_X != old_X so that the visible rows stay visible.
@@ -765,7 +764,8 @@ def __init__(self, parent, model, hpos, vpos):
765764
AbstractView.__init__(self, parent, model, hpos, vpos)
766765

767766
# FIXME: only have this if the adapter supports any extra action on axes
768-
# self.clicked.connect(self.on_clicked)
767+
if self.vpos == TOP:
768+
self.clicked.connect(self.on_clicked)
769769

770770
def on_clicked(self, index: QModelIndex):
771771
if not index.isValid():
@@ -774,6 +774,8 @@ def on_clicked(self, index: QModelIndex):
774774
row_idx = index.row()
775775
column_idx = index.column()
776776

777+
assert self.vpos == TOP
778+
777779
# FIXME: column_idx works fine for the unfiltered/initial array but on
778780
# an already filtered array it breaks because column_idx is the
779781
# idx of the *filtered* array which can contain less axes while
@@ -782,11 +784,7 @@ def on_clicked(self, index: QModelIndex):
782784
# try:
783785
adapter = self.model().adapter
784786
filtrable = adapter.can_filter_hlabel(1, column_idx)
785-
# hlabels
786-
if self.vpos == TOP:
787-
sortable = adapter.can_sort_hlabel(row_idx, column_idx)
788-
else:
789-
sortable = False
787+
sortable = adapter.can_sort_hlabel(row_idx, column_idx)
790788
if sortable:
791789
sort_direction = adapter.hlabel_sort_direction(row_idx, column_idx)
792790
print(f"LabelsView.on_clicked sortable {sort_direction}")
@@ -1709,8 +1707,9 @@ def update_cell_sizes_from_content(self):
17091707
self._update_hlabels_column_widths_from_content()
17101708
# we do not need to update axes cell size on scroll but vlabels
17111709
# width can change on scroll (and they are linked to axes widths)
1712-
self._update_axes_column_widths_from_content()
17131710
self._update_vlabels_row_heights_from_content()
1711+
self._update_axes_column_widths_from_content()
1712+
self._update_axes_row_heights_from_content()
17141713

17151714
def visible_hscroll_changed(self, value):
17161715
# 'value' will be the first visible column
@@ -2278,6 +2277,9 @@ def sort_axis_labels(self, axis_idx, ascending):
22782277
def sort_hlabel(self, row_idx, col_idx, ascending):
22792278
self.data_adapter.sort_hlabel(row_idx, col_idx, ascending)
22802279
self._set_models_adapter()
2280+
# since we will probably display different rows, they can have different
2281+
# column widths
2282+
self.update_cell_sizes_from_content()
22812283

22822284
def copy(self):
22832285
"""Copy selection as text to clipboard"""

0 commit comments

Comments
 (0)