GitHub Access Token became invalid

It seems like the GitHub access token used for retrieving details about this repository from GitHub became invalid. This might prevent certain types of inspections from being run (in particular, everything related to pull requests).
Please ask an admin of your repository to re-new the access token on this website.

Orange.widgets.visualize.OWHeatMap   F
last analyzed

Complexity

Total Complexity 173

Size/Duplication

Total Lines 939
Duplicated Lines 0 %
Metric Value
dl 0
loc 939
rs 1.2632
wmc 173

39 Methods

Rating   Name   Duplication   Size   Complexity  
A dendrogram_widgets() 0 6 3
C __update_margins() 0 42 7
A update_averages_stripe() 0 10 3
C __update_size_constraints() 0 51 7
A __fixup_grid_layout() 0 5 1
F construct_heatmaps_scene() 0 28 11
A color_palette() 0 9 2
F setup_scene() 0 204 20
B __init__() 0 141 2
A clear() 0 11 1
A heatmap_widgets() 0 6 3
A on_selection_finished() 0 3 1
A offset() 0 5 2
A commit() 0 8 4
D construct_heatmaps() 0 41 8
A update_heatmaps() 0 7 2
B __select_by_cluster() 0 17 7
A label_widgets() 0 6 3
F _make() 0 37 11
A selected_split_label() 0 4 2
A __update_selection_geometry() 0 15 3
A select_col() 0 10 4
D cluster_rows() 0 29 8
A legend_widgets() 0 4 3
C update_annotations() 0 22 8
C update_column_annotations() 0 22 7
A clear_scene() 0 12 1
A update_sorting_attributes() 0 3 2
F set_dataset() 0 45 14
A eventFilter() 0 6 3
F cluster_columns() 0 31 12
A update_color_schema() 0 7 3
A update_grid_spacing() 0 7 2
A __aspect_mode_changed() 0 13 2
A update_sorting_examples() 0 3 2
A select_row() 0 10 4
A update_legend() 0 4 3
A sizeHint() 0 2 1
A save_graph() 0 6 1

How to fix   Complexity   

Complex Class

Complex classes like Orange.widgets.visualize.OWHeatMap often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

1
import sys
2
import math
3
from collections import defaultdict
4
from types import SimpleNamespace as namespace
5
6
import numpy as np
0 ignored issues
show
Configuration introduced by
The import numpy could not be resolved.

This can be caused by one of the following:

1. Missing Dependencies

This error could indicate a configuration issue of Pylint. Make sure that your libraries are available by adding the necessary commands.

# .scrutinizer.yml
before_commands:
    - sudo pip install abc # Python2
    - sudo pip3 install abc # Python3
Tip: We are currently not using virtualenv to run pylint, when installing your modules make sure to use the command for the correct version.

2. Missing __init__.py files

This error could also result from missing __init__.py files in your module folders. Make sure that you place one file in each sub-folder.

Loading history...
7
8
from PyQt4 import QtGui
0 ignored issues
show
Configuration introduced by
The import PyQt4 could not be resolved.

This can be caused by one of the following:

1. Missing Dependencies

This error could indicate a configuration issue of Pylint. Make sure that your libraries are available by adding the necessary commands.

# .scrutinizer.yml
before_commands:
    - sudo pip install abc # Python2
    - sudo pip3 install abc # Python3
Tip: We are currently not using virtualenv to run pylint, when installing your modules make sure to use the command for the correct version.

2. Missing __init__.py files

This error could also result from missing __init__.py files in your module folders. Make sure that you place one file in each sub-folder.

Loading history...
9
from PyQt4.QtGui import (
0 ignored issues
show
Configuration introduced by
The import PyQt4.QtGui could not be resolved.

This can be caused by one of the following:

1. Missing Dependencies

This error could indicate a configuration issue of Pylint. Make sure that your libraries are available by adding the necessary commands.

# .scrutinizer.yml
before_commands:
    - sudo pip install abc # Python2
    - sudo pip3 install abc # Python3
Tip: We are currently not using virtualenv to run pylint, when installing your modules make sure to use the command for the correct version.

2. Missing __init__.py files

This error could also result from missing __init__.py files in your module folders. Make sure that you place one file in each sub-folder.

Loading history...
10
    QSizePolicy, QGraphicsScene, QGraphicsView, QFontMetrics,
11
    QPen, QPixmap, QColor
12
)
13
from PyQt4.QtCore import Qt, QSize, QPointF, QSizeF, QRectF, QObject, QEvent
0 ignored issues
show
Configuration introduced by
The import PyQt4.QtCore could not be resolved.

This can be caused by one of the following:

1. Missing Dependencies

This error could indicate a configuration issue of Pylint. Make sure that your libraries are available by adding the necessary commands.

# .scrutinizer.yml
before_commands:
    - sudo pip install abc # Python2
    - sudo pip3 install abc # Python3
Tip: We are currently not using virtualenv to run pylint, when installing your modules make sure to use the command for the correct version.

2. Missing __init__.py files

This error could also result from missing __init__.py files in your module folders. Make sure that you place one file in each sub-folder.

Loading history...
14
from PyQt4.QtCore import pyqtSignal as Signal
0 ignored issues
show
Configuration introduced by
The import PyQt4.QtCore could not be resolved.

This can be caused by one of the following:

1. Missing Dependencies

This error could indicate a configuration issue of Pylint. Make sure that your libraries are available by adding the necessary commands.

# .scrutinizer.yml
before_commands:
    - sudo pip install abc # Python2
    - sudo pip3 install abc # Python3
Tip: We are currently not using virtualenv to run pylint, when installing your modules make sure to use the command for the correct version.

2. Missing __init__.py files

This error could also result from missing __init__.py files in your module folders. Make sure that you place one file in each sub-folder.

Loading history...
15
import pyqtgraph as pg
0 ignored issues
show
Configuration introduced by
The import pyqtgraph could not be resolved.

This can be caused by one of the following:

1. Missing Dependencies

This error could indicate a configuration issue of Pylint. Make sure that your libraries are available by adding the necessary commands.

# .scrutinizer.yml
before_commands:
    - sudo pip install abc # Python2
    - sudo pip3 install abc # Python3
Tip: We are currently not using virtualenv to run pylint, when installing your modules make sure to use the command for the correct version.

2. Missing __init__.py files

This error could also result from missing __init__.py files in your module folders. Make sure that you place one file in each sub-folder.

Loading history...
16
17
import Orange.data
18
import Orange.distance
19
20
from Orange.clustering import hierarchical
21
from Orange.widgets.utils import colorbrewer
22
from Orange.widgets import widget, gui, settings
23
from Orange.widgets.io import FileFormats
24
25
from Orange.widgets.unsupervised.owhierarchicalclustering import \
26
    DendrogramWidget
27
28
29
def split_domain(domain, split_label):
30
    """Split the domain based on values of `split_label` value.
31
    """
32
    groups = defaultdict(list)
33
    for attr in domain.attributes:
34
        groups[attr.attributes.get(split_label)].append(attr)
35
36
    attr_values = [attr.attributes.get(split_label)
37
                   for attr in domain.attributes]
38
39
    domains = []
40
    for value, attrs in groups.items():
41
        group_domain = Orange.data.Domain(
42
            attrs, domain.class_vars, domain.metas)
43
44
        domains.append((value, group_domain))
45
46
    if domains:
47
        assert(all(len(dom) == len(domains[0][1]) for _, dom in domains))
0 ignored issues
show
Unused Code Coding Style introduced by
There is an unnecessary parenthesis after assert.
Loading history...
48
49
    return sorted(domains, key=lambda t: attr_values.index(t[0]))
50
51
52
def vstack_by_subdomain(data, sub_domains):
53
    domain = sub_domains[0]
54
    newtable = Orange.data.Table(domain)
55
56
    for sub_dom in sub_domains:
57
        sub_data = data.from_table(sub_dom, data)
58
        # TODO: improve O(N ** 2)
0 ignored issues
show
Coding Style introduced by
TODO and FIXME comments should generally be avoided.
Loading history...
59
        newtable.extend(sub_data)
60
61
    return newtable
62
63
64
def select_by_class(data, class_):
65
    indices = select_by_class_indices(data, class_)
66
    return data[indices]
67
68
69
def select_by_class_indices(data, class_):
70
    col, _ = data.get_column_view(data.domain.class_var)
71
    return col == class_
72
73
74
def group_by_unordered(iterable, key):
75
    groups = defaultdict(list)
76
    for item in iterable:
77
        groups[key(item)].append(item)
78
    return groups.items()
79
80
81
def candidate_split_labels(data):
82
    """
83
    Return candidate labels on which we can split the data.
84
    """
85
    groups = defaultdict(list)
86
    for attr in data.domain.attributes:
87
        for item in attr.attributes.items():
88
            groups[item].append(attr)
89
90
    by_keys = defaultdict(list)
91
    for (key, _), attrs in groups.items():
92
        by_keys[key].append(attrs)
93
94
    # Find the keys for which all values have the same number
95
    # of attributes.
96
    candidates = []
97
    for key, groups in by_keys.items():
98
        count = len(groups[0])
99
        if all(len(attrs) == count for attrs in groups) and \
100
                len(groups) > 1 and count > 1:
101
            candidates.append(key)
102
103
    return candidates
104
105
106
def leaf_indices(tree):
107
    return [leaf.value.index for leaf in hierarchical.leaves(tree)]
108
109
110
def palette_gradient(colors, discrete=False):
0 ignored issues
show
Unused Code introduced by
The argument discrete seems to be unused.
Loading history...
111
    n = len(colors)
112
    stops = np.linspace(0.0, 1.0, n, endpoint=True)
113
    gradstops = [(float(stop), color) for stop, color in zip(stops, colors)]
114
    grad = QtGui.QLinearGradient(QPointF(0, 0), QPointF(1, 0))
115
    grad.setStops(gradstops)
116
    return grad
117
118
119
def palette_pixmap(colors, size):
120
    img = QPixmap(size)
121
    img.fill(Qt.transparent)
122
123
    grad = palette_gradient(colors)
124
    grad.setCoordinateMode(QtGui.QLinearGradient.ObjectBoundingMode)
125
126
    painter = QtGui.QPainter(img)
127
    painter.setPen(Qt.NoPen)
128
    painter.setBrush(QtGui.QBrush(grad))
129
    painter.drawRect(0, 0, size.width(), size.height())
130
    painter.end()
131
    return img
132
133
134
def color_palette_model(palettes, iconsize=QSize(64, 16)):
135
    model = QtGui.QStandardItemModel()
136
    for name, palette in palettes:
137
        _, colors = max(palette.items())
138
        colors = [QColor(*c) for c in colors]
139
        item = QtGui.QStandardItem(name)
140
        item.setIcon(QtGui.QIcon(palette_pixmap(colors, iconsize)))
141
        item.setData(palette, Qt.UserRole)
142
        model.appendRow([item])
143
    return model
144
145
146
def color_palette_table(colors, samples=255,
0 ignored issues
show
Unused Code introduced by
The argument samples seems to be unused.
Loading history...
147
                        threshold_low=0.0, threshold_high=1.0):
148
    N = len(colors)
149
    colors = np.array(colors, dtype=np.ubyte)
150
    low, high = threshold_low * 255, threshold_high * 255
151
    points = np.linspace(low, high, N)
152
    space = np.linspace(0, 255, 255)
153
154
    r = np.interp(space, points, colors[:, 0], left=255, right=0)
155
    g = np.interp(space, points, colors[:, 1], left=255, right=0)
156
    b = np.interp(space, points, colors[:, 2], left=255, right=0)
157
    return np.c_[r, g, b]
158
159
# TODO:
0 ignored issues
show
Coding Style introduced by
TODO and FIXME comments should generally be avoided.
Loading history...
160
#     * Richer Tool Tips
161
#     * Color map edit/manage
162
#     * 'Gamma' color transform (nonlinear exponential interpolation)
163
#     * Restore saved row selection (?)
164
#     * 'namespace' use cleanup
165
166
167
class OWHeatMap(widget.OWWidget):
168
    name = "Heat Map"
169
    description = "Heatmap visualization."
170
    icon = "icons/Heatmap.svg"
171
    priority = 1040
