Skip to content

Commit 144f491

Browse files
committed
* implemented a combobox to choose which color gradient you want to use and provide a few
* changing the filter does not reset/recomputes min/max (so that colors are comparable between two slices of the same array) * always use a gradient defined outside of DataArrayModel. There is no hardcoded gradient anymore. * you can set the gradient and bg_value independently of each other * vmin and vmax are computed by ignoring -inf/+inf so that the whole array is not the same color in those cases. * nan values display as Grey so that they stand out more * fixed background color in compare() being wrong after changing axes order by drag-and-dropping them (closes #89) * refactored ArrayComparator and SessionComparator dialogs to both use a ComparatorWidget to reduce code duplication * compare() will color values depending on relative difference instead of absolute difference as this is usually more useful * like for arraywidget, better color behavior in the presence of -inf/+inf * use nan_equal to compare arrays so that identical arrays are not marked different when they contain nans * changed stack axis name: arrays -> array and sessions -> session. It makes for more natural selections. Sorry for the massive commit. It is all interlinked so it is too hard to commit piecemeal now that I did not commit intermediate steps.
1 parent d28a62c commit 144f491

File tree

5 files changed

+319
-191
lines changed

5 files changed

+319
-191
lines changed

larray_editor/arrayadapter.py

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ def __init__(self, axes_model, xlabels_model, ylabels_model, data_model,
2525
# set data
2626
if data is None:
2727
data = np.empty((0, 0), dtype=np.int8)
28-
self.set_data(data, bg_gradient, bg_value)
28+
self.set_data(data, bg_value, current_filter)
2929

3030
def set_changes(self, changes=None):
3131
assert isinstance(changes, dict)
@@ -99,18 +99,17 @@ def get_bg_value_2D(self, shape_2D):
9999
# XXX: or create two methods?:
100100
# - set_data (which reset the current filter)
101101
# - update_data (which sets new data but keeps current filter unchanged)
102-
def set_data(self, data, bg_gradient=None, bg_value=None, current_filter=None):
102+
def set_data(self, data, bg_value=None, current_filter=None):
103103
if data is None:
104104
data = la.LArray([])
105105
if current_filter is None:
106106
self.current_filter = {}
107107
self.changes = {}
108108
self.la_data = la.aslarray(data)
109109
self.bg_value = la.aslarray(bg_value) if bg_value is not None else None
110-
self.bg_gradient = bg_gradient
111-
self.update_filtered_data(current_filter)
110+
self.update_filtered_data(current_filter, reset_minmax=True)
112111

113-
def update_filtered_data(self, current_filter=None):
112+
def update_filtered_data(self, current_filter=None, reset_minmax=False):
114113
if current_filter is not None:
115114
assert isinstance(current_filter, dict)
116115
self.current_filter = current_filter
@@ -126,8 +125,8 @@ def update_filtered_data(self, current_filter=None):
126125
self.axes_model.set_data(axes)
127126
self.xlabels_model.set_data(xlabels)
128127
self.ylabels_model.set_data(ylabels)
129-
self.data_model.set_data(data_2D, changes_2D)
130-
self.data_model.set_background(self.bg_gradient, bg_value_2D)
128+
self.data_model.set_data(data_2D, changes_2D, reset_minmax=reset_minmax)
129+
self.data_model.set_bg_value(bg_value_2D)
131130

132131
def get_data(self):
133132
return self.la_data

larray_editor/arraymodel.py

Lines changed: 41 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,12 @@
11
from __future__ import absolute_import, division, print_function
22

33
import numpy as np
4+
from larray_editor.utils import (get_font, from_qvariant, to_qvariant, to_text_string,
5+
is_float, is_number, LinearGradient, SUPPORTED_FORMATS, scale_to_01range)
46
from qtpy.QtCore import Qt, QModelIndex, QAbstractTableModel
57
from qtpy.QtGui import QColor
68
from qtpy.QtWidgets import QMessageBox
79

8-
from larray_editor.utils import (get_font, from_qvariant, to_qvariant, to_text_string,
9-
is_float, is_number, LinearGradient, SUPPORTED_FORMATS)
10-
1110
LARGE_SIZE = 5e5
1211
LARGE_NROWS = 1e5
1312
LARGE_COLS = 60
@@ -50,8 +49,8 @@ def __init__(self, parent=None, data=None, readonly=False, font=None):
5049
def _set_data(self, data, changes=None):
5150
raise NotImplementedError()
5251

53-
def set_data(self, data, changes=None):
54-
self._set_data(data, changes)
52+
def set_data(self, data, changes=None, **kwargs):
53+
self._set_data(data, changes, **kwargs)
5554
self.reset()
5655

5756
def rowCount(self, parent=QModelIndex()):
@@ -216,15 +215,13 @@ def __init__(self, parent=None, data=None, readonly=False, format="%.3f", font=N
216215
AbstractArrayModel.__init__(self, parent, data, readonly, font)
217216
self._format = format
218217

219-
# Backgroundcolor settings (HSV --> Hue, Saturation, Value, Alpha-channel)
220-
self.hsv_min = [0.99, 0.7, 1.0, 0.6]
221-
self.hsv_max = [0.66, 0.7, 1.0, 0.6]
222-
self.bgcolor_enabled = True
223-
224218
self.minvalue = minvalue
225219
self.maxvalue = maxvalue
226-
self.set_data(data)
227-
self.set_background(bg_gradient, bg_value)
220+
self._set_data(data)
221+
self._set_bg_gradient(bg_gradient)
222+
self._set_bg_value(bg_value)
223+
# XXX: unsure this is necessary at all in __init__
224+
self.reset()
228225

229226
def get_format(self):
230227
"""Return current format"""
@@ -235,7 +232,7 @@ def get_data(self):
235232
"""Return data"""
236233
return self._data
237234

238-
def _set_data(self, data, changes=None):
235+
def _set_data(self, data, changes=None, reset_minmax=True):
239236
if changes is None:
240237
changes = {}
241238
self.changes = changes
@@ -260,62 +257,51 @@ def _set_data(self, data, changes=None):
260257
if dtype in (np.complex64, np.complex128):
261258
self.color_func = np.abs
262259
else:
263-
# XXX: this is a no-op (it returns the array itself) for most types (I think all non complex types)
264-
# => use an explicit nop?
265-
# def nop(v):
266-
# return v
267-
# self.color_func = nop
268-
self.color_func = np.real
260+
self.color_func = None
269261
# --------------------------------------
270262
self.total_rows, self.total_cols = self._data.shape
271-
self.reset_minmax()
263+
if reset_minmax:
264+
self.reset_minmax()
272265
self._compute_rows_cols_loaded()
273266

274267
def reset_minmax(self):
275-
# this will be awful to get right, because ideally, we should
276-
# include self.changes.values() and ignore values corresponding to
277-
# self.changes.keys()
278268
data = self.get_values()
279269
try:
280-
color_value = self.color_func(data)
270+
color_value = self.color_func(data) if self.color_func is not None else data
271+
# ignore nan, -inf, inf (setting them to 0 or to very large numbers is not an option)
272+
color_value = color_value[np.isfinite(color_value)]
281273
self.vmin = float(np.nanmin(color_value))
282274
self.vmax = float(np.nanmax(color_value))
283-
if self.vmax == self.vmin:
284-
self.vmin -= 1
285-
self.bgcolor_enabled = True
286-
self.bg_gradient = LinearGradient([(self.vmin, self.hsv_min), (self.vmax, self.hsv_max)])
287-
288-
# a) ValueError for empty arrays
289-
# b) AssertionError for fail of LinearGradient.init
290-
# -> for very large float number (e.g. 1664780726569649730), doing self.vmin -= 1 does nothing.
291-
# Then, vman = vmin and LinearGradient.init fails
292-
except (TypeError, ValueError, AssertionError):
275+
self.bgcolor_possible = True
276+
# ValueError for empty arrays, TypeError for object/string arrays
277+
except (TypeError, ValueError):
293278
self.vmin = None
294279
self.vmax = None
295-
self.bgcolor_enabled = False
296-
self.bg_gradient = None
280+
self.bgcolor_possible = False
297281

298282
def set_format(self, format):
299283
"""Change display format"""
300284
self._format = format
301285
self.reset()
302286

303-
def set_background(self, bg_gradient=None, bg_value=None):
287+
def set_bg_gradient(self, bg_gradient):
288+
self._set_bg_gradient(bg_gradient)
289+
self.reset()
290+
291+
def _set_bg_gradient(self, bg_gradient):
304292
if bg_gradient is not None and not isinstance(bg_gradient, LinearGradient):
305293
raise ValueError("Expected None or LinearGradient instance for `bg_gradient` argument")
294+
self.bg_gradient = bg_gradient
295+
296+
def set_bg_value(self, bg_value):
297+
self._set_bg_value(bg_value)
298+
self.reset()
299+
300+
def _set_bg_value(self, bg_value):
306301
if bg_value is not None and not (isinstance(bg_value, np.ndarray) and bg_value.shape == self._data.shape):
307302
raise ValueError("Expected None or 2D Numpy ndarray with shape {} for `bg_value` argument"
308303
.format(self._data.shape))
309-
# self.bg_gradient must never be None
310-
if bg_gradient is not None:
311-
self.bg_gradient = bg_gradient
312304
self.bg_value = bg_value
313-
self.reset()
314-
315-
def bgcolor(self, state):
316-
"""Toggle backgroundcolor"""
317-
self.bgcolor_enabled = state > 0
318-
self.reset()
319305

320306
def get_value(self, index):
321307
i, j = index.row(), index.column()
@@ -354,12 +340,14 @@ def data(self, index, role=Qt.DisplayRole):
354340
else:
355341
return to_qvariant(self._format % value)
356342
elif role == Qt.BackgroundColorRole:
357-
if self.bgcolor_enabled and value is not np.ma.masked:
343+
if self.bgcolor_possible and self.bg_gradient is not None and value is not np.ma.masked:
358344
if self.bg_value is None:
359-
return self.bg_gradient[float(self.color_func(value))]
345+
v = float(self.color_func(value) if self.color_func is not None else value)
346+
v = scale_to_01range(v, self.vmin, self.vmax)
360347
else:
361348
i, j = index.row(), index.column()
362-
return self.bg_gradient[self.bg_value[i, j]]
349+
v = self.bg_value[i, j]
350+
return self.bg_gradient[v]
363351
# elif role == Qt.ToolTipRole:
364352
# return to_qvariant("{}\n{}".format(repr(value),self.get_labels(index)))
365353
return to_qvariant()
@@ -469,11 +457,13 @@ def set_values(self, left, top, right, bottom, values):
469457

470458
# Update vmin/vmax if necessary
471459
if self.vmin is not None and self.vmax is not None:
472-
colorval = self.color_func(values)
473-
old_colorval = self.color_func(oldvalues)
460+
colorval = self.color_func(values) if self.color_func is not None else values
461+
old_colorval = self.color_func(oldvalues) if self.color_func is not None else oldvalues
474462
if np.any(((old_colorval == self.vmax) & (colorval < self.vmax)) |
475463
((old_colorval == self.vmin) & (colorval > self.vmin))):
476464
self.reset_minmax()
465+
# this is faster, when the condition is False (which should be most of the cases) than computing
466+
# subset_max and checking if subset_max > self.vmax
477467
if np.any(colorval > self.vmax):
478468
self.vmax = float(np.nanmax(colorval))
479469
if np.any(colorval < self.vmin):

larray_editor/arraywidget.py

Lines changed: 66 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -74,19 +74,20 @@
7474
from itertools import chain
7575

7676
import numpy as np
77-
from qtpy.QtCore import Qt, QPoint, QItemSelection, QItemSelectionModel, Signal
78-
from qtpy.QtGui import QDoubleValidator, QIntValidator, QKeySequence, QFontMetrics, QCursor
77+
from qtpy.QtCore import Qt, QPoint, QItemSelection, QItemSelectionModel, Signal, QSize
78+
from qtpy.QtGui import (QDoubleValidator, QIntValidator, QKeySequence, QFontMetrics, QCursor, QPixmap, QPainter,
79+
QLinearGradient, QColor, QIcon)
7980
from qtpy.QtWidgets import (QApplication, QTableView, QItemDelegate, QLineEdit, QCheckBox,
8081
QMessageBox, QMenu, QLabel, QSpinBox, QWidget, QToolTip, QShortcut, QScrollBar,
81-
QHBoxLayout, QVBoxLayout, QGridLayout, QSizePolicy, QFrame)
82+
QHBoxLayout, QVBoxLayout, QGridLayout, QSizePolicy, QFrame, QComboBox)
8283

8384
try:
8485
import xlwings as xw
8586
except ImportError:
8687
xw = None
8788

8889
from larray_editor.utils import (keybinding, create_action, clear_layout, get_font, from_qvariant, to_qvariant,
89-
is_number, is_float, _, ima)
90+
is_number, is_float, _, ima, LinearGradient)
9091
from larray_editor.arrayadapter import LArrayDataAdapter
9192
from larray_editor.arraymodel import LabelsArrayModel, DataArrayModel
9293
from larray_editor.combo import FilterComboBox, FilterMenu
@@ -505,10 +506,30 @@ def __init__(self, parent, data_scrollbar):
505506
self.rangeChanged.connect(data_scrollbar.setRange)
506507

507508

509+
available_gradients = [
510+
# Hue, Saturation, Value, Alpha-channel
511+
('red-blue', LinearGradient([(0, [0.99, 0.7, 1.0, 0.6]), (1, [0.66, 0.7, 1.0, 0.6])])),
512+
('blue-red', LinearGradient([(0, [0.66, 0.7, 1.0, 0.6]), (1, [0.99, 0.7, 1.0, 0.6])])),
513+
('red-white-blue', LinearGradient([(0, [.99, .85, 1., .6]),
514+
(0.5 - 1e-16, [.99, .15, 1., .6]),
515+
(0.5, [1., 0., 1., 1.]),
516+
(0.5 + 1e-16, [.66, .15, 1., .6]),
517+
(1, [.66, .85, 1., .6])])),
518+
('blue-white-red', LinearGradient([(0, [.66, .85, 1., .6]),
519+
(0.5 - 1e-16, [.66, .15, 1., .6]),
520+
(0.5, [1., 0., 1., 1.]),
521+
(0.5 + 1e-16, [.99, .15, 1., .6]),
522+
(1, [.99, .85, 1., .6])])),
523+
]
524+
gradient_map = dict(available_gradients)
525+
526+
527+
508528
class ArrayEditorWidget(QWidget):
509-
def __init__(self, parent, data=None, readonly=False, bg_value=None, bg_gradient=None,
529+
def __init__(self, parent, data=None, readonly=False, bg_value=None, bg_gradient='blue-red',
510530
minvalue=None, maxvalue=None):
511531
QWidget.__init__(self, parent)
532+
assert bg_gradient in gradient_map
512533
readonly = np.isscalar(data)
513534
self.readonly = readonly
514535

@@ -613,10 +634,33 @@ def __init__(self, parent, data=None, readonly=False, bg_value=None, bg_gradient
613634
self.scientific_checkbox = scientific
614635
btn_layout.addWidget(scientific)
615636

616-
bgcolor = QCheckBox(_('Background color'))
617-
bgcolor.stateChanged.connect(self.model_data.bgcolor)
618-
self.bgcolor_checkbox = bgcolor
619-
btn_layout.addWidget(bgcolor)
637+
gradient_chooser = QComboBox()
638+
gradient_chooser.setMaximumSize(120, 20)
639+
gradient_chooser.setIconSize(QSize(100, 20))
640+
641+
pixmap = QPixmap(100, 15)
642+
pixmap.fill(Qt.white)
643+
gradient_chooser.addItem(QIcon(pixmap), " ")
644+
645+
pixmap.fill(Qt.transparent)
646+
painter = QPainter(pixmap)
647+
for name, gradient in available_gradients:
648+
qgradient = gradient.as_qgradient()
649+
650+
# * fill with white because gradient can be transparent and if we do not "start from whilte", it skews the
651+
# colors.
652+
# * 1 and 13 instead of 0 and 15 to have a transparent border around/between the gradients
653+
painter.fillRect(0, 1, 100, 13, Qt.white)
654+
painter.fillRect(0, 1, 100, 13, qgradient)
655+
gradient_chooser.addItem(QIcon(pixmap), name, gradient)
656+
657+
# without this, we can crash python :)
658+
del painter, pixmap
659+
# select default gradient
660+
gradient_chooser.setCurrentText(bg_gradient)
661+
gradient_chooser.currentIndexChanged.connect(self.gradient_changed)
662+
btn_layout.addWidget(gradient_chooser)
663+
self.gradient_chooser = gradient_chooser
620664

621665
# Set widget layout
622666
layout = QVBoxLayout()
@@ -625,11 +669,16 @@ def __init__(self, parent, data=None, readonly=False, bg_value=None, bg_gradient
625669
layout.addLayout(btn_layout)
626670
layout.setContentsMargins(0, 0, 0, 0)
627671
self.setLayout(layout)
628-
self.set_data(data, bg_value=bg_value, bg_gradient=bg_gradient)
672+
self.set_data(data, bg_value=bg_value)
673+
self.model_data.set_bg_gradient(gradient_map[bg_gradient])
629674

630675
# See http://doc.qt.io/qt-4.8/qt-draganddrop-fridgemagnets-dragwidget-cpp.html for an example
631676
self.setAcceptDrops(True)
632677

678+
def gradient_changed(self, index):
679+
gradient = self.gradient_chooser.itemData(index) if index > 0 else None
680+
self.model_data.set_bg_gradient(gradient)
681+
633682
def mousePressEvent(self, event):
634683
self.dragLabel = self.childAt(event.pos()) if event.button() == Qt.LeftButton else None
635684
self.dragStartPosition = event.pos()
@@ -692,7 +741,10 @@ def dropEvent(self, event):
692741
new_axes = la_data.axes.copy()
693742
new_axes.insert(new_index, new_axes.pop(new_axes[previous_index]))
694743
la_data = la_data.transpose(new_axes)
695-
self.set_data(la_data, self.model_data.bg_gradient, self.model_data.bg_value)
744+
bg_value = self.data_adapter.bg_value
745+
if bg_value is not None:
746+
bg_value = bg_value.transpose(new_axes)
747+
self.set_data(la_data, bg_value)
696748

697749
event.setDropAction(Qt.MoveAction)
698750
event.accept()
@@ -701,9 +753,9 @@ def dropEvent(self, event):
701753
else:
702754
event.ignore()
703755

704-
def set_data(self, data=None, bg_gradient=None, bg_value=None):
756+
def set_data(self, data=None, bg_value=None):
705757
# update adapter
706-
self.data_adapter.set_data(data, bg_gradient=bg_gradient, bg_value=bg_value)
758+
self.data_adapter.set_data(data, bg_value=bg_value)
707759
la_data = self.data_adapter.get_data()
708760
axes = la_data.axes
709761
display_names = axes.display_names
@@ -756,8 +808,7 @@ def _update_digits_scientific(self, data):
756808
self.scientific_checkbox.setChecked(use_scientific)
757809
self.scientific_checkbox.setEnabled(is_number(dtype))
758810

759-
self.bgcolor_checkbox.setChecked(self.model_data.bgcolor_enabled)
760-
self.bgcolor_checkbox.setEnabled(self.model_data.bgcolor_enabled)
811+
self.gradient_chooser.setEnabled(self.model_data.bgcolor_possible)
761812

762813
def choose_scientific(self, data):
763814
# max_digits = self.get_max_digits()

0 commit comments

Comments
 (0)