Skip to content

Commit acf1577

Browse files
committed
heatmap: Add column color annotations
1 parent 3ca4a36 commit acf1577

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
@@ -281,7 +281,7 @@ class Position(enum.IntFlag):
281281

282282
# Start row/column where the heatmap items are inserted
283283
# (after the titles/legends/dendrograms)
284-
Row0 = 4
284+
Row0 = 5
285285
Col0 = 3
286286
# The (color) legend row and column
287287
LegendRow, LegendCol = 0, 4
@@ -293,6 +293,8 @@ class Position(enum.IntFlag):
293293
DendrogramRow = 2
294294
# The row for top column annotation labels
295295
TopLabelsRow = 3
296+
# Top color annotation row
297+
TopAnnotationRow = 4
296298
# Vertical split title column
297299
GroupTitleColumn = 0
298300

@@ -315,6 +317,7 @@ def __init__(self, parent=None, **kwargs):
315317
self.col_dendrograms = [] # type: List[Optional[DendrogramWidget]]
316318
self.row_dendrograms = [] # type: List[Optional[DendrogramWidget]]
317319
self.right_side_colors = [] # type: List[Optional[GraphicsPixmapWidget]]
320+
self.top_side_colors = [] # type: List[Optional[GraphicsPixmapWidget]]
318321
self.heatmap_colormap_legend = None
319322
self.bottom_legend_container = None
320323
self.__layout = GridLayout()
@@ -346,6 +349,7 @@ def clear(self):
346349
self.col_dendrograms = []
347350
self.row_dendrograms = []
348351
self.right_side_colors = []
352+
self.top_side_colors = []
349353
self.heatmap_colormap_legend = None
350354
self.bottom_legend_container = None
351355
self.parts = None
@@ -366,12 +370,14 @@ def setHeatmaps(self, parts: 'Parts') -> None:
366370
# The row for the horizontal dendrograms
367371
DendrogramRow = self.DendrogramRow
368372
RightLabelColumn = Col0 + 2 * M + 1
373+
TopAnnotationRow = self.TopAnnotationRow
369374
TopLabelsRow = self.TopLabelsRow
370375
BottomLabelsRow = Row0 + N
371376
colormap = self.__colormap
372377
column_dendrograms: List[Optional[DendrogramWidget]] = [None] * M
373378
row_dendrograms: List[Optional[DendrogramWidget]] = [None] * N
374379
right_side_colors: List[Optional[GraphicsPixmapWidget]] = [None] * N
380+
top_side_colors: List[Optional[GraphicsPixmapWidget]] = [None] * M
375381

376382
data = parts.data
377383
if parts.col_names is None:
@@ -495,8 +501,6 @@ def setHeatmaps(self, parts: 'Parts') -> None:
495501
objectName="row-labels-right"
496502
)
497503
labelslist.setMaximumWidth(300)
498-
pm = QPixmap(1, rowitem.size)
499-
pm.fill(Qt.transparent)
500504
rowauxsidecolor = GraphicsPixmapWidget(
501505
parent=self, visible=False,
502506
scaleContents=True, aspectMode=Qt.IgnoreAspectRatio,
@@ -523,10 +527,20 @@ def setHeatmaps(self, parts: 'Parts') -> None:
523527
visible=self.__columnLabelPosition & Position.Top,
524528
objectName="column-labels-top",
525529
)
530+
colauxsidecolor = GraphicsPixmapWidget(
531+
parent=self, visible=False,
532+
scaleContents=True, aspectMode=Qt.IgnoreAspectRatio,
533+
sizePolicy=QSizePolicy(QSizePolicy.Ignored,
534+
QSizePolicy.Maximum),
535+
minimumSize=QSizeF(-1, 10)
536+
)
537+
526538
grid.addItem(labelslist, TopLabelsRow, Col0 + 2 * j + 1,
527539
Qt.AlignBottom | Qt.AlignLeft)
540+
grid.addItem(colauxsidecolor, TopAnnotationRow, Col0 + 2 * j + 1)
528541
col_annotation_widgets.append(labelslist)
529542
col_annotation_widgets_top.append(labelslist)
543+
top_side_colors[j] = colauxsidecolor
530544

531545
# Bottom attr annotations
532546
labelslist = TextListWidget(
@@ -553,6 +567,16 @@ def setHeatmaps(self, parts: 'Parts') -> None:
553567
self.TopLabelsRow, RightLabelColumn - 1,
554568
)
555569