172
173
    inputs = [("Data", Orange.data.Table, "set_dataset")]
174
    outputs = [("Selected Data", Orange.data.Table, widget.Default)]
175
176
    settingsHandler = settings.DomainContextHandler()
177
178
    NoSorting, Clustering, OrderedClustering = 0, 1, 2
179
    NoPosition, PositionTop, PositionBottom = 0, 1, 2
180
181
    gamma = settings.Setting(0)
182
    threshold_low = settings.Setting(0.0)
183
    threshold_high = settings.Setting(1.0)
184
    # Type of sorting to apply on rows
185
    sort_rows = settings.Setting(NoSorting)
186
    # Type of sorting to apply on columns
187
    sort_columns = settings.Setting(NoSorting)
188
    # Display stripe with averages
189
    averages = settings.Setting(True)
190
    # Display legend
191
    legend = settings.Setting(True)
192
    # Annotations
193
    annotation_index = settings.ContextSetting(0)
194
    # Stored color palette settings
195
    color_settings = settings.Setting(None)
196
    user_palettes = settings.Setting([])
197
    palette_index = settings.Setting(0)
198
    column_label_pos = settings.Setting(PositionTop)
199
200
    auto_commit = settings.Setting(True)
201
202
    want_graph = True
203
204
    def __init__(self, parent=None):
205
        super().__init__(self, parent)
206
207
        # set default settings
208
        self.SpaceX = 10
209
        self.ShowAnnotation = 0
210
211
        self.colorSettings = None
212
        self.selectedSchemaIndex = 0
213
214
        self.palette = None
215
        self.keep_aspect = False
216
        #: The data striped of discrete features
217
        self.data = None
218
        #: The original data with all features (retained to
219
        #: preserve the domain on the output)
220
        self.input_data = None
221
222
        self.annotation_vars = ['(None)']
223
        self.__rows_cache = {}
224
        self.__columns_cache = {}
225
226
        # GUI definition
227
        colorbox = gui.widgetBox(self.controlArea, "Color")
228
        self.color_cb = gui.comboBox(colorbox, self, "palette_index")
229
        self.color_cb.setIconSize(QSize(64, 16))
230
        palettes = sorted(colorbrewer.colorSchemes["sequential"].items())
231
        palettes += [("Green-Black-Red",
232
                      {3: [(0, 255, 0), (0, 0, 0), (255, 0, 0)]})]
233
        palettes += self.user_palettes
234
        model = color_palette_model(palettes, self.color_cb.iconSize())
235
        model.setParent(self)
236
        self.color_cb.setModel(model)
237
        self.color_cb.activated.connect(self.update_color_schema)
238
        self.color_cb.setCurrentIndex(self.palette_index)
239
        # TODO: Add 'Manage/Add/Remove' action.
0 ignored issues
show
Coding Style introduced by
TODO and FIXME comments should generally be avoided.
Loading history...
240
241
        form = QtGui.QFormLayout(
242
            formAlignment=Qt.AlignLeft,
243
            labelAlignment=Qt.AlignLeft,
244
            fieldGrowthPolicy=QtGui.QFormLayout.AllNonFixedFieldsGrow
245
        )
246
247
        lowslider = gui.hSlider(
248
            colorbox, self, "threshold_low", minValue=0.0, maxValue=1.0,
249
            step=0.05, ticks=True, intOnly=False,
250
            createLabel=False, callback=self.update_color_schema)
251
        highslider = gui.hSlider(
252
            colorbox, self, "threshold_high", minValue=0.0, maxValue=1.0,
253
            step=0.05, ticks=True, intOnly=False,
254
            createLabel=False, callback=self.update_color_schema)
255
256
        form.addRow("Low:", lowslider)
257
        form.addRow("High:", highslider)
258
        colorbox.layout().addLayout(form)
259
260
        sortbox = gui.widgetBox(self.controlArea, "Sorting")
261
        # For attributes
262
        gui.comboBox(sortbox, self, "sort_columns",
263
                     items=["No sorting",
264
                            "Clustering",
265
                            "Clustering with leaf ordering"],
266
                     label='Columns',
267
                     callback=self.update_sorting_attributes)
268
269
        # For examples
270
        gui.comboBox(sortbox, self, "sort_rows",
271
                     items=["No sorting",
272
                            "Clustering",
273
                            "Clustering with leaf ordering"],
274
                     label='Rows',
275
                     callback=self.update_sorting_examples)
276
277
        box = gui.widgetBox(self.controlArea, 'Annotation && Legends')
278
279
        gui.checkBox(box, self, 'legend', 'Show legend',
280
                     callback=self.update_legend)
281
282
        gui.checkBox(box, self, 'averages', 'Stripes with averages',
283
                     callback=self.update_averages_stripe)
284
285
        annotbox = gui.widgetBox(box, "Row Annotations")
286
        annotbox.setFlat(True)
287
        self.annotations_cb = gui.comboBox(annotbox, self, "annotation_index",
288
                                           items=self.annotation_vars,
289
                                           callback=self.update_annotations)
290
291
        posbox = gui.widgetBox(box, "Column Labels Position")
292
        posbox.setFlat(True)
293
294
        gui.comboBox(
295
            posbox, self, "column_label_pos",
296
            items=["None", "Top", "Bottom", "Top and Bottom"],
297
            callback=self.update_column_annotations)
298
299
        gui.checkBox(self.controlArea, self, "keep_aspect",
300
                     "Keep aspect ratio", box="Resize",
301
                     callback=self.__aspect_mode_changed)
302
303
        splitbox = gui.widgetBox(self.controlArea, "Split By")
304
        self.split_lb = QtGui.QListWidget()
305
        self.split_lb.itemSelectionChanged.connect(self.update_heatmaps)
306
        splitbox.layout().addWidget(self.split_lb)
307
308
        gui.rubber(self.controlArea)
309
        gui.auto_commit(self.controlArea, self, "auto_commit", "Commit")
310
311
        # Scene with heatmap
312
        self.heatmap_scene = self.scene = HeatmapScene(parent=self)
313
        self.selection_manager = HeatmapSelectionManager(self)
314
        self.selection_manager.selection_changed.connect(
315
            self.__update_selection_geometry)
316
        self.selection_manager.selection_finished.connect(
317
            self.on_selection_finished)
318
        self.heatmap_scene.set_selection_manager(self.selection_manager)
319
320
        item = QtGui.QGraphicsRectItem(0, 0, 10, 10, None, self.heatmap_scene)
321
        self.heatmap_scene.itemsBoundingRect()
322
        self.heatmap_scene.removeItem(item)
323
324
        policy = (Qt.ScrollBarAlwaysOn if self.keep_aspect
325
                  else Qt.ScrollBarAlwaysOff)
326
        self.sceneView = QGraphicsView(
327
            self.scene,
328
            verticalScrollBarPolicy=policy,
329
            horizontalScrollBarPolicy=policy)
330
331
        self.sceneView.viewport().installEventFilter(self)
332
333
        self.mainArea.layout().addWidget(self.sceneView)
334
        self.heatmap_scene.widget = None
335
336
        self.heatmap_widget_grid = [[]]
337
        self.attr_annotation_widgets = []
338
        self.attr_dendrogram_widgets = []
339
        self.gene_annotation_widgets = []
340
        self.gene_dendrogram_widgets = []
341
342
        self.selection_rects = []
343
        self.selected_rows = []
344
        self.graphButton.clicked.connect(self.save_graph)
345
346
    def sizeHint(self):
0 ignored issues
show
Coding Style introduced by
This method could be written as a function/class method.

If a method does not access any attributes of the class, it could also be implemented as a function or static method. This can help improve readability. For example

class Foo:
    def some_method(self, x, y):
        return x + y;

could be written as

class Foo:
    @classmethod
    def some_method(cls, x, y):
        return x + y;
Loading history...
347
        return QSize(800, 400)
348
349
    def color_palette(self):
350
        data = self.color_cb.itemData(self.palette_index, role=Qt.UserRole)
351
        if data is None:
352
            return []
353
        else:
354
            _, colors = max(data.items())
355
            return color_palette_table(
356
                colors, threshold_low=self.threshold_low,
357
                threshold_high=self.threshold_high)
358
359
    def selected_split_label(self):
360
        """Return the current selected split label."""
361
        item = self.split_lb.currentItem()
362
        return str(item.text()) if item else None
363
364
    def clear(self):
365
        self.data = None
366
        self.input_data = None
367
        self.annotations_cb.clear()
368
        self.annotations_cb.addItem('(None)')
369
        self.split_lb.clear()
370
        self.annotation_vars = ['(None)']
371
        self.clear_scene()
372
        self.selected_rows = []
373
        self.__columns_cache.clear()
374
        self.__rows_cache.clear()
375
376
    def clear_scene(self):
377
        self.selection_manager.set_heatmap_widgets([[]])
378
        self.heatmap_scene.clear()
379
        self.heatmap_scene.widget = None
380
        self.heatmap_widget_grid = [[]]
381
        self.col_annotation_widgets = []
0 ignored issues
show
Coding Style introduced by
The attribute col_annotation_widgets was defined outside __init__.

It is generally a good practice to initialize all attributes to default values in the __init__ method:

class Foo:
    def __init__(self, x=None):
        self.x = x
Loading history...
382
        self.col_annotation_widgets_bottom = []
0 ignored issues
show
Coding Style introduced by
The attribute col_annotation_widgets_bottom was defined outside __init__.

It is generally a good practice to initialize all attributes to default values in the __init__ method:

class Foo:
    def __init__(self, x=None):
        self.x = x
Loading history...
383
        self.col_annotation_widgets_top = []
0 ignored issues
show
Coding Style introduced by
The attribute col_annotation_widgets_top was defined outside __init__.

It is generally a good practice to initialize all attributes to default values in the __init__ method:

class Foo:
    def __init__(self, x=None):
        self.x = x
Loading history...
384
        self.row_annotation_widgets = []
0 ignored issues
show
Coding Style introduced by
The attribute row_annotation_widgets was defined outside __init__.

It is generally a good practice to initialize all attributes to default values in the __init__ method:

class Foo:
    def __init__(self, x=None):
        self.x = x
Loading history...
385
        self.col_dendrograms = []
0 ignored issues
show
Coding Style introduced by
The attribute col_dendrograms was defined outside __init__.

It is generally a good practice to initialize all attributes to default values in the __init__ method:

class Foo:
    def __init__(self, x=None):
        self.x = x
Loading history...
386
        self.row_dendrograms = []
0 ignored issues
show
Coding Style introduced by
The attribute row_dendrograms was defined outside __init__.

It is generally a good practice to initialize all attributes to default values in the __init__ method:

class Foo:
    def __init__(self, x=None):
        self.x = x
Loading history...
387
        self.selection_rects = []
388
389
    def set_dataset(self, data=None):
390
        """Set the input dataset to display."""
391
        self.closeContext()
392
        self.clear()
393
394
        self.error(0)
395
        self.warning(0)
396
        input_data = data
397
        if data is not None and \
398
                any(var.is_discrete for var in data.domain.attributes):
399
            ndisc = sum(var.is_discrete for var in data.domain.attributes)
400
            data = data.from_table(
401
                Orange.data.Domain([var for var in data.domain.attributes
402
                                    if var.is_continuous],
403
                                   data.domain.class_vars,
404
                                   data.domain.metas),
405
                data)
406
            if not data.domain.attributes:
407
                self.error(0, "No continuous feature columns")
408
                input_data = data = None
409
            else:
410
                self.warning(0, "{} discrete column{} removed"
411
                                .format(ndisc, "s" if ndisc > 1 else ""))
412
413
        self.data = data
414
        self.input_data = input_data
415
        if data is not None:
416
            variables = self.data.domain.class_vars + self.data.domain.metas
417
            variables = [var for var in variables
418
                         if isinstance(var, (Orange.data.DiscreteVariable,
419
                                             Orange.data.StringVariable))]
420
            self.annotation_vars.extend(variables)
421
422
            for var in variables:
423
                self.annotations_cb.addItem(*gui.attributeItem(var))
424
425
            self.split_lb.addItems(candidate_split_labels(data))
426
427
            self.openContext(self.data)
428
            if self.annotation_index >= len(self.annotation_vars):
429
                self.annotation_index = 0
