Skip to content

Commit a5af7de

Browse files
committed
heatmap: Add column color annotations
1 parent edfe697 commit a5af7de

File tree

1 file changed

+136
-22
lines changed

1 file changed

+136
-22
lines changed

Orange/widgets/visualize/utils/heatmap.py

Lines changed: 136 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -290,7 +290,7 @@ class Position(enum.IntFlag):
290290

291291
# Start row/column where the heatmap items are inserted
292292
# (after the titles/legends/dendrograms)
293-
Row0 = 4
293+
Row0 = 5
294294
Col0 = 3
295295
# The (color) legend row and column
296296
LegendRow, LegendCol = 0, 4
@@ -302,6 +302,8 @@ class Position(enum.IntFlag):
302302
DendrogramRow = 2
303303
# The row for top column annotation labels
304304
TopLabelsRow = 3
305+
# Top color annotation row
306+
TopAnnotationRow = 4
305307
# Vertical split title column
306308
GroupTitleColumn = 0
307309

@@ -324,6 +326,7 @@ def __init__(self, parent=None, **kwargs):
324326
self.col_dendrograms = [] # type: List[Optional[DendrogramWidget]]
325327
self.row_dendrograms = [] # type: List[Optional[DendrogramWidget]]
326328
self.right_side_colors = [] # type: List[Optional[GraphicsPixmapWidget]]
329+
self.top_side_colors = [] # type: List[Optional[GraphicsPixmapWidget]]
327330
self.heatmap_colormap_legend = None
328331
self.bottom_legend_container = None
329332
self.__layout = GridLayout()
@@ -355,6 +358,7 @@ def clear(self):
355358
self.col_dendrograms = []
356359
self.row_dendrograms = []
357360
self.right_side_colors = []
361+
self.top_side_colors = []
358362
self.heatmap_colormap_legend = None
359363
self.bottom_legend_container = None
360364
self.parts = None
@@ -375,12 +379,14 @@ def setHeatmaps(self, parts: 'Parts') -> None:
375379
# The row for the horizontal dendrograms
376380
DendrogramRow = self.DendrogramRow
377381
RightLabelColumn = Col0 + 2 * M + 1
382+
TopAnnotationRow = self.TopAnnotationRow
378383
TopLabelsRow = self.TopLabelsRow
379384
BottomLabelsRow = Row0 + N
380385
colormap = self.__colormap
381386
column_dendrograms: List[Optional[DendrogramWidget]] = [None] * M
382387
row_dendrograms: List[Optional[DendrogramWidget]] = [None] * N
383388
right_side_colors: List[Optional[GraphicsPixmapWidget]] = [None] * N
389+
top_side_colors: List[Optional[GraphicsPixmapWidget]] = [None] * M
384390

385391
data = parts.data
386392
if parts.col_names is None:
@@ -504,8 +510,6 @@ def setHeatmaps(self, parts: 'Parts') -> None:
504510
objectName="row-labels-right"
505511
)
506512
labelslist.setMaximumWidth(300)
507-
pm = QPixmap(1, rowitem.size)
508-
pm.fill(Qt.transparent)
509513
rowauxsidecolor = GraphicsPixmapWidget(
510514
parent=self, visible=False,
511515
scaleContents=True, aspectMode=Qt.IgnoreAspectRatio,
@@ -532,10 +536,20 @@ def setHeatmaps(self, parts: 'Parts') -> None:
532536
visible=self.__columnLabelPosition & Position.Top,
533537
objectName="column-labels-top",
534538
)
539+
colauxsidecolor = GraphicsPixmapWidget(
540+
parent=self, visible=False,
541+
scaleContents=True, aspectMode=Qt.IgnoreAspectRatio,
542+
sizePolicy=QSizePolicy(QSizePolicy.Ignored,
543+
QSizePolicy.Maximum),
544+
minimumSize=QSizeF(-1, 10)
545+
)
546+
535547
grid.addItem(labelslist, TopLabelsRow, Col0 + 2 * j + 1,
536548
Qt.AlignBottom | Qt.AlignLeft)
549+
grid.addItem(colauxsidecolor, TopAnnotationRow, Col0 + 2 * j + 1)
537550
col_annotation_widgets.append(labelslist)
538551
col_annotation_widgets_top.append(labelslist)
552+
top_side_colors[j] = colauxsidecolor
539553

