Skip to content

Commit 7b03ad3

Browse files
committed
reworked editor internals to avoid calling update methods several times per change (issue #93)
1 parent 9dd39cb commit 7b03ad3

File tree

2 files changed

+88
-86
lines changed

2 files changed

+88
-86
lines changed

larray_editor/arraywidget.py

Lines changed: 81 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -524,7 +524,6 @@ def __init__(self, parent, data_scrollbar):
524524
gradient_map = dict(available_gradients)
525525

526526

527-
528527
class ArrayEditorWidget(QWidget):
529528
def __init__(self, parent, data=None, readonly=False, bg_value=None, bg_gradient='blue-red',
530529
minvalue=None, maxvalue=None):
@@ -756,19 +755,21 @@ def dropEvent(self, event):
756755

757756
def set_data(self, data=None, bg_value=None):
758757
# update adapter
759-
self.data_adapter.set_data(data, bg_value=bg_value)
760-
la_data = self.data_adapter.get_data()
761-
axes = la_data.axes
758+
if data is None:
759+
data = la.LArray([])
760+
else:
761+
data = la.aslarray(data)
762+
axes = data.axes
762763
display_names = axes.display_names
763764

764-
# update data format and bgcolor
765-
self._update_digits_scientific(la_data)
765+
# update data format
766+
self._update_digits_scientific(data)
766767

767768
# update filters
768769
filters_layout = self.filters_layout
769770
clear_layout(filters_layout)
770771
# data.size > 0 to avoid arrays with length 0 axes and len(axes) > 0 to avoid scalars (scalar.size == 1)
771-
if la_data.size > 0 and len(axes) > 0:
772+
if data.size > 0 and len(axes) > 0:
772773
filters_layout.addWidget(QLabel(_("Filters")))
773774
for axis, display_name in zip(axes, display_names):
774775
filters_layout.addWidget(QLabel(display_name))
@@ -779,87 +780,96 @@ def set_data(self, data=None, bg_value=None):
779780
else:
780781
filters_layout.addWidget(QLabel("too big to be filtered"))
781782
filters_layout.addStretch()
782-
self.data_adapter.update_filtered_data({})
783+
784+
self.gradient_chooser.setEnabled(self.model_data.bgcolor_possible)
785+
786+
self.data_adapter.set_data(data, bg_value=bg_value)
783787

784788
# reset default size
785789
self.view_axes.set_default_size()
786790
self.view_ylabels.set_default_size()
787791
self.view_xlabels.set_default_size()
788792
self.view_data.set_default_size()
789793

790-
def _update_digits_scientific(self, data):
794+
# called by set_data and ArrayEditorWidget.accept_changes (this should not be the case IMO)
795+
# two cases:
796+
# * set_data should update both scientific and ndigits
797+
# * toggling scientific checkbox should update only ndigits
798+
def _update_digits_scientific(self, data, scientific=None):
791799
"""
792800
data : LArray
793801
"""
794-
# TODO: Adapter must provide a method to return a data sample as a Numpy array
795-
assert isinstance(data, la.LArray)
796-
data = data.data
797-
size, dtype = data.size, data.dtype
798-
# this will yield a data sample of max 199
799-
step = (size // 100) if size > 100 else 1
800-
data_sample = data.flat[::step]
801-
802-
# TODO: refactor so that the expensive format_helper is not called
803-
# twice (or the values are cached)
804-
use_scientific = self.choose_scientific(data_sample)
802+
dtype = data.dtype
803+
if dtype.type in (np.str, np.str_, np.bool_, np.bool, np.object_):
804+
scientific = False
805+
ndecimals = 0
806+
else:
807+
# XXX: move this to the adapter (return a data sample as a Numpy array)
808+
data = self._get_sample(data)
809+
810+
# max_digits = self.get_max_digits()
811+
# default width can fit 8 chars
812+
# FIXME: use max_digits?
813+
avail_digits = 8
814+
frac_zeros, int_digits, has_negative = self.format_helper(data)
815+
816+
# choose whether or not to use scientific notation
817+
# ================================================
818+
if scientific is None:
819+
# use scientific format if there are more integer digits than we can display or if we can display more
820+
# information that way (scientific format "uses" 4 digits, so we have a net win if we have >= 4 zeros --
821+
# *including the integer one*)
822+
# TODO: only do so if we would actually display more information
823+
# 0.00001 can be displayed with 8 chars
824+
# 1e-05
825+
# would
826+
scientific = int_digits > avail_digits or frac_zeros >= 4
827+
828+
# determine best number of decimals to display
829+
# ============================================
830+
# TODO: ndecimals vs self.digits => rename self.digits to either frac_digits or ndecimals
831+
data_frac_digits = self._data_digits(data)
832+
if scientific:
833+
int_digits = 2 if has_negative else 1
834+
exp_digits = 4
835+
else:
836+
exp_digits = 0
837+
# - 1 for the dot
838+
ndecimals = avail_digits - 1 - int_digits - exp_digits
805839

806-
# XXX: self.ndecimals vs self.digits
807-
self.digits = self.choose_ndecimals(data_sample, use_scientific)
808-
self.use_scientific = use_scientific
809-
self.model_data.set_format(self.cell_format)
840+
if ndecimals < 0:
841+
ndecimals = 0
810842

811-
self.digits_spinbox.setValue(self.digits)
812-
self.digits_spinbox.setEnabled(is_number(dtype))
843+
if data_frac_digits < ndecimals:
844+
ndecimals = data_frac_digits
813845

814-
self.scientific_checkbox.setChecked(use_scientific)
815-
self.scientific_checkbox.setEnabled(is_number(dtype))
846+
self.digits = ndecimals
816847

817-
self.gradient_chooser.setEnabled(self.model_data.bgcolor_possible)
848+
self.use_scientific = scientific
818849

819-
def choose_scientific(self, data):
820-
# max_digits = self.get_max_digits()
821-
# default width can fit 8 chars
822-
# FIXME: use max_digits?
823-
avail_digits = 8
824-
if data.dtype.type in (np.str, np.str_, np.bool_, np.bool, np.object_):
825-
return False
826-
827-
frac_zeros, int_digits, _ = self.format_helper(data)
828-
829-
# if there are more integer digits than we can display or we can
830-
# display more information by using scientific format, do so
831-
# (scientific format "uses" 4 digits, so we win if have >= 4 zeros
832-
# -- *including the integer one*)
833-
# TODO: only do so if we would actually display more information
834-
# 0.00001 can be displayed with 8 chars
835-
# 1e-05
836-
# would
837-
return int_digits > avail_digits or frac_zeros >= 4
838-
839-
def choose_ndecimals(self, data, scientific):
840-
if data.dtype.type in (np.str, np.str_, np.bool_, np.bool, np.object_):
841-
return 0
850+
self.digits_spinbox.blockSignals(True)
851+
self.digits_spinbox.setValue(ndecimals)
852+
self.digits_spinbox.setEnabled(is_number(dtype))
853+
self.digits_spinbox.blockSignals(False)
842854

843-
# max_digits = self.get_max_digits()
844-
# default width can fit 8 chars
845-
# FIXME: use max_digits?
846-
avail_digits = 8
847-
data_frac_digits = self._data_digits(data)
848-
_, int_digits, negative = self.format_helper(data)
849-
if scientific:
850-
int_digits = 2 if negative else 1
851-
exp_digits = 4
852-
else:
853-
exp_digits = 0
854-
# - 1 for the dot
855-
ndecimals = avail_digits - 1 - int_digits - exp_digits
855+
self.scientific_checkbox.blockSignals(True)
856+
self.scientific_checkbox.setChecked(scientific)
857+
self.scientific_checkbox.setEnabled(is_number(dtype))
858+
self.scientific_checkbox.blockSignals(False)
856859

857-
if ndecimals < 0:
858-
ndecimals = 0
860+
# setting the format explicitly instead of relying on digits_spinbox.digits_changed to set it because
861+
# digits_changed is only triggered when digits actually changed, not when passing from scientific -> non
862+
# scientific or number -> object
863+
self.model_data.set_format(self.cell_format)
859864

860-
if data_frac_digits < ndecimals:
861-
ndecimals = data_frac_digits
862-
return ndecimals
865+
def _get_sample(self, data):
866+
assert isinstance(data, la.LArray)
867+
data = data.data
868+
size = data.size
869+
# this will yield a data sample of max 199
870+
step = (size // 100) if size > 100 else 1
871+
sample = data.flat[::step]
872+
return sample[np.isfinite(sample)]
863873

864874
def format_helper(self, data):
865875
if not data.size:
@@ -963,10 +973,7 @@ def cell_format(self):
963973
return '%%.%d%s' % (self.digits, format_letter)
964974

965975
def scientific_changed(self, value):
966-
self.use_scientific = value
967-
self.digits = self.choose_ndecimals(self.data_adapter.get_data(), value)
968-
self.digits_spinbox.setValue(self.digits)
969-
self.model_data.set_format(self.cell_format)
976+
self._update_digits_scientific(self.data_adapter.get_data(), value)
970977

971978
def digits_changed(self, value):
972979
self.digits = value

larray_editor/editor.py

Lines changed: 7 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -108,9 +108,7 @@ def setup_and_check(self, data, title='', readonly=False, minvalue=None, maxvalu
108108
widget.setLayout(layout)
109109

110110
self._listwidget = QListWidget(self)
111-
self._listwidget.currentItemChanged.connect(self.on_item_changed)
112-
# this is a workaround for the fact that no currentItemChanged signal is emitted when no item was selected
113-
# before
111+
# this is a bit more reliable than currentItemChanged which is not emitted when no item was selected before
114112
self._listwidget.itemSelectionChanged.connect(self.on_selection_changed)
115113
self._listwidget.setMinimumWidth(45)
116114

@@ -346,16 +344,14 @@ def select_list_item(self, to_display):
346344
# we need to update the array widget explicitly
347345
self.set_current_array(self.data[to_display], to_display)
348346
else:
349-
# for some reason, on_item_changed is not triggered when no item was selected
350-
if not prev_selected:
351-
self.set_current_array(self.data[to_display], to_display)
352347
self._listwidget.setCurrentItem(changed_items[0])
353348

354349
def update_mapping(self, value):
355350
# XXX: use ordered set so that the order is non-random if the underlying container is ordered?
356351
keys_before = set(self.data.keys())
357352
keys_after = set(value.keys())
358-
# contains both new and updated keys (but not deleted keys)
353+
# Contains both new and keys for which the object id changed (but not deleted keys nor inplace modified keys).
354+
# Inplace modified arrays should be already handled in ipython_cell_executed by the setitem_pattern.
359355
changed_keys = [k for k in keys_after if value[k] is not self.data.get(k)]
360356

361357
# when a key is re-assigned, it can switch from being displayable to non-displayable or vice versa
@@ -438,6 +434,8 @@ def ipython_cell_executed(self):
438434
# otherwise it should have failed at this point, but let us be sure
439435
if varname in clean_ns:
440436
if self._display_in_grid(varname, clean_ns[varname]):
437+
# XXX: this completely refreshes the array, including detecting scientific & ndigits, which might
438+
# not be what we want in this case
441439
self.select_list_item(varname)
442440
else:
443441
# not setitem => assume expr or normal assignment
@@ -448,6 +446,7 @@ def ipython_cell_executed(self):
448446
self.select_list_item(last_input)
449447
else:
450448
# any statement can contain a call to a function which updates globals
449+
# this will select (or refresh) the "first" changed array
451450
self.update_mapping(clean_ns)
452451

453452
# if the statement produced any output (probably because it is a simple expression), display it.
@@ -469,11 +468,7 @@ def on_selection_changed(self, *args, **kwargs):
469468
assert len(selected) == 1
470469
selected_item = selected[0]
471470
assert isinstance(selected_item, QListWidgetItem)
472-
self.on_item_changed(selected_item, None)
473-
474-
def on_item_changed(self, curr, prev):
475-
if curr is not None:
476-
name = str(curr.text())
471+
name = str(selected_item.text())
477472
array = self.data[name]
478473
self.set_current_array(array, name)
479474
expr = self.expressions.get(name, name)

0 commit comments

Comments
 (0)