430
431
            self.update_heatmaps()
432
433
        self.commit()
434
435
    def update_heatmaps(self):
436
        if self.data is not None:
437
            self.clear_scene()
438
            self.construct_heatmaps(self.data, self.selected_split_label())
439
            self.construct_heatmaps_scene(self.heatmapparts)
440
        else:
441
            self.clear()
442
443
    def _make(self, data, group_var=None, group_key=None):
0 ignored issues
show
Coding Style introduced by
This method could be written as a function/class method.

If a method does not access any attributes of the class, it could also be implemented as a function or static method. This can help improve readability. For example

class Foo:
    def some_method(self, x, y):
        return x + y;

could be written as

class Foo:
    @classmethod
    def some_method(cls, x, y):
        return x + y;
Loading history...
444
        if group_var is not None:
445
            assert group_var.is_discrete
446
            _col_data, _ = data.get_column_view(group_var)
447
            row_groups = [np.flatnonzero(_col_data == i)
448
                          for i in range(len(group_var.values))]
449
            row_indices = [np.flatnonzero(_col_data == i)
450
                           for i in range(len(group_var.values))]
451
            row_groups = [namespace(title=name, indices=ind, cluster=None,
452
                                    cluster_ord=None)
453
                          for name, ind in zip(group_var.values, row_indices)]
454
        else:
455
            row_groups = [namespace(title=None, indices=slice(0, -1),
456
                                    cluster=None, cluster_ord=None)]
457
458
        if group_key is not None:
459
            col_groups = split_domain(data.domain, group_key)
460
            assert len(col_groups) > 0
461
            col_indices = [np.array([data.domain.index(var) for var in group])
462
                           for _, group in col_groups]
463
            col_groups = [namespace(title=name, domain=d, indices=ind,
464
                                    cluster=None, cluster_ord=None)
465
                          for (name, d), ind in zip(col_groups, col_indices)]
466
        else:
467
            col_groups = [
468
                namespace(
469
                    title=None, indices=slice(0, len(data.domain.attributes)),
470
                    domain=data.domain, cluster=None, cluster_ord=None)
471
            ]
472
473
        minv, maxv = np.nanmin(data.X), np.nanmax(data.X)
474
475
        parts = namespace(
476
            rows=row_groups, columns=col_groups,
477
            levels=(minv, maxv),
478
        )
479
        return parts
480
481
    def cluster_rows(self, data, parts, ordered=False):
0 ignored issues
show
Coding Style introduced by
This method could be written as a function/class method.

If a method does not access any attributes of the class, it could also be implemented as a function or static method. This can help improve readability. For example

class Foo:
    def some_method(self, x, y):
        return x + y;

could be written as

class Foo:
    @classmethod
    def some_method(cls, x, y):
        return x + y;
Loading history...
482
        row_groups = []
483
        for row in parts.rows:
484
            if row.cluster is not None:
485
                cluster = row.cluster
486
            else:
487
                cluster = None
488
            if row.cluster_ord is not None:
489
                cluster_ord = row.cluster_ord
490
            else:
491
                cluster_ord = None
492
493
            need_dist = cluster is None or (ordered and cluster_ord is None)
494
            if need_dist:
495
                subset = data[row.indices]
496
                subset = Orange.distance._preprocess(subset)
0 ignored issues
show
Coding Style Best Practice introduced by
It seems like _preprocess was declared protected and should not be accessed from this context.

Prefixing a member variable _ is usually regarded as the equivalent of declaring it with protected visibility that exists in other languages. Consequentially, such a member should only be accessed from the same class or a child class:

class MyParent:
    def __init__(self):
        self._x = 1;
        self.y = 2;

class MyChild(MyParent):
    def some_method(self):
        return self._x    # Ok, since accessed from a child class

class AnotherClass:
    def some_method(self, instance_of_my_child):
        return instance_of_my_child._x   # Would be flagged as AnotherClass is not
                                         # a child class of MyParent
Loading history...
497
                matrix = Orange.distance.Euclidean(subset)
498
499
            if cluster is None:
500
                cluster = hierarchical.dist_matrix_clustering(matrix)
501
502
            if ordered and cluster_ord is None:
503
                cluster_ord = hierarchical.optimal_leaf_ordering(cluster, matrix)
504
505
            row_groups.append(namespace(title=row.title, indices=row.indices,
506
                                        cluster=cluster, cluster_ord=cluster_ord))
507
508
        return namespace(columns=parts.columns, rows=row_groups,
509
                         levels=parts.levels)
510
511
    def cluster_columns(self, data, parts, ordered=False):
0 ignored issues
show
Coding Style introduced by
This method could be written as a function/class method.

If a method does not access any attributes of the class, it could also be implemented as a function or static method. This can help improve readability. For example

class Foo:
    def some_method(self, x, y):
        return x + y;

could be written as

class Foo:
    @classmethod
    def some_method(cls, x, y):
        return x + y;
Loading history...
512
        if len(parts.columns) > 1:
513
            data = vstack_by_subdomain(data, [col.domain for col in parts.columns])
514
        assert all(var.is_continuous for var in data.domain.attributes)
515
516
        col0 = parts.columns[0]
517
        if col0.cluster is not None:
518
            cluster = col0.cluster
519
        else:
520
            cluster = None
521
        if col0.cluster_ord is not None:
522
            cluster_ord = col0.cluster_ord
523
        else:
524
            cluster_ord = None
525
        need_dist = cluster is None or (ordered and cluster_ord is None)
526
527
        if need_dist:
528
            data = Orange.distance._preprocess(data)
0 ignored issues
show
Coding Style Best Practice introduced by
It seems like _preprocess was declared protected and should not be accessed from this context.

Prefixing a member variable _ is usually regarded as the equivalent of declaring it with protected visibility that exists in other languages. Consequentially, such a member should only be accessed from the same class or a child class:

class MyParent:
    def __init__(self):
        self._x = 1;
        self.y = 2;

class MyChild(MyParent):
    def some_method(self):
        return self._x    # Ok, since accessed from a child class

class AnotherClass:
    def some_method(self, instance_of_my_child):
        return instance_of_my_child._x   # Would be flagged as AnotherClass is not
                                         # a child class of MyParent
Loading history...
529
            matrix = Orange.distance.PearsonR(data, axis=0)
530
531
        if cluster is None:
532
            cluster = hierarchical.dist_matrix_clustering(matrix)
533
        if ordered and cluster_ord is None:
534
            cluster_ord = hierarchical.optimal_leaf_ordering(cluster, matrix)
535
536
        col_groups = [namespace(title=col.title, indices=col.indices,
537
                                cluster=cluster, cluster_ord=cluster_ord,
538
                                domain=col.domain)
539
                      for col in parts.columns]
540
        return namespace(columns=col_groups,  rows=parts.rows,
541
                         levels=parts.levels)
542
543
    def construct_heatmaps(self, data, split_label=None):
544
545
        if split_label is not None:
546
            groups = split_domain(data.domain, split_label)
547
            assert len(groups) > 0
548
        else:
549
            groups = [("", data.domain)]
550
551
        if data.domain.has_discrete_class:
552
            group_var = data.domain.class_var
553
        else:
554
            group_var = None
555
556
        self.progressBarInit()
557
558
        group_label = split_label
559
560
        parts = self._make(data, group_var, group_label)
561
        # Restore/update the row/columns items descriptions from cache if
562
        # available
563
        if group_var in self.__rows_cache:
564
            parts.rows = self.__rows_cache[group_var].rows
565
        if group_label in self.__columns_cache:
566
            parts.columns = self.__columns_cache[group_label].columns
567
568
        if self.sort_rows != OWHeatMap.NoSorting:
569
            parts = self.cluster_rows(
570
                self.data, parts,
571
                ordered=self.sort_rows == OWHeatMap.OrderedClustering)
572
573
        if self.sort_columns != OWHeatMap.NoSorting:
574
            parts = self.cluster_columns(
575
                self.data, parts,
576
                ordered=self.sort_columns == OWHeatMap.OrderedClustering)
577
578
        # Cache the updated parts
579
        self.__rows_cache[group_var] = parts
580
        self.__columns_cache[group_label] = parts
581
582
        self.heatmapparts = parts
0 ignored issues
show
Coding Style introduced by
The attribute heatmapparts was defined outside __init__.

It is generally a good practice to initialize all attributes to default values in the __init__ method:

class Foo:
    def __init__(self, x=None):
        self.x = x
Loading history...
583
        self.progressBarFinished()
584
585
    def construct_heatmaps_scene(self, parts):
586
        def select_row(item):
587
            if self.sort_rows == OWHeatMap.NoSorting:
588
                return namespace(title=item.title, indices=item.indices,
589
                                 cluster=None)
590
            elif self.sort_rows == OWHeatMap.Clustering:
591
                return namespace(title=item.title, indices=item.indices,
592
                                 cluster=item.cluster)
593
            elif self.sort_rows == OWHeatMap.OrderedClustering:
594
                return namespace(title=item.title, indices=item.indices,
595
                                 cluster=item.cluster_ord)
596
597
        def select_col(item):
598
            if self.sort_columns == OWHeatMap.NoSorting:
599
                return namespace(title=item.title, indices=item.indices,
600
                                 cluster=None, domain=item.domain)
601
            elif self.sort_columns == OWHeatMap.Clustering:
602
                return namespace(title=item.title, indices=item.indices,
603
                                 cluster=item.cluster, domain=item.domain)
604
            elif self.sort_columns == OWHeatMap.OrderedClustering:
605
                return namespace(title=item.title, indices=item.indices,
606
                                 cluster=item.cluster_ord, domain=item.domain)
607
608
        rows = [select_row(rowitem) for rowitem in parts.rows]
609
        cols = [select_col(colitem) for colitem in parts.columns]
610
        parts = namespace(columns=cols, rows=rows, levels=parts.levels)
611
612
        self.setup_scene(parts)
613
614
    def setup_scene(self, parts):
615
        # parts = * a list of row descriptors (title, indices, cluster,)
616
        #         * a list of col descriptors (title, indices, cluster, domain)
617
618
        self.heatmap_scene.clear()
619
        # The top level container widget
620
        widget = GraphicsWidget()
0 ignored issues
show
Comprehensibility Bug introduced by
widget is re-defining a name which is already available in the outer-scope (previously defined on line 22).

It is generally a bad practice to shadow variables from the outer-scope. In most cases, this is done unintentionally and might lead to unexpected behavior:

param = 5

class Foo:
    def __init__(self, param):   # "param" would be flagged here
        self.param = param
Loading history...
621
        widget.layoutDidActivate.connect(self.__update_selection_geometry)
622
623
        grid = QtGui.QGraphicsGridLayout()
624
        grid.setSpacing(self.SpaceX)
625
        self.heatmap_scene.addItem(widget)
626
627
        N, M = len(parts.rows), len(parts.columns)
628
629
        # Start row/column where the heatmap items are inserted
630
        # (after the titles/legends/dendrograms)
631
        Row0 = 3
632
        Col0 = 3
633
        LegendRow = 0
634
        # The column for the vertical dendrogram
635
        DendrogramColumn = 0
636
        # The row for the horizontal dendrograms
637
        DendrogramRow = 1
638
        RightLabelColumn = Col0 + M
639
        TopLabelsRow = 2
640
        BottomLabelsRow = Row0 + 2 * N
641
642
        widget.setLayout(grid)
643
644
        palette = self.color_palette()
645
646
        sort_i = []
647
        sort_j = []
648
649
        column_dendrograms = [None] * M
650
        row_dendrograms = [None] * N
651
652
        for i, rowitem in enumerate(parts.rows):
653
            if rowitem.title:
654
                title = QtGui.QGraphicsSimpleTextItem(rowitem.title, widget)
655
                item = GraphicsSimpleTextLayoutItem(title, parent=grid)
656
                grid.addItem(item, Row0 + i * 2, Col0)
657
658
            if rowitem.cluster:
659
                dendrogram = DendrogramWidget(
660
                    parent=widget,
661
                    selectionMode=DendrogramWidget.NoSelection,
662
                    hoverHighlightEnabled=True)
663
                dendrogram.set_root(rowitem.cluster)
664
                dendrogram.setMaximumWidth(100)
665
                dendrogram.setMinimumWidth(100)