570+
col_color_annotation_header = QGraphicsSimpleTextItem("", self)
571+
grid.addItem(SimpleLayoutItem(
572+
col_color_annotation_header, anchor=(1, 1), anchorItem=(1, 1),
573+
resizeContents=True,
574+
aspectMode=Qt.KeepAspectRatio,
575+
sizePolicy=QSizePolicy(QSizePolicy.Maximum, QSizePolicy.Preferred),
576+
),
577+
TopAnnotationRow, 0, 1, Col0, alignment=Qt.AlignRight
578+
)
579+
556580
legend = GradientLegendWidget(
557581
parts.span[0], parts.span[1],
558582
colormap,
@@ -563,12 +587,38 @@ def setHeatmaps(self, parts: 'Parts') -> None:
563587
)
564588
legend.setMaximumWidth(300)
565589
grid.addItem(legend, self.LegendRow, self.LegendCol, 1, M * 2)
566-
legend_container = QGraphicsWidget(
567-
visible=False,
568-
sizePolicy=QSizePolicy(QSizePolicy.Maximum, QSizePolicy.Fixed)
590+
591+
def container(parent=None, orientation=Qt.Horizontal, margin=0, spacing=0, **kwargs):
592+
widget = QGraphicsWidget(**kwargs)
593+
layout = QGraphicsLinearLayout(orientation)
594+
layout.setContentsMargins(margin, margin, margin, margin)
595+
layout.setSpacing(spacing)
596+
widget.setLayout(layout)
597+
if parent is not None:
598+
widget.setParentItem(parent)
599+
600+
return widget
601+
# Container for color annotation legends
602+
legend_container = container(
603+
spacing=3,
604+
sizePolicy=QSizePolicy(QSizePolicy.Maximum, QSizePolicy.Fixed),
605+
visible=False, objectName="annotation-legend-container"
606+
)
607+
legend_container_rows = container(
608+
parent=legend_container,
609+
sizePolicy=QSizePolicy(QSizePolicy.Maximum, QSizePolicy.Fixed),
610+
visible=False, objectName="row-annotation-legend-container"
569611
)
570-
legend_container.setLayout(QGraphicsLinearLayout())
571-
legend_container.layout().setContentsMargins(0, 0, 0, 0)
612+
legend_container_cols = container(
613+
parent=legend_container,
614+
sizePolicy=QSizePolicy(QSizePolicy.Maximum, QSizePolicy.Fixed),
615+
visible=False, objectName="col-annotation-legend-container"
616+
)
617+
# ? keep refs to child containers; segfault in scene.clear() ?
618+
legend_container._refs = (legend_container_rows, legend_container_cols)
619+
legend_container.layout().addItem(legend_container_rows)
620+
legend_container.layout().addItem(legend_container_cols)
621+
572622
grid.addItem(legend_container, BottomLabelsRow + 1, Col0 + 1, 1, M * 2 - 1,
573623
alignment=Qt.AlignRight)
574624

@@ -580,6 +630,7 @@ def setHeatmaps(self, parts: 'Parts') -> None:
580630
self.col_dendrograms = column_dendrograms
581631
self.row_dendrograms = row_dendrograms
582632
self.right_side_colors = right_side_colors
633+
self.top_side_colors = top_side_colors
583634
self.heatmap_colormap_legend = legend
584635
self.bottom_legend_container = legend_container
585636
self.parts = parts
@@ -682,50 +733,113 @@ def setRowSideColorAnnotations(
682733
name: str
683734
Name/title for the annotation column.
684735
"""
685-
items = self.right_side_colors
686736
col = self.Col0 + 2 * len(self.parts.columns)
737+
legend_layout = self.bottom_legend_container.layout()
738+
legend_container = legend_layout.itemAt(1)
739+
self.__setColorAnnotationsHelper(
740+
data, colormap, name, self.right_side_colors, col, Qt.Vertical,
741+
legend_container
742+
)
743+
legend_container.setVisible(True)
744+
745+
def setColumnSideColorAnnotations(
746+
self, data: np.ndarray, colormap: ColorMap = None, name=""
747+
):
748+
"""
749+
Set an optional column color annotations.
750+
751+
Parameters
752+
----------
753+
data: Optional[np.ndarray]
754+
A sequence such that it is accepted by `colormap.apply`. If None
755+
then the color annotations are cleared.
756+
colormap: ColorMap
757+
name: str
758+
Name/title for the annotation column.
759+
"""
760+
row = self.TopAnnotationRow
761+
legend_layout = self.bottom_legend_container.layout()
762+
legend_container = legend_layout.itemAt(0)
763+
self.__setColorAnnotationsHelper(
764+
data, colormap, name, self.top_side_colors, row, Qt.Horizontal,
765+
legend_container)
766+
legend_container.setVisible(True)
767+
768+
def __setColorAnnotationsHelper(
769+
self, data: np.ndarray, colormap: ColorMap, name: str,
770+
items: List[GraphicsPixmapWidget], position: int,
771+
orientation: Qt.Orientation, legend_container: QGraphicsWidget):
687772
layout = self.__layout
688-
nameitem = layout.itemAt(self.TopLabelsRow, col)
689-
width = QFontMetrics(self.font()).lineSpacing()
690-
legend_container = self.bottom_legend_container
773+
if orientation == Qt.Horizontal:
774+
nameitem = layout.itemAt(position, 0)
775+
else:
776+
nameitem = layout.itemAt(self.TopLabelsRow, position)
777+
size = QFontMetrics(self.font()).lineSpacing()
691778
layout_clear(legend_container.layout())
692779

780+
def grid_set_maximum_size(position: int, size: float):
781+
if orientation == Qt.Horizontal:
782+
layout.setRowMaximumHeight(position, size)
783+
else:
784+
layout.setColumnMaximumWidth(position, size)
785+
786+
def set_minimum_size(item: QGraphicsLayoutItem, size: float):
787+
if orientation == Qt.Horizontal:
788+
item.setMinimumHeight(size)
789+
else:
790+
item.setMinimumWidth(size)
791+
item.updateGeometry()
792+
793+
def reset_minimum_size(item: QGraphicsLayoutItem):
794+
set_minimum_size(item, -1)
795+
693796
def set_hidden(item: GraphicsPixmapWidget):
694797
item.setVisible(False)
695-
item.setMinimumWidth(-1)
696-
item.updateGeometry()
798+
reset_minimum_size(item,)
697799

698800
def set_visible(item: GraphicsPixmapWidget):
699801
item.setVisible(True)
700-
item.setMinimumWidth(10)
802+
set_minimum_size(item, 10)
803+
804+
def set_preferred_size(item, size):
805+
if orientation == Qt.Horizontal:
806+
item.setPreferredHeight(size)
807+
else:
808+
item.setPreferredWidth(size)
701809
item.updateGeometry()
702810

703811
if data is None:
704812
apply_all(filter(None, items), set_hidden)
705-
layout.setColumnMaximumWidth(col, 0)
813+
grid_set_maximum_size(position, 0)
814+
706815
nameitem.item.setVisible(False)
707816
nameitem.updateGeometry()
708817
legend_container.setVisible(False)
709818
return
710819
else:
711820
apply_all(filter(None, items), set_visible)
712-
layout.setColumnMaximumWidth(col, FLT_MAX)
821+
grid_set_maximum_size(position, FLT_MAX)
713822
legend_container.setVisible(True)
714823

715-
parts = self.parts.rows
824+
if orientation == Qt.Horizontal:
825+
parts = self.parts.columns
826+
else:
827+
parts = self.parts.rows
716828
for p, item in zip(parts, items):
717829
if item is not None:
718830
subset = data[p.normalized_indices]
719831
subset = colormap.apply(subset)
720-
img = qimage_from_array(subset.reshape((-1, 1, subset.shape[-1])))
832+
rgbdata = subset.reshape((-1, 1, subset.shape[-1]))
833+
if orientation == Qt.Horizontal:
834+
rgbdata = rgbdata.reshape((1, -1, rgbdata.shape[-1]))
835+
img = qimage_from_array(rgbdata)
721836
item.setPixmap(img)
722837
item.setVisible(True)
723-
item.setPreferredWidth(width)
838+
set_preferred_size(item, size)
724839

725840
nameitem.item.setText(name)
726841
nameitem.item.setVisible(True)
727-
nameitem.setPreferredWidth(width)
728-
nameitem.updateGeometry()
842+
set_preferred_size(nameitem, size)
729843

730844
container = legend_container.layout()
731845
if isinstance(colormap, CategoricalColorMap):

0 commit comments

Comments
 (0)