1
|
|
|
import sys |
2
|
|
|
import math |
3
|
|
|
from collections import defaultdict |
4
|
|
|
from types import SimpleNamespace as namespace |
5
|
|
|
|
6
|
|
|
import numpy as np |
|
|
|
|
7
|
|
|
|
8
|
|
|
from PyQt4 import QtGui |
|
|
|
|
9
|
|
|
from PyQt4.QtGui import ( |
|
|
|
|
10
|
|
|
QSizePolicy, QGraphicsScene, QGraphicsView, QFontMetrics, |
11
|
|
|
QPen, QPixmap, QColor |
12
|
|
|
) |
13
|
|
|
from PyQt4.QtCore import Qt, QSize, QPointF, QSizeF, QRectF, QObject, QEvent |
|
|
|
|
14
|
|
|
from PyQt4.QtCore import pyqtSignal as Signal |
|
|
|
|
15
|
|
|
import pyqtgraph as pg |
|
|
|
|
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)) |
|
|
|
|
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) |
|
|
|
|
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): |
|
|
|
|
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, |
|
|
|
|
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: |
|
|
|
|
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. |
|
|
|
|
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): |
|
|
|
|
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 = [] |
|
|
|
|
382
|
|
|
self.col_annotation_widgets_bottom = [] |
|
|
|
|
383
|
|
|
self.col_annotation_widgets_top = [] |
|
|
|
|
384
|
|
|
self.row_annotation_widgets = [] |
|
|
|
|
385
|
|
|
self.col_dendrograms = [] |
|
|
|
|
386
|
|
|
self.row_dendrograms = [] |
|
|
|
|
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): |
|
|
|
|
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): |
|
|
|
|
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) |
|
|
|
|
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): |
|
|
|
|
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) |
|
|
|
|
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 |
|
|
|
|
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() |
|
|
|
|
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 |
|
|
|
|
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 |
|
|
|
|
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 |
|
|
|
|
808
|
|
|
self.col_annotation_widgets = col_annotation_widgets |
|
|
|
|
809
|
|
|
self.col_annotation_widgets_top = col_annotation_widgets_top |
|
|
|
|
810
|
|
|
self.col_annotation_widgets_bottom = col_annotation_widgets_bottom |
|
|
|
|
811
|
|
|
self.col_dendrograms = column_dendrograms |
|
|
|
|
812
|
|
|
self.row_dendrograms = row_dendrograms |
|
|
|
|
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 |
|
|
|
|
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(): |
|
|
|
|
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 |
|
|
|
|
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]) |
|
|
|
|
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, |
|
|
|
|
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 |
|
|
|
|
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)) |
|
|
|
|
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)) |
|
|
|
|
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)) |
|
|
|
|
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)) |
|
|
|
|
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()): |
|
|
|
|
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): |
|
|
|
|
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): |
|
|
|
|
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) |
|
|
|
|
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): |
|
|
|
|
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): |
|
|
|
|
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): |
|
|
|
|
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
|
|
|
|
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.
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.