666
                # Ignore dendrogram vertical size hint (heatmap's size
667
                # should define the  row's vertical size).
668
                dendrogram.setSizePolicy(
669
                    QSizePolicy.Expanding, QSizePolicy.Ignored)
670
                dendrogram.itemClicked.connect(
671
                    lambda item, partindex=i:
672
                        self.__select_by_cluster(item, partindex)
673
                )
674
675
                grid.addItem(dendrogram, Row0 + i * 2 + 1, DendrogramColumn)
676
                sort_i.append(np.array(leaf_indices(rowitem.cluster)))
677
                row_dendrograms[i] = dendrogram
678
            else:
679
                sort_i.append(None)
680
681
        for j, colitem in enumerate(parts.columns):
682
            if colitem.title:
683
                title = QtGui.QGraphicsSimpleTextItem(colitem.title, widget)
684
                item = GraphicsSimpleTextLayoutItem(title, parent=grid)
685
                grid.addItem(item, 1, Col0 + j)
686
687
            if colitem.cluster:
688
                dendrogram = DendrogramWidget(
689
                    parent=widget,
690
                    orientation=DendrogramWidget.Top,
691
                    selectionMode=DendrogramWidget.NoSelection,
692
                    hoverHighlightEnabled=False)
693
694
                dendrogram.set_root(colitem.cluster)
695
                dendrogram.setMaximumHeight(100)
696
                dendrogram.setMinimumHeight(100)
697
                # Ignore dendrogram horizontal size hint (heatmap's width
698
                # should define the column width).
699
                dendrogram.setSizePolicy(
700
                    QSizePolicy.Ignored, QSizePolicy.Expanding)
701
                grid.addItem(dendrogram, DendrogramRow, Col0 + j)
702
                sort_j.append(np.array(leaf_indices(colitem.cluster)))
703
                column_dendrograms[j] = dendrogram
704
            else:
705
                sort_j.append(None)
706
707
        heatmap_widgets = []
708
        for i in range(N):
709
            heatmap_row = []
710
            for j in range(M):
711
                row_ix = parts.rows[i].indices
712
                col_ix = parts.columns[j].indices
713
                hw = GraphicsHeatmapWidget(parent=widget)
714
                data = self.data[row_ix, col_ix].X
715
716
                if sort_i[i] is not None:
717
                    data = data[sort_i[i]]
718
                if sort_j[j] is not None:
719
                    data = data[:, sort_j[j]]
720
721
                hw.set_heatmap_data(data)
722
                hw.set_levels(parts.levels)
723
                hw.set_color_table(palette)
724
                hw.set_show_averages(self.averages)
725
726
                grid.addItem(hw, Row0 + i * 2 + 1, Col0 + j)
727
                grid.setRowStretchFactor(Row0 + i * 2 + 1, data.shape[0] * 100)
728
                heatmap_row.append(hw)
729
            heatmap_widgets.append(heatmap_row)
730
731
        row_annotation_widgets = []
732
        col_annotation_widgets = []
733
        col_annotation_widgets_top = []
734
        col_annotation_widgets_bottom = []
735
736
        for i, rowitem in enumerate(parts.rows):
737
            if isinstance(rowitem.indices, slice):
738
                indices = np.array(
739
                    range(*rowitem.indices.indices(self.data.X.shape[0])))
740
            else:
741
                indices = rowitem.indices
742
            if sort_i[i] is not None:
743
                indices = indices[sort_i[i]]
744
745
            labels = [str(i) for i in indices]
746
747
            labelslist = GraphicsSimpleTextList(
748
                labels, parent=widget, orientation=Qt.Vertical)
749
750
            labelslist._indices = indices
0 ignored issues
show
Coding Style Best Practice introduced by
It seems like _indices was declared protected and should not be accessed from this context.

Prefixing a member variable _ is usually regarded as the equivalent of declaring it with protected visibility that exists in other languages. Consequentially, such a member should only be accessed from the same class or a child class:

class MyParent:
    def __init__(self):
        self._x = 1;
        self.y = 2;

class MyChild(MyParent):
    def some_method(self):
        return self._x    # Ok, since accessed from a child class

class AnotherClass:
    def some_method(self, instance_of_my_child):
        return instance_of_my_child._x   # Would be flagged as AnotherClass is not
                                         # a child class of MyParent
Loading history...
Coding Style introduced by
The attribute _indices was defined outside __init__.

It is generally a good practice to initialize all attributes to default values in the __init__ method:

class Foo:
    def __init__(self, x=None):
        self.x = x
Loading history...
751
            labelslist.setAlignment(Qt.AlignLeft | Qt.AlignVCenter)
752
            labelslist.setContentsMargins(0.0, 0.0, 0.0, 0.0)
753
            labelslist.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Expanding)
754
755
            grid.addItem(labelslist, Row0 + i * 2 + 1, RightLabelColumn)
756
            grid.setAlignment(labelslist, Qt.AlignLeft)
757
            row_annotation_widgets.append(labelslist)
758
759
        for j, colitem in enumerate(parts.columns):
760
            # Top attr annotations
761
            if isinstance(colitem.indices, slice):
762
                indices = np.array(
763
                    range(*colitem.indices.indices(self.data.X.shape[1])))
764
            else:
765
                indices = colitem.indices
766
            if sort_j[j] is not None:
767
                indices = indices[sort_j[j]]
768
769
            labels = [self.data.domain[i].name for i in indices]
770
771
            labelslist = GraphicsSimpleTextList(
772
                labels, parent=widget, orientation=Qt.Horizontal)
773
            labelslist.setAlignment(Qt.AlignBottom | Qt.AlignLeft)
774
            labelslist._indices = indices
0 ignored issues
show
Coding Style Best Practice introduced by
It seems like _indices was declared protected and should not be accessed from this context.

Prefixing a member variable _ is usually regarded as the equivalent of declaring it with protected visibility that exists in other languages. Consequentially, such a member should only be accessed from the same class or a child class:

class MyParent:
    def __init__(self):
        self._x = 1;
        self.y = 2;

class MyChild(MyParent):
    def some_method(self):
        return self._x    # Ok, since accessed from a child class

class AnotherClass:
    def some_method(self, instance_of_my_child):
        return instance_of_my_child._x   # Would be flagged as AnotherClass is not
                                         # a child class of MyParent
Loading history...
Coding Style introduced by
The attribute _indices was defined outside __init__.

It is generally a good practice to initialize all attributes to default values in the __init__ method:

class Foo:
    def __init__(self, x=None):
        self.x = x
Loading history...
775
776
            labelslist.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
777
778
            grid.addItem(labelslist, TopLabelsRow, Col0 + j,
779
                         Qt.AlignBottom | Qt.AlignLeft)
780
            col_annotation_widgets.append(labelslist)
781
            col_annotation_widgets_top.append(labelslist)
782
783
            # Bottom attr annotations
784
            labelslist = GraphicsSimpleTextList(
785
                labels, parent=widget, orientation=Qt.Horizontal)
786
            labelslist.setAlignment(Qt.AlignTop | Qt.AlignHCenter)
787
            labelslist.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
788
789
            grid.addItem(labelslist, BottomLabelsRow, Col0 + j)
790
            col_annotation_widgets.append(labelslist)
791
            col_annotation_widgets_bottom.append(labelslist)
792
793
        legend = GradientLegendWidget(
794
            parts.levels[0], parts.levels[1],
795
            parent=widget)
796
797
        legend.set_color_table(palette)
798
        legend.setMinimumSize(QSizeF(100, 20))
799
        legend.setVisible(self.legend)
800
801
        grid.addItem(legend, LegendRow, Col0)
802
803
        self.heatmap_scene.widget = widget
804
        self.heatmap_widget_grid = heatmap_widgets
805
        self.selection_manager.set_heatmap_widgets(heatmap_widgets)
806
807
        self.row_annotation_widgets = row_annotation_widgets
0 ignored issues
show
Coding Style introduced by
The attribute row_annotation_widgets was defined outside __init__.

It is generally a good practice to initialize all attributes to default values in the __init__ method:

class Foo:
    def __init__(self, x=None):
        self.x = x
Loading history...
808
        self.col_annotation_widgets = col_annotation_widgets
0 ignored issues
show
Coding Style introduced by
The attribute col_annotation_widgets was defined outside __init__.

It is generally a good practice to initialize all attributes to default values in the __init__ method:

class Foo:
    def __init__(self, x=None):
        self.x = x
Loading history...
809
        self.col_annotation_widgets_top = col_annotation_widgets_top
0 ignored issues
show
Coding Style introduced by
The attribute col_annotation_widgets_top was defined outside __init__.

It is generally a good practice to initialize all attributes to default values in the __init__ method:

class Foo:
    def __init__(self, x=None):
        self.x = x
Loading history...
810
        self.col_annotation_widgets_bottom = col_annotation_widgets_bottom
0 ignored issues
show
Coding Style introduced by
The attribute col_annotation_widgets_bottom was defined outside __init__.

It is generally a good practice to initialize all attributes to default values in the __init__ method:

class Foo:
    def __init__(self, x=None):
        self.x = x
Loading history...
811
        self.col_dendrograms = column_dendrograms
0 ignored issues
show
Coding Style introduced by
The attribute col_dendrograms was defined outside __init__.

It is generally a good practice to initialize all attributes to default values in the __init__ method:

class Foo:
    def __init__(self, x=None):
        self.x = x
Loading history...
812
        self.row_dendrograms = row_dendrograms
0 ignored issues
show
Coding Style introduced by
The attribute row_dendrograms was defined outside __init__.

It is generally a good practice to initialize all attributes to default values in the __init__ method:

class Foo:
    def __init__(self, x=None):
        self.x = x
Loading history...
813
814
        self.update_annotations()
815
        self.update_column_annotations()
816
817
        self.__update_size_constraints()
818
819
    def __update_size_constraints(self):
820
        if self.heatmap_scene.widget is not None:
821
            mode = Qt.KeepAspectRatio if self.keep_aspect \
822
                   else Qt.IgnoreAspectRatio
823
            size = QSizeF(self.sceneView.viewport().size())
824
            widget = self.heatmap_scene.widget
0 ignored issues
show
Comprehensibility Bug introduced by
widget is re-defining a name which is already available in the outer-scope (previously defined on line 22).

It is generally a bad practice to shadow variables from the outer-scope. In most cases, this is done unintentionally and might lead to unexpected behavior:

param = 5

class Foo:
    def __init__(self, param):   # "param" would be flagged here
        self.param = param
Loading history...
825
            layout = widget.layout()
826
            if mode == Qt.IgnoreAspectRatio:
827
                # Reset the row height constraints ...
828
                for i, hm_row in enumerate(self.heatmap_widget_grid):
829
                    layout.setRowMaximumHeight(3 + i * 2 + 1, np.finfo(np.float32).max)
830
                    layout.setRowPreferredHeight(3 + i * 2 + 1, 0)
831
                # ... and resize to match the viewport, taking the minimum size
832
                # into account
833
                minsize = widget.minimumSize()
834
                size = size.expandedTo(minsize)
835
                widget.resize(size)
836
            else:
837
                # First set/update the widget's width (the layout will
838
                # distribute the available width to heatmap widgets in
839
                # the grid)
840
                minsize = widget.minimumSize()
841
                widget.resize(size.expandedTo(minsize).width(),
842
                              widget.size().height())
843
                # calculate and set the heatmap row's heights based on
844
                # the width
845
                for i, hm_row in enumerate(self.heatmap_widget_grid):
846
                    heights = []
847
                    for hm in hm_row:
848
                        hm_size = QSizeF(hm.heatmap_item.pixmap().size())
849
                        hm_size = scaled(
850
                            hm_size, QSizeF(hm.size().width(), -1),
851
                            Qt.KeepAspectRatioByExpanding)
852
853
                        heights.append(hm_size.height())
854
                    layout.setRowMaximumHeight(3 + i * 2 + 1, max(heights))
855
                    layout.setRowPreferredHeight(3 + i * 2 + 1, max(heights))
856
857
                # set/update the widget's height
858
                constraint = QSizeF(size.width(), -1)
859
                sh = widget.effectiveSizeHint(Qt.PreferredSize, constraint)