540554
# Bottom attr annotations
541555
labelslist = TextListWidget(
@@ -562,6 +576,16 @@ def setHeatmaps(self, parts: 'Parts') -> None:
562576
self.TopLabelsRow, RightLabelColumn - 1,
563577
)
564578

579+
col_color_annotation_header = QGraphicsSimpleTextItem("", self)
580+
grid.addItem(SimpleLayoutItem(
581+
col_color_annotation_header, anchor=(1, 1), anchorItem=(1, 1),
582+
resizeContents=True,
583+
aspectMode=Qt.KeepAspectRatio,
584+
sizePolicy=QSizePolicy(QSizePolicy.Maximum, QSizePolicy.Preferred),
585+
),
586+
TopAnnotationRow, 0, 1, Col0, alignment=Qt.AlignRight
587+
)
588+
565589
legend = GradientLegendWidget(
566590
parts.span[0], parts.span[1],
567591
colormap,
@@ -572,12 +596,38 @@ def setHeatmaps(self, parts: 'Parts') -> None:
572596
)
573597
legend.setMaximumWidth(300)
574598
grid.addItem(legend, self.LegendRow, self.LegendCol, 1, M * 2)
575-
legend_container = QGraphicsWidget(
576-
visible=False,
577-
sizePolicy=QSizePolicy(QSizePolicy.Maximum, QSizePolicy.Fixed)
599+
600+
def container(parent=None, orientation=Qt.Horizontal, margin=0, spacing=0, **kwargs):
601+
widget = QGraphicsWidget(**kwargs)
602+
layout = QGraphicsLinearLayout(orientation)
603+
layout.setContentsMargins(margin, margin, margin, margin)
604+
layout.setSpacing(spacing)
605+
widget.setLayout(layout)
606+
if parent is not None:
607+
widget.setParentItem(parent)
608+
609+
return widget
610+
# Container for color annotation legends
611+
legend_container = container(
612+
spacing=3,
613+
sizePolicy=QSizePolicy(QSizePolicy.Maximum, QSizePolicy.Fixed),
614+
visible=False, objectName="annotation-legend-container"
615+
)
616+
legend_container_rows = container(
617+
parent=legend_container,
618+
sizePolicy=QSizePolicy(QSizePolicy.Maximum, QSizePolicy.Fixed),
619+
visible=False, objectName="row-annotation-legend-container"
578620
)
579-
legend_container.setLayout(QGraphicsLinearLayout())
580-
legend_container.layout().setContentsMargins(0, 0, 0, 0)
621+
legend_container_cols = container(
622+
parent=legend_container,
623+
sizePolicy=QSizePolicy(QSizePolicy.Maximum, QSizePolicy.Fixed),
624+
visible=False, objectName="col-annotation-legend-container"
625+
)
626+
# ? keep refs to child containers; segfault in scene.clear() ?
627+
legend_container._refs = (legend_container_rows, legend_container_cols)
628+
legend_container.layout().addItem(legend_container_rows)
629+
legend_container.layout().addItem(legend_container_cols)
630+
581631
grid.addItem(legend_container, BottomLabelsRow + 1, Col0 + 1, 1, M * 2 - 1,
582632
alignment=Qt.AlignRight)
583633

@@ -589,6 +639,7 @@ def setHeatmaps(self, parts: 'Parts') -> None:
589639
self.col_dendrograms = column_dendrograms
590640
self.row_dendrograms = row_dendrograms
591641
self.right_side_colors = right_side_colors
642+
self.top_side_colors = top_side_colors
592643
self.heatmap_colormap_legend = legend
593644
self.bottom_legend_container = legend_container
594645
self.parts = parts
@@ -691,50 +742,113 @@ def setRowSideColorAnnotations(
691742
name: str
692743
Name/title for the annotation column.
693744
"""
694-
items = self.right_side_colors
695745
col = self.Col0 + 2 * len(self.parts.columns)
746+
legend_layout = self.bottom_legend_container.layout()
747+
legend_container = legend_layout.itemAt(1)
748+
self.__setColorAnnotationsHelper(
749+
data, colormap, name, self.right_side_colors, col, Qt.Vertical,
750+
legend_container
751+
)
752+
legend_container.setVisible(True)
753+
754+
def setColumnSideColorAnnotations(
755+
self, data: np.ndarray, colormap: ColorMap = None, name=""
756+
):
757+
"""
758+
Set an optional column color annotations.
759+
760+
Parameters
761+
----------
762+
data: Optional[np.ndarray]
763+
A sequence such that it is accepted by `colormap.apply`. If None
764+
then the color annotations are cleared.
765+
colormap: ColorMap
766+
name: str
767+
Name/title for the annotation column.
768+
"""
769+
row = self.TopAnnotationRow
770+
legend_layout = self.bottom_legend_container.layout()
771+
legend_container = legend_layout.itemAt(0)
772+
self.__setColorAnnotationsHelper(
773+
data, colormap, name, self.top_side_colors, row, Qt.Horizontal,
774+
legend_container)
775+
legend_container.setVisible(True)
776+
777+
def __setColorAnnotationsHelper(
778+
self, data: np.ndarray, colormap: ColorMap, name: str,
779+
items: List[GraphicsPixmapWidget], position: int,
780+
orientation: Qt.Orientation, legend_container: QGraphicsWidget):
696781
layout = self.__layout
697-
nameitem = layout.itemAt(self.TopLabelsRow, col)
698-
width = QFontMetrics(self.font()).lineSpacing()
699-
legend_container = self.bottom_legend_container
782+
if orientation == Qt.Horizontal:
783+
nameitem = layout.itemAt(position, 0)
784+
else:
785+
nameitem = layout.itemAt(self.TopLabelsRow, position)
786+
size = QFontMetrics(self.font()).lineSpacing()
700787
layout_clear(legend_container.layout())
701788

789+
def grid_set_maximum_size(position: int, size: float):
790+
if orientation == Qt.Horizontal:
791+
layout.setRowMaximumHeight(position, size)
792+
else:
793+
layout.setColumnMaximumWidth(position, size)
794+
795+
def set_minimum_size(item: QGraphicsLayoutItem, size: float):
796+
if orientation == Qt.Horizontal:
797+
item.setMinimumHeight(size)
798+
else:
799+
item.setMinimumWidth(size)
800+
item.updateGeometry()
801+
802+
def reset_minimum_size(item: QGraphicsLayoutItem):
803+
set_minimum_size(item, -1)
804+
702805
def set_hidden(item: GraphicsPixmapWidget):
703806
item.setVisible(False)
704-
item.setMinimumWidth(-1)
705-
item.updateGeometry()
807+
reset_minimum_size(item,)
706808

707809
def set_visible(item: GraphicsPixmapWidget):
708810
item.setVisible(True)
709-
item.setMinimumWidth(10)
811+
set_minimum_size(item, 10)
812+
813+
def set_preferred_size(item, size):
814+
if orientation == Qt.Horizontal:
815+
item.setPreferredHeight(size)
816+
else:
817+
item.setPreferredWidth(size)
710818
item.updateGeometry()
711819

712820
if data is None:
713821
apply_all(filter(None, items), set_hidden)
714-
layout.setColumnMaximumWidth(col, 0)
822+
grid_set_maximum_size(position, 0)
823+
715824
nameitem.item.setVisible(False)
716825
nameitem.updateGeometry()
717826
legend_container.setVisible(False)
718827
return
719828
else:
720829
apply_all(filter(None, items), set_visible)
721-
layout.setColumnMaximumWidth(col, FLT_MAX)
830+
grid_set_maximum_size(position, FLT_MAX)
722831
legend_container.setVisible(True)
723832

724-
parts = self.parts.rows
833+
if orientation == Qt.Horizontal:
834+
parts = self.parts.columns
835+
else:
836+
parts = self.parts.rows
725837
for p, item in zip(parts, items):
726838
if item is not None:
727839
subset = data[p.normalized_indices]
728840
subset = colormap.apply(subset)
729-
img = qimage_from_array(subset.reshape((-1, 1, subset.shape[-1])))
841+
rgbdata = subset.reshape((-1, 1, subset.shape[-1]))
842+
if orientation == Qt.Horizontal:
843+
rgbdata = rgbdata.reshape((1, -1, rgbdata.shape[-1]))
844+
img = qimage_from_array(rgbdata)
730845
item.setPixmap(img)
731846
item.setVisible(True)
732-
item.setPreferredWidth(width)
847+
set_preferred_size(item, size)
733848

734849
nameitem.item.setText(name)
735850
nameitem.item.setVisible(True)
736-
nameitem.setPreferredWidth(width)
737-
nameitem.updateGeometry()
851+
set_preferred_size(nameitem, size)
738852

739853
container = legend_container.layout()
740854
if isinstance(colormap, CategoricalColorMap):

0 commit comments

Comments
 (0)