860
                minsize = widget.effectiveSizeHint(Qt.MinimumSize, constraint)
861
                sh = sh.expandedTo(minsize).expandedTo(widget.minimumSize())
862
863
#                 print("Resize 2", sh)
864
#                 print("  old:", widget.size().width(), widget.size().height())
865
#                 print("  new:", widget.size().width(), sh.height())
866
867
                widget.resize(sh)
868
#                 print("Did resize")
869
            self.__fixup_grid_layout()
870
871
    def __fixup_grid_layout(self):
872
        self.__update_margins()
873
        rect = self.scene.widget.geometry()
874
        self.heatmap_scene.setSceneRect(rect)
875
        self.__update_selection_geometry()
876
877
    def __aspect_mode_changed(self):
878
        if self.keep_aspect:
879
            policy = Qt.ScrollBarAlwaysOn
880
        else:
881
            policy = Qt.ScrollBarAlwaysOff
882
883
        viewport = self.sceneView.viewport()
884
        # Temp. remove the event filter so we won't process the resize twice
885
        viewport.removeEventFilter(self)
886
        self.sceneView.setVerticalScrollBarPolicy(policy)
887
        self.sceneView.setHorizontalScrollBarPolicy(policy)
888
        viewport.installEventFilter(self)
889
        self.__update_size_constraints()
890
891
    def eventFilter(self, reciever, event):
892
        if reciever is self.sceneView.viewport() and \
893
                event.type() == QEvent.Resize:
894
            self.__update_size_constraints()
895
896
        return super().eventFilter(reciever, event)
897
898
    def __update_margins(self):
899
        """
900
        Update dendrogram and text list widgets margins to include the
901
        space for average stripe.
902
        """
903
        def offset(hm):
904
            if hm.show_averages:
905
                return hm.averages_item.size().width()
906
            else:
907
                return 0
908
909
        hm_row = self.heatmap_widget_grid[0]
910
        hm_col = next(zip(*self.heatmap_widget_grid))
911
        dendrogram_col = self.col_dendrograms
912
        dendrogram_row = self.row_dendrograms
913
914
        col_annot = zip(self.col_annotation_widgets_top,
915
                        self.col_annotation_widgets_bottom)
916
        row_annot = self.row_annotation_widgets
917
918
        for hm, annot, dendrogram in zip(hm_row, col_annot, dendrogram_col):
919
            width = hm.size().width()
920
            left_offset = offset(hm)
921
            col_count = hm.heatmap_data().shape[1]
922
            half_col = (width - left_offset) / col_count / 2
923
            if dendrogram is not None:
924
                _, top, _, bottom = dendrogram.getContentsMargins()
925
                dendrogram.setContentsMargins(
926
                    left_offset + half_col, top, half_col, bottom)
927
928
            _, top, right, bottom = annot[0].getContentsMargins()
929
            annot[0].setContentsMargins(left_offset, top, right, bottom)
930
            _, top, right, bottom = annot[1].getContentsMargins()
931
            annot[1].setContentsMargins(left_offset, top, right, bottom)
932
933
        for hm, annot, dendrogram in zip(hm_col, row_annot, dendrogram_row):
934
            height = hm.size().height()
935
            row_count = hm.heatmap_data().shape[0]
936
            half_row = height / row_count / 2
937
            if dendrogram is not None:
938
                left, _, right, _ = dendrogram.getContentsMargins()
939
                dendrogram.setContentsMargins(left, half_row, right, half_row)
940
941
    def heatmap_widgets(self):
942
        """Iterate over heatmap widgets.
943
        """
944
        for item in self.heatmap_scene.items():
945
            if isinstance(item, GraphicsHeatmapWidget):
946
                yield item
947
948
    def label_widgets(self):
949
        """Iterate over GraphicsSimpleTextList widgets.
950
        """
951
        for item in self.heatmap_scene.items():
952
            if isinstance(item, GraphicsSimpleTextList):
953
                yield item
954
955
    def dendrogram_widgets(self):
956
        """Iterate over dendrogram widgets
957
        """
958
        for item in self.heatmap_scene.items():
959
            if isinstance(item, DendrogramWidget):
960
                yield item
961
962
    def legend_widgets(self):
963
        for item in self.heatmap_scene.items():
964
            if isinstance(item, GradientLegendWidget):
965
                yield item
966
967
    def update_averages_stripe(self):
968
        """Update the visibility of the averages stripe.
969
        """
970
        if self.data is not None:
971
            for widget in self.heatmap_widgets():
0 ignored issues
show
Comprehensibility Bug introduced by
widget is re-defining a name which is already available in the outer-scope (previously defined on line 22).

It is generally a bad practice to shadow variables from the outer-scope. In most cases, this is done unintentionally and might lead to unexpected behavior:

param = 5

class Foo:
    def __init__(self, param):   # "param" would be flagged here
        self.param = param
Loading history...
972
                widget.set_show_averages(self.averages)
973
                widget.layout().activate()
974
975
            self.scene.widget.layout().activate()
976
            self.__fixup_grid_layout()
977
978
    def update_grid_spacing(self):
979
        """Update layout spacing.
980
        """
981
        if self.scene.widget:
982
            layout = self.scene.widget.layout()
983
            layout.setSpacing(self.SpaceX)
984
            self.__fixup_grid_layout()
985
986
    def update_color_schema(self):
987
        palette = self.color_palette()
988
        for heatmap in self.heatmap_widgets():
989
            heatmap.set_color_table(palette)
990
991
        for legend in self.legend_widgets():
992
            legend.set_color_table(palette)
993
994
    def update_sorting_examples(self):
995
        if self.data:
996
            self.update_heatmaps()
997
998
    def update_sorting_attributes(self):
999
        if self.data:
1000
            self.update_heatmaps()
1001
1002
    def update_legend(self):
1003
        for item in self.heatmap_scene.items():
1004
            if isinstance(item, GradientLegendWidget):
1005
                item.setVisible(self.legend)
1006
1007
    def update_annotations(self):
1008
        if self.data is not None:
1009
            if self.annotation_vars:
1010
                var = self.annotation_vars[self.annotation_index]
1011
                if var == '(None)':
1012
                    var = None
1013
            else:
1014
                var = None
1015
1016
            show = var is not None
1017
            if show:
1018
                annot_col, _ = self.data.get_column_view(var)
1019
            else:
1020
                annot_col = None
1021
1022
            for labelslist in self.row_annotation_widgets:
1023
                labelslist.setVisible(bool(show))
1024
                if show:
1025
                    indices = labelslist._indices
0 ignored issues
show
Coding Style Best Practice introduced by
It seems like _indices was declared protected and should not be accessed from this context.

Prefixing a member variable _ is usually regarded as the equivalent of declaring it with protected visibility that exists in other languages. Consequentially, such a member should only be accessed from the same class or a child class:

class MyParent:
    def __init__(self):
        self._x = 1;
        self.y = 2;

class MyChild(MyParent):
    def some_method(self):
        return self._x    # Ok, since accessed from a child class

class AnotherClass:
    def some_method(self, instance_of_my_child):
        return instance_of_my_child._x   # Would be flagged as AnotherClass is not
                                         # a child class of MyParent
Loading history...
1026
                    data = annot_col[indices]
1027
                    labels = [var.str_val(val) for val in data]
1028
                    labelslist.set_labels(labels)
1029
1030
    def update_column_annotations(self):
1031
        if self.data is not None:
1032
            show_top = self.column_label_pos & OWHeatMap.PositionTop
1033
            show_bottom = self.column_label_pos & OWHeatMap.PositionBottom
1034
1035
            for labelslist in self.col_annotation_widgets_top:
1036
                labelslist.setVisible(show_top)
1037
1038
            TopLabelsRow = 2
1039
            Row0 = 3
1040
            BottomLabelsRow = Row0 + 2 * len(self.heatmapparts.rows)
1041
1042
            layout = self.heatmap_scene.widget.layout()
1043
            layout.setRowMaximumHeight(TopLabelsRow, -1 if show_top else 0)
1044
            layout.setRowSpacing(TopLabelsRow, -1 if show_top else 0)
1045
1046
            for labelslist in self.col_annotation_widgets_bottom:
1047
                labelslist.setVisible(show_bottom)
1048
1049
            layout.setRowMaximumHeight(BottomLabelsRow, -1 if show_top else 0)
1050
1051
            self.__fixup_grid_layout()
1052
1053
    def __select_by_cluster(self, item, dendrogramindex):
1054
        # User clicked on a dendrogram node.
1055
        # Select all rows corresponding to the cluster item.
1056
        node = item.node
1057
        try:
1058
            hm = self.heatmap_widget_grid[dendrogramindex][0]
1059
        except IndexError:
1060
            pass
1061
        else:
1062
            key = QtGui.QApplication.keyboardModifiers()
1063
            clear = not (key & ((Qt.ControlModifier | Qt.ShiftModifier |
1064
                                 Qt.AltModifier)))
1065
            remove = (key & (Qt.ControlModifier | Qt.AltModifier))
1066
            append = (key & Qt.ControlModifier)
1067
            self.selection_manager.selection_add(
1068
                node.value.first, node.value.last - 1, hm,
1069
                clear=clear, remove=remove, append=append)
1070
1071
    def __update_selection_geometry(self):
1072
        for item in self.selection_rects:
1073
            item.setParentItem(None)
1074
            self.heatmap_scene.removeItem(item)
1075
1076
        self.selection_rects = []
1077
        self.selection_manager.update_selection_rects()
1078
        rects = self.selection_manager.selection_rects
1079
        for rect in rects:
1080
            item = QtGui.QGraphicsRectItem(rect, None)
1081
            pen = QPen(Qt.black, 2)
1082
            pen.setCosmetic(True)
1083
            item.setPen(pen)
1084
            self.heatmap_scene.addItem(item)
1085
            self.selection_rects.append(item)
1086
1087
    def on_selection_finished(self):
1088
        self.selected_rows = self.selection_manager.selections
1089
        self.commit()
1090
1091
    def commit(self):
1092
        data = None
1093
        if self.input_data is not None and self.selected_rows:
1094
            sortind = np.hstack([labels._indices for labels in self.row_annotation_widgets])
0 ignored issues
show
Coding Style Best Practice introduced by
It seems like _indices was declared protected and should not be accessed from this context.

Prefixing a member variable _ is usually regarded as the equivalent of declaring it with protected visibility that exists in other languages. Consequentially, such a member should only be accessed from the same class or a child class:

class MyParent:
    def __init__(self):
        self._x = 1;
        self.y = 2;

class MyChild(MyParent):
    def some_method(self):
        return self._x    # Ok, since accessed from a child class

class AnotherClass:
    def some_method(self, instance_of_my_child):
        return instance_of_my_child._x   # Would be flagged as AnotherClass is not
                                         # a child class of MyParent
Loading history...
1095
            indices = sortind[self.selected_rows]
1096
            data = self.input_data[indices]
1097
1098
        self.send("Selected Data", data)
1099
1100
    def save_graph(self):
1101
        from Orange.widgets.data.owsave import OWSave
1102
1103
        save_img = OWSave(parent=self, data=self.scene,
1104
                          file_formats=FileFormats.img_writers)
1105
        save_img.exec_()
1106
1107
1108
class GraphicsWidget(QtGui.QGraphicsWidget):
1109
    """A graphics widget which can notify on relayout events.
1110
    """
1111
    #: The widget's layout has activated (i.e. did a relayout
1112
    #: of the widget's contents)
1113
    layoutDidActivate = Signal()
1114
1115
    def event(self, event):
1116
        rval = super().event(event)
1117
        if event.type() == QEvent.LayoutRequest and self.layout() is not None:
1118
            self.layoutDidActivate.emit()
1119
        return rval
1120
1121
QWIDGETSIZE_MAX = 16777215
1122
1123
1124
def scaled(size, constraint, mode=Qt.KeepAspectRatio):
1125
    if constraint.width() < 0 and constraint.height() < 0:
1126
        return size
1127
1128
    size, constraint = QSizeF(size), QSizeF(constraint)
1129
    if mode == Qt.IgnoreAspectRatio:
1130
        if constraint.width() >= 0:
1131
            size.setWidth(constraint.width())
1132
        if constraint.height() >= 0:
1133
            size.setHeight(constraint.height())
1134
    elif mode == Qt.KeepAspectRatio:
1135
        if constraint.width() < 0:
1136
            constraint.setWidth(QWIDGETSIZE_MAX)
1137
        if constraint.height() < 0:
1138
            constraint.setHeight(QWIDGETSIZE_MAX)
1139
        size.scale(constraint, mode)
1140
    elif mode == Qt.KeepAspectRatioByExpanding:
1141
        if constraint.width() < 0:
1142
            constraint.setWidth(0)
1143
        if constraint.height() < 0:
1144
            constraint.setHeight(0)
1145
        size.scale(constraint, mode)
1146
    return size
1147
1148
1149
class GraphicsPixmapWidget(QtGui.QGraphicsWidget):
1150
    def __init__(self, parent=None, pixmap=None, scaleContents=False,
0 ignored issues
show
Unused Code introduced by
The argument kwargs seems to be unused.
Loading history...
1151
                 aspectMode=Qt.KeepAspectRatio, **kwargs):
1152
        super().__init__(parent)
1153
        self.setContentsMargins(0, 0, 0, 0)
1154
        self.__scaleContents = scaleContents
1155
        self.__aspectMode = aspectMode
1156
1157
        self.__pixmap = pixmap or QPixmap()
1158
        self.__item = QtGui.QGraphicsPixmapItem(self.__pixmap, self)
1159
        self.__updateScale()
1160
1161
    def setPixmap(self, pixmap):
1162
        self.prepareGeometryChange()
1163
        self.__pixmap = pixmap or QPixmap()
1164
        self.__item.setPixmap(self.__pixmap)
1165
        self.updateGeometry()
1166
1167
    def pixmap(self):
1168
        return self.__pixmap
1169
1170
    def setAspectRatioMode(self, mode):
1171
        if self.__aspectMode != mode:
1172
            self.__aspectMode = mode
1173
1174
    def aspectRatioMode(self):
1175
        return self.__aspectMode
1176
1177
    def setScaleContents(self, scale):
1178
        if self.__scaleContents != scale:
1179
            self.__scaleContents = bool(scale)
1180
            self.updateGeometry()
1181
            self.__updateScale()
1182
1183
    def scaleContents(self):
1184
        return self.__scaleContents
1185
1186
    def sizeHint(self, which, constraint=QSizeF()):
1187
        if which == Qt.PreferredSize:
1188
            sh = QSizeF(self.__pixmap.size())
1189
            if self.__scaleContents:
1190
                sh = scaled(sh, constraint, self.__aspectMode)
1191
            return sh
1192
        elif which == Qt.MinimumSize:
1193
            if self.__scaleContents:
1194
                return QSizeF(0, 0)
1195
            else:
1196
                return QSizeF(self.__pixmap.size())
1197
        elif which == Qt.MaximumSize:
1198
            if self.__scaleContents:
1199
                return QSizeF()
1200
            else:
1201
                return QSizeF(self.__pixmap.size())
1202
        else:
1203
            # Qt.MinimumDescent
1204
            return QSizeF()
1205
1206
    def setGeometry(self, rect):
1207
        super().setGeometry(rect)
1208
        crect = self.contentsRect()
1209
        self.__item.setPos(crect.topLeft())
1210
        self.__updateScale()
1211
1212
    def __updateScale(self):
1213
        if self.__pixmap.isNull():
1214
            return
1215
        pxsize = QSizeF(self.__pixmap.size())
1216
        crect = self.contentsRect()
1217
        self.__item.setPos(crect.topLeft())
1218
1219
        if self.__scaleContents:
1220
            csize = scaled(pxsize, crect.size(), self.__aspectMode)
1221
        else:
1222
            csize = pxsize
1223
1224
        xscale = csize.width() / pxsize.width()
1225
        yscale = csize.height() / pxsize.height()
1226
1227
        t = QtGui.QTransform().scale(xscale, yscale)
1228
        self.__item.setTransform(t)
1229
1230
    def pixmapTransform(self):
1231
        return QtGui.QTransform(self.__item.transform())
1232
1233
1234
class GraphicsHeatmapWidget(QtGui.QGraphicsWidget):
1235
    def __init__(self, parent=None, data=None, **kwargs):
1236
        super().__init__(parent, **kwargs)
1237
        self.setAcceptHoverEvents(True)
1238
1239
        self.__levels = None
1240
        self.__colortable = None
1241
        self.__data = data
1242
1243
        self.__pixmap = QPixmap()
1244
        self.__avgpixmap = QPixmap()
1245
1246
        layout = QtGui.QGraphicsLinearLayout(Qt.Horizontal)
1247
        layout.setContentsMargins(0, 0, 0, 0)
1248
        self.heatmap_item = GraphicsPixmapWidget(
1249
            self, scaleContents=True, aspectMode=Qt.IgnoreAspectRatio)
1250
1251
        self.averages_item = GraphicsPixmapWidget(
1252
            self, scaleContents=True, aspectMode=Qt.IgnoreAspectRatio)
1253
1254
        layout.addItem(self.averages_item)
1255
        layout.addItem(self.heatmap_item)
1256
        layout.setItemSpacing(0, 2)
1257
1258
        self.setLayout(layout)
1259
1260
        self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
1261
1262
        self.show_averages = True
1263
1264
        self.set_heatmap_data(data)
1265
1266
    def clear(self):
1267
        """Clear/reset the widget."""
1268
        self.__data = None
1269
        self.__pixmap = None
1270
        self.__avgpixmap = None
1271
1272
        self.heatmap_item.setPixmap(QtGui.QPixmap())
1273
        self.averages_item.setPixmap(QtGui.QPixmap())
1274
        self.show_averages = True
1275
        self.updateGeometry()
1276
        self.layout().invalidate()
1277
1278
    def set_heatmap(self, heatmap):
1279
        """Set the heatmap data for display.
1280
        """
1281
        self.clear()
1282
1283
        self.set_heatmap_data(heatmap)
1284
        self.update()
1285
1286
    def set_heatmap_data(self, data):
1287
        """Set the heatmap data for display."""
1288
        if self.__data is not data:
1289
            self.clear()
1290
            self.__data = data
1291
            self._update_pixmap()
1292
            self.update()
1293
1294
    def heatmap_data(self):
1295
        if self.__data is not None:
1296
            v = self.__data.view()
1297
            v.flags.writeable = False
1298
            return v
1299
        else:
1300
            return None
1301
1302
    def set_levels(self, levels):
1303
        if levels != self.__levels:
1304
            self.__levels = levels
1305
            self._update_pixmap()
1306
            self.update()
1307
1308
    def set_show_averages(self, show):
1309
        if self.show_averages != show:
1310
            self.show_averages = show
1311
            self.averages_item.setVisible(show)
1312
            self.averages_item.setMaximumWidth(-1 if show else 0)
1313
            self.layout().invalidate()
1314
            self.update()
1315
1316
    def set_color_table(self, table):
1317
        self.__colortable = table
1318
        self._update_pixmap()
1319
        self.update()
1320
1321
    def _update_pixmap(self):
1322
        """
1323
        Update the pixmap if its construction arguments changed.
1324
        """
1325
        if self.__data is not None:
1326
            if self.__colortable is not None:
1327
                lut = self.__colortable
1328
            else:
1329
                lut = None
1330
            argb, _ = pg.makeARGB(
1331
                self.__data, lut=lut, levels=self.__levels, scale=250)
1332
            argb[np.isnan(self.__data)] = (100, 100, 100, 255)
1333
1334
            qimage = pg.makeQImage(argb, transpose=False)
1335
            self.__pixmap = QPixmap.fromImage(qimage)
1336
            avg = np.nanmean(self.__data, axis=1, keepdims=True)
1337
            argb, _ = pg.makeARGB(
1338
                avg, lut=lut, levels=self.__levels, scale=250)
1339
            qimage = pg.makeQImage(argb, transpose=False)
1340
            self.__avgpixmap = QPixmap.fromImage(qimage)
1341
        else:
1342
            self.__pixmap = QPixmap()
1343
            self.__avgpixmap = QPixmap()
1344
1345
        self.heatmap_item.setPixmap(self.__pixmap)
1346
        self.averages_item.setPixmap(self.__avgpixmap)
1347
        self.layout().invalidate()
1348
1349
    def cell_at(self, pos):
1350
        """Return the cell row, column from `pos` in local coordinates.
1351
        """
1352
        if self.__pixmap.isNull() or not (
1353
                    self.heatmap_item.geometry().contains(pos) or
1354
                    self.averages_item.geometry().contains(pos)):
1355
            return (-1, -1)
1356
1357
        if self.heatmap_item.geometry().contains(pos):
1358
            item_clicked = self.heatmap_item
1359
        elif self.averages_item.geometry().contains(pos):
1360
            item_clicked = self.averages_item
1361
        pos = self.mapToItem(item_clicked, pos)
1362
        size = self.heatmap_item.size()
1363
1364
        x, y = pos.x(), pos.y()
1365
1366
        N, M = self.__data.shape
1367
        fx = x / size.width()
1368
        fy = y / size.height()
1369
        i = min(int(math.floor(fy * N)), N - 1)
1370
        j = min(int(math.floor(fx * M)), M - 1)
1371
        return i, j
1372
1373
    def cell_rect(self, row, column):
1374
        """Return a rectangle in local coordinates containing the cell
1375
        at `row` and `column`.
1376
        """
1377
        size = self.__pixmap.size()
1378
        if not (0 <= column < size.width() or 0 <= row < size.height()):
1379
            return QRectF()
1380
1381
        topleft = QPointF(column, row)
1382
        bottomright = QPointF(column + 1, row + 1)
1383
        t = self.heatmap_item.pixmapTransform()
1384
        rect = t.mapRect(QRectF(topleft, bottomright))
1385
        rect.translated(self.heatmap_item.pos())
1386
        return rect
1387
1388
    def row_rect(self, row):
1389
        """
1390
        Return a QRectF in local coordinates containing the entire row.
1391
        """
1392
        rect = self.cell_rect(row, 0)
1393
        rect.setLeft(0)
1394
        rect.setRight(self.size().width())
1395
        return rect
1396
1397
    def cell_tool_tip(self, row, column):
1398
        return "{}, {}: {:g}".format(row, column, self.__data[row, column])
1399
1400
    def hoverMoveEvent(self, event):
1401
        pos = event.pos()
1402
        row, column = self.cell_at(pos)
1403
        tooltip = self.cell_tool_tip(row, column)
1404
        # TODO: Move/delegate to (Scene) helpEvent
0 ignored issues
show
Coding Style introduced by
TODO and FIXME comments should generally be avoided.
Loading history...
1405
        self.setToolTip(tooltip)
1406
        return super().hoverMoveEvent(event)
1407
1408
1409
class HeatmapScene(QGraphicsScene):
1410
    """A Graphics Scene with heatmap widgets."""
1411
    def __init__(self, parent=None):
1412
        QGraphicsScene.__init__(self, parent)
1413
        self.selection_manager = HeatmapSelectionManager()
1414
1415
    def set_selection_manager(self, manager):
1416
        self.selection_manager = manager
1417
1418
    def _items(self, pos=None, cls=object):
1419
        if pos is not None:
1420
            items = self.items(QRectF(pos, QSizeF(3, 3)).translated(-1.5, -1.5))
1421
        else:
1422
            items = self.items()
1423
1424
        for item in items:
1425
            if isinstance(item, cls):
1426
                yield item
1427
1428
    def heatmap_at_pos(self, pos):
1429
        items = list(self._items(pos, GraphicsHeatmapWidget))
1430
        if items:
1431
            return items[0]
1432
        else:
1433
            return None
1434
1435
    def dendrogram_at_pos(self, pos):
1436
        return None
1437
1438
        items = list(self._items(pos, DendrogramItem))
0 ignored issues
show
Unused Code introduced by
This code does not seem to be reachable.
Loading history...
1439
        if items:
1440
            return items[0]
1441
        else:
1442
            return None
1443
1444
    def heatmap_widgets(self):
1445
        return self._items(None, GraphicsHeatmapWidget)
1446
1447
    def select_from_dendrogram(self, dendrogram, key):
1448
        """Select all heatmap rows which belong to the dendrogram.
1449
        """
1450
        dendrogram_widget = dendrogram.parentWidget()
1451
        anchors = list(dendrogram_widget.leaf_anchors())
1452
        cluster = dendrogram.cluster
1453
        start, end = anchors[cluster.first], anchors[cluster.last - 1]
1454
        start, end = dendrogram_widget.mapToScene(start), dendrogram_widget.mapToScene(end)
1455
        # Find a heatmap widget containing start and end y coordinates.
1456
1457
        heatmap = None
1458
        for hm in self.heatmap_widgets():
1459
            b_rect = hm.sceneBoundingRect()
1460
            if b_rect.contains(QPointF(b_rect.center().x(), start.y())):
1461
                heatmap = hm
1462
                break
1463
1464
        if dendrogram:
1465
            b_rect = heatmap.boundingRect()
1466
            start, end = heatmap.mapFromScene(start), heatmap.mapFromScene(end)
1467
            start, _ = heatmap.cell_at(QPointF(b_rect.center().x(), start.y()))
1468
            end, _ = heatmap.cell_at(QPointF(b_rect.center().x(), end.y()))
1469
            clear = not (key & ((Qt.ControlModifier | Qt.ShiftModifier |
1470
                                 Qt.AltModifier)))
1471
            remove = (key & (Qt.ControlModifier | Qt.AltModifier))
1472
            append = (key & Qt.ControlModifier)
1473
            self.selection_manager.selection_add(
1474
                start, end, heatmap, clear=clear, remove=remove, append=append)
1475
        return
1476
1477
    def mousePressEvent(self, event):
1478
        pos = event.scenePos()
1479
        heatmap = self.heatmap_at_pos(pos)
1480
        if heatmap and event.button() & Qt.LeftButton:
1481
            row, _ = heatmap.cell_at(heatmap.mapFromScene(pos))
0 ignored issues
show
Unused Code introduced by
The variable row seems to be unused.
Loading history...
1482
            self.selection_manager.selection_start(heatmap, event)
1483
1484
        dendrogram = self.dendrogram_at_pos(pos)
1485
        if dendrogram and event.button() & Qt.LeftButton:
1486
            if dendrogram.orientation == Qt.Vertical:
1487
                self.select_from_dendrogram(dendrogram, event.modifiers())
1488
            return
1489
1490
        return QGraphicsScene.mousePressEvent(self, event)
1491
1492
    def mouseMoveEvent(self, event):
1493
        pos = event.scenePos()
1494
        heatmap = self.heatmap_at_pos(pos)
1495
        if heatmap and event.buttons() & Qt.LeftButton:
1496
            row, _ = heatmap.cell_at(heatmap.mapFromScene(pos))
0 ignored issues
show
Unused Code introduced by
The variable row seems to be unused.
Loading history...
1497
            self.selection_manager.selection_update(heatmap, event)
1498
1499
        dendrogram = self.dendrogram_at_pos(pos)
1500
        if dendrogram and dendrogram.orientation == Qt.Horizontal:  # Filter mouse move events
1501
            return
1502
1503
        return QGraphicsScene.mouseMoveEvent(self, event)
1504
1505
    def mouseReleaseEvent(self, event):
1506
        pos = event.scenePos()
1507
        heatmap = self.heatmap_at_pos(pos)
1508
        if heatmap:
1509
            row, _ = heatmap.cell_at(heatmap.mapFromScene(pos))
0 ignored issues
show
Unused Code introduced by
The variable row seems to be unused.
Loading history...
1510
            self.selection_manager.selection_finish(heatmap, event)
1511
1512
        dendrogram = self.dendrogram_at_pos(pos)
1513
        if dendrogram and dendrogram.orientation == Qt.Horizontal:  # Filter mouse events
1514
            return
1515
1516
        return QGraphicsScene.mouseReleaseEvent(self, event)
1517
1518
    def mouseDoubleClickEvent(self, event):
1519
        pos = event.scenePos()
1520
        dendrogram = self.dendrogram_at_pos(pos)
1521
        if dendrogram:  # Filter mouse events
1522
            return
1523
        return QGraphicsScene.mouseDoubleClickEvent(self, event)
1524
1525
1526
class GraphicsSimpleTextLayoutItem(QtGui.QGraphicsLayoutItem):
1527
    """ A Graphics layout item wrapping a QGraphicsSimpleTextItem alowing it
1528
    to be managed by a layout.
1529
1530
    """
1531
    def __init__(self, text_item, orientation=Qt.Horizontal, parent=None):
1532
        super().__init__(parent)
1533
        self.orientation = orientation
1534
        self.text_item = text_item
1535
        if orientation == Qt.Vertical:
1536
            self.text_item.rotate(-90)
1537
            self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
1538
        else:
1539
            self.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Expanding)
1540
1541
    def setGeometry(self, rect):
1542
        super().setGeometry(rect)
1543
        if self.orientation == Qt.Horizontal:
1544
            self.text_item.setPos(rect.topLeft())
1545
        else:
1546
            self.text_item.setPos(rect.bottomLeft())
1547
1548
    def sizeHint(self, which, constraint=QSizeF()):
0 ignored issues
show
Unused Code introduced by
The argument constraint seems to be unused.
Loading history...
1549
        if which in [Qt.PreferredSize]:
1550
            size = self.text_item.boundingRect().size()
1551
            if self.orientation == Qt.Horizontal:
1552
                return size
1553
            else:
1554
                return QSizeF(size.height(), size.width())
1555
        else:
1556
            return QSizeF()
1557
1558
    def updateGeometry(self):
1559
        super().updateGeometry()
1560
        parent = self.parentLayoutItem()
1561
        if parent.isLayout():
1562
            parent.updateGeometry()
1563
1564
    def setFont(self, font):
1565
        self.text_item.setFont(font)
1566
        self.updateGeometry()
1567
1568
    def setText(self, text):
1569
        self.text_item.setText(text)
1570
        self.updateGeometry()
1571
1572
1573
class GraphicsSimpleTextList(QtGui.QGraphicsWidget):
1574
    """A simple text list widget."""
1575
    def __init__(self, labels=[], orientation=Qt.Vertical, parent=None):
0 ignored issues
show
Bug Best Practice introduced by
The default value [] might cause unintended side-effects.

Objects as default values are only created once in Python and not on each invocation of the function. If the default object is modified, this modification is carried over to the next invocation of the method.

# Bad:
# If array_param is modified inside the function, the next invocation will
# receive the modified object.
def some_function(array_param=[]):
    # ...

# Better: Create an array on each invocation
def some_function(array_param=None):
    array_param = array_param or []
    # ...
Loading history...
1576
        super().__init__(parent)
1577
        self.label_items = []
1578
        self.orientation = orientation
1579
        self.alignment = Qt.AlignCenter
1580
        self.__resize_in_progress = False
1581
1582
        layout = QtGui.QGraphicsLinearLayout(orientation)
1583
        layout.setContentsMargins(0, 0, 0, 0)
1584
        layout.setSpacing(0)
1585
        self.setLayout(layout)
1586
        self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
1587
        self.set_labels(labels)
1588
1589
    def clear(self):
1590
        """Remove all text items."""
1591
        layout = self.layout()
1592
        for i in reversed(range(layout.count())):
1593
            item = layout.itemAt(i)
1594
            item.text_item.setParentItem(None)
1595
            if self.scene():
1596
                self.scene().removeItem(item.text_item)
1597
            layout.removeAt(i)
1598
1599
        self.label_items = []
1600
#         self.updateGeometry()
1601
1602
    def set_labels(self, labels):
1603
        """Set the text labels to show in the widget.
1604
        """
1605
        self.clear()
1606
        orientation = Qt.Horizontal if self.orientation == Qt.Vertical else Qt.Vertical
1607
        for text in labels:
1608
            item = QtGui.QGraphicsSimpleTextItem(text, self)
1609
            item.setFont(self.font())
1610
            item.setToolTip(text)
1611
            item = GraphicsSimpleTextLayoutItem(item, orientation, parent=self)
1612
            self.layout().addItem(item)
1613
            self.layout().setAlignment(item, self.alignment)
1614
            self.label_items.append(item)
1615
1616
    def setAlignment(self, alignment):
1617
        """Set alignment of text items in the widget
1618
        """
1619
        self.alignment = alignment
1620
        layout = self.layout()
1621
        for i in range(layout.count()):
1622
            layout.setAlignment(layout.itemAt(i), alignment)
1623
1624
    def sizeHint(self, which, constraint=QRectF()):
1625
        if not self.isVisible():
1626
            return QSizeF(0, 0)
1627
        else:
1628
            return super().sizeHint(which, constraint)
1629
1630
    def setVisible(self, visible):
1631
        super().setVisible(visible)
1632
        self.updateGeometry()
1633
1634
    def resizeEvent(self, event):
1635
        super().resizeEvent(event)
1636
        self.__resize_in_progress = True
1637
        self._updateFontSize()
1638
        self.__resize_in_progress = False
1639
1640
    def changeEvent(self, event):
1641
        super().changeEvent(event)
1642
        if event.type() == QEvent.FontChange:
1643
            font = self.font()
1644
            for item in self.label_items:
1645
                item.setFont(font)
1646
1647
            if not self.__resize_in_progress:
1648
                self.updateGeometry()
1649
                self.layout().invalidate()
1650
                self.layout().activate()
1651
1652
    def _updateFontSize(self):
1653
        crect = self.contentsRect()
1654
        if self.orientation == Qt.Vertical:
1655
            h = crect.height()
1656
        else:
1657
            h = crect.width()
1658
        n = len(self.label_items)
1659
        if n == 0:
1660
            return
1661
1662
        if self.scene() is not None:
1663
            maxfontsize = self.scene().font().pointSize()
1664
        else:
1665
            maxfontsize = QtGui.QApplication.instance().font().pointSize()
1666
1667
        lineheight = max(1, h / n)
1668
        fontsize = min(self._pointSize(lineheight), maxfontsize)
1669
1670
        font = self.font()
1671
        font.setPointSize(fontsize)
1672
        self.setFont(font)
1673
1674
    def _pointSize(self, height):
1675
        font = self.font()
1676
        font.setPointSize(height)
1677
        fix = 0
1678
        while QFontMetrics(font).lineSpacing() > height and height - fix > 1:
1679
            fix += 1
1680
            font.setPointSize(height - fix)
1681
        return height - fix
1682
1683
1684
class GradientLegendWidget(QtGui.QGraphicsWidget):
1685
    def __init__(self, low, high, parent=None):
1686
        super().__init__(parent)
1687
        self.low = low
1688
        self.high = high
1689
        self.color_table = None
1690
1691
        layout = QtGui.QGraphicsLinearLayout(Qt.Vertical)
1692
        self.setLayout(layout)
1693
        layout.setContentsMargins(0, 0, 0, 0)
1694
        layout.setSpacing(1)
1695
1696
        layout_labels = QtGui.QGraphicsLinearLayout(Qt.Horizontal)
1697
        layout.addItem(layout_labels)
1698
        layout_labels.setContentsMargins(0, 0, 0, 0)
1699
        label_lo = QtGui.QGraphicsSimpleTextItem("%.2f" % low, self)
1700
        label_hi = QtGui.QGraphicsSimpleTextItem("%.2f" % high, self)
1701
        self.item_low = GraphicsSimpleTextLayoutItem(label_lo, parent=self)
1702
        self.item_high = GraphicsSimpleTextLayoutItem(label_hi, parent=self)
1703
1704
        layout_labels.addItem(self.item_low)
1705
        layout_labels.addStretch(10)
1706
        layout_labels.addItem(self.item_high)
1707
1708
        self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
1709
        self.__pixitem = GraphicsPixmapWidget(parent=self, scaleContents=True,
1710
                                              aspectMode=Qt.IgnoreAspectRatio)
1711
        self.__pixitem.setMinimumHeight(12)
1712
        layout.addItem(self.__pixitem)
1713
        self.__update()
1714
1715
    def set_color_table(self, color_table):
1716
        self.color_table = color_table
1717
        self.__update()
1718
1719
    def __update(self):
1720
        data = np.linspace(self.low, self.high, num=50, endpoint=True)
1721
        data = data.reshape((1, -1))
1722
        argb, _ = pg.makeARGB(data, lut=self.color_table,
1723
                              levels=(self.low, self.high))
1724
        qimg = pg.makeQImage(argb, transpose=False)
1725
        self.__pixitem.setPixmap(QPixmap.fromImage(qimg))
1726
1727
        self.item_low.setText("%.2f" % self.low)
1728
        self.item_high.setText("%.2f" % self.high)
1729
        self.layout().invalidate()
1730
1731
1732
class HeatmapSelectionManager(QObject):
1733
    """Selection manager for heatmap rows
1734
    """
1735
    selection_changed = Signal()
1736
    selection_finished = Signal()
1737
1738
    def __init__(self, parent=None):
1739
        QObject.__init__(self, parent)
1740
        self.selections = []
1741
        self.selection_ranges = []
1742
        self.selection_ranges_temp = []
1743
        self.heatmap_widgets = []
1744
        self.selection_rects = []
1745
        self.heatmaps = []
1746
        self._heatmap_ranges = {}
1747
        self._start_row = 0
1748
1749
    def clear(self):
1750
        self.remove_rows(self.selection)
1751
1752
    def set_heatmap_widgets(self, widgets):
1753
        self.remove_rows(self.selections)
1754
        self.heatmaps = list(zip(*widgets))
1755
1756
        # Compute row ranges for all heatmaps
1757
        self._heatmap_ranges = {}
1758
        start = end = 0
1759
1760
        for group in zip(*widgets):
1761
            start = end = 0
1762
            for heatmap in group:
1763
                end += heatmap.heatmap_data().shape[0]
1764
                self._heatmap_ranges[heatmap] = (start, end)
1765
                start = end
1766
1767
    def select_rows(self, rows, heatmap=None, clear=True):
1768
        """Add `rows` to selection. If `heatmap` is provided the rows
1769
        are mapped from the local indices to global heatmap indics. If `clear`
1770
        then remove previous rows.
1771
        """
1772
        if heatmap is not None:
1773
            start, _ = self._heatmap_ranges[heatmap]
1774
            rows = [start + r for r in rows]
1775
1776
        old_selection = list(self.selections)
1777
        if clear:
1778
            self.selections = rows
1779
        else:
1780
            self.selections = sorted(set(self.selections + rows))
1781
1782
        if self.selections != old_selection:
1783
            self.update_selection_rects()
1784
            self.selection_changed.emit()
1785
1786
    def remove_rows(self, rows):
1787
        """Remove `rows` from the selection.
1788
        """
1789
        old_selection = list(self.selections)
1790
        self.selections = sorted(set(self.selections) - set(rows))
1791
        if old_selection != self.selections:
1792
            self.update_selection_rects()
1793
            self.selection_changed.emit()
1794
1795
    def combined_ranges(self, ranges):
0 ignored issues
show
Coding Style introduced by
This method could be written as a function/class method.

If a method does not access any attributes of the class, it could also be implemented as a function or static method. This can help improve readability. For example

class Foo:
    def some_method(self, x, y):
        return x + y;

could be written as

class Foo:
    @classmethod
    def some_method(cls, x, y):
        return x + y;
Loading history...
1796
        combined_ranges = set()
1797
        for start, end in ranges:
1798
            if start <= end:
1799
                rng = range(start, end + 1)
1800
            else:
1801
                rng = range(start, end - 1, -1)
1802
            combined_ranges.update(rng)
1803
        return sorted(combined_ranges)
1804
1805
    def selection_start(self, heatmap_widget, event):
1806
        """ Selection  started by `heatmap_widget` due to `event`.
1807
        """
1808
        pos = heatmap_widget.mapFromScene(event.scenePos())
1809
        row, _ = heatmap_widget.cell_at(pos)
1810
1811
        start, _ = self._heatmap_ranges[heatmap_widget]
1812
        row = start + row
1813
        self._start_row = row
1814
        range = (row, row)
0 ignored issues
show
Bug Best Practice introduced by
This seems to re-define the built-in range.

It is generally discouraged to redefine built-ins as this makes code very hard to read.

Loading history...
1815
        self.selection_ranges_temp = []
1816
        if event.modifiers() & Qt.ControlModifier:
1817
            self.selection_ranges_temp = self.selection_ranges
1818
            self.selection_ranges = self.remove_range(
1819
                self.selection_ranges, row, row, append=True)
1820
        elif event.modifiers() & Qt.ShiftModifier:
1821
            self.selection_ranges.append(range)
1822
        elif event.modifiers() & Qt.AltModifier:
1823
            self.selection_ranges = self.remove_range(
1824
                self.selection_ranges, row, row, append=False)
1825
        else:
1826
            self.selection_ranges = [range]
1827
        self.select_rows(self.combined_ranges(self.selection_ranges))
1828
1829
    def selection_update(self, heatmap_widget, event):
1830
        """ Selection updated by `heatmap_widget due to `event` (mouse drag).
1831
        """
1832
        pos = heatmap_widget.mapFromScene(event.scenePos())
1833
        row, _ = heatmap_widget.cell_at(pos)
1834
        if row < 0:
1835
            return
1836
1837
        start, _ = self._heatmap_ranges[heatmap_widget]
1838
        row = start + row
1839
        if event.modifiers() & Qt.ControlModifier:
1840
            self.selection_ranges = self.remove_range(
1841
                self.selection_ranges_temp, self._start_row, row, append=True)
1842
        elif event.modifiers() & Qt.AltModifier:
1843
            self.selection_ranges = self.remove_range(
1844
                self.selection_ranges, self._start_row, row, append=False)
1845
        else:
1846
            if self.selection_ranges:
1847
                self.selection_ranges[-1] = (self._start_row, row)
1848
            else:
1849
                self.selection_ranges = [(row, row)]
1850
1851
        self.select_rows(self.combined_ranges(self.selection_ranges))
1852
1853
    def selection_finish(self, heatmap_widget, event):
1854
        """ Selection finished by `heatmap_widget due to `event`.
1855
        """
1856
        pos = heatmap_widget.mapFromScene(event.scenePos())
1857
        row, _ = heatmap_widget.cell_at(pos)
1858
        start, _ = self._heatmap_ranges[heatmap_widget]
1859
        row = start + row
1860
        if event.modifiers() & Qt.ControlModifier:
1861
            pass
1862
        elif event.modifiers() & Qt.AltModifier:
1863
            self.selection_ranges = self.remove_range(
1864
                self.selection_ranges, self._start_row, row, append=False)
1865
        else:
1866
            self.selection_ranges[-1] = (self._start_row, row)
1867
        self.select_rows(self.combined_ranges(self.selection_ranges))
1868
        self.selection_finished.emit()
1869
1870
    def selection_add(self, start, end, heatmap=None, clear=True,
1871
                      remove=False, append=False):
1872
        """ Add/remove a selection range from `start` to `end`.
1873
        """
1874
        if heatmap is not None:
1875
            _start, _ = self._heatmap_ranges[heatmap]
1876
            start = _start + start
1877
            end = _start + end
1878
1879
        if clear:
1880
            self.selection_ranges = []
1881
        if remove:
1882
            self.selection_ranges = self.remove_range(
1883
                self.selection_ranges, start, end, append=append)
1884
        else:
1885
            self.selection_ranges.append((start, end))
1886
        self.select_rows(self.combined_ranges(self.selection_ranges))
1887
        self.selection_finished.emit()
1888
1889
    def remove_range(self, ranges, start, end, append=False):
1890
        if start > end:
1891
            start, end = end, start
1892
        comb_ranges = [i for i in self.combined_ranges(ranges)
1893
                       if i > end or i < start]
1894
        if append:
1895
            comb_ranges += [i for i in range(start, end + 1)
1896
                            if i not in self.combined_ranges(ranges)]
1897
            comb_ranges = sorted(comb_ranges)
1898
        return self.combined_to_ranges(comb_ranges)
1899
1900
    def combined_to_ranges(self, comb_ranges):
0 ignored issues
show
Coding Style introduced by
This method could be written as a function/class method.

If a method does not access any attributes of the class, it could also be implemented as a function or static method. This can help improve readability. For example

class Foo:
    def some_method(self, x, y):
        return x + y;

could be written as

class Foo:
    @classmethod
    def some_method(cls, x, y):
        return x + y;
Loading history...
1901
        ranges = []
1902
        if len(comb_ranges) > 0:
1903
            i, start, end = 0, comb_ranges[0], comb_ranges[0]
1904
            for val in comb_ranges[1:]:
1905
                i += 1
1906
                if start + i < val:
1907
                    ranges.append((start, end))
1908
                    i, start = 0, val
1909
                end = val
1910
            ranges.append((start, end))
1911
        return ranges
1912
1913
    def update_selection_rects(self):
1914
        """ Update the selection rects.
1915
        """
1916
        def continuous_ranges(selections):
0 ignored issues
show
Unused Code introduced by
The variable continuous_ranges seems to be unused.
Loading history...
1917
            """ Group continuous ranges
1918
            """
1919
            selections = iter(selections)
1920
            start = end = next(selections)
1921
            try:
1922
                while True:
1923
                    new_end = next(selections)
1924
                    if new_end > end + 1:
1925
                        yield start, end
1926
                        start = end = new_end
1927
                    else:
1928
                        end = new_end
1929
            except StopIteration:
1930
                yield start, end
1931
1932
        def group_selections(selections):
1933
            """Group selections along with heatmaps.
1934
            """
1935
            rows2hm = self.rows_to_heatmaps()
1936
            selections = iter(selections)
1937
            start = end = next(selections)
1938
            end_heatmaps = rows2hm[end]
1939
            try:
1940
                while True:
1941
                    new_end = next(selections)
1942
                    new_end_heatmaps = rows2hm[new_end]
1943
                    if new_end > end + 1 or new_end_heatmaps != end_heatmaps:
1944
                        yield start, end, end_heatmaps
1945
                        start = end = new_end
1946
                        end_heatmaps = new_end_heatmaps
1947
                    else:
1948
                        end = new_end
1949
1950
            except StopIteration:
1951
                yield start, end, end_heatmaps
1952
1953
        def selection_rect(start, end, heatmaps):
1954
            rect = QRectF()
1955
            for heatmap in heatmaps:
1956
                h_start, _ = self._heatmap_ranges[heatmap]
1957
                rect |= heatmap.mapToScene(heatmap.row_rect(start - h_start)).boundingRect()
1958
                rect |= heatmap.mapToScene(heatmap.row_rect(end - h_start)).boundingRect()
1959
            return rect
1960
1961
        self.selection_rects = []
1962
        for start, end, heatmaps in group_selections(self.selections):
1963
            rect = selection_rect(start, end, heatmaps)
1964
            self.selection_rects.append(rect)
1965
1966
    def rows_to_heatmaps(self):
1967
        heatmap_groups = zip(*self.heatmaps)
1968
        rows2hm = {}
1969
        for heatmaps in heatmap_groups:
1970
            hm = heatmaps[0]
1971
            start, end = self._heatmap_ranges[hm]
1972
            rows2hm.update(dict.fromkeys(range(start, end), heatmaps))
1973
        return rows2hm
1974
1975
1976
def test_main(argv=sys.argv):
0 ignored issues
show
Bug Best Practice introduced by
The default value sys.argv (builtins.list) might cause unintended side-effects.

Objects as default values are only created once in Python and not on each invocation of the function. If the default object is modified, this modification is carried over to the next invocation of the method.

# Bad:
# If array_param is modified inside the function, the next invocation will
# receive the modified object.
def some_function(array_param=[]):
    # ...

# Better: Create an array on each invocation
def some_function(array_param=None):
    array_param = array_param or []
    # ...
Loading history...
1977
    if len(argv) > 1:
1978
        filename = argv[1]
1979
    else:
1980
        filename = "brown-selected"
1981
1982
    app = QtGui.QApplication(argv)
1983
    ow = OWHeatMap()
1984
1985
    ow.set_dataset(Orange.data.Table(filename))
1986
    ow.handleNewSignals()
1987
    ow.show()
1988
    ow.raise_()
1989
    app.exec_()
1990
    ow.set_dataset(None)
1991
    ow.handleNewSignals()
1992
    ow.saveSettings()
1993
    return 0
1994
1995
if __name__ == "__main__":
1996
    sys.exit(test_main())
1997