From 82b259828ad7543f8bb84a3beac01643b01f624b Mon Sep 17 00:00:00 2001 From: janezd Date: Fri, 13 Sep 2024 20:24:04 +0200 Subject: [PATCH] Feature Statistics: Add legend --- Orange/widgets/data/owfeaturestatistics.py | 55 ++++++++++++++++++- .../data/tests/test_owfeaturestatistics.py | 33 +++++++++++ 2 files changed, 86 insertions(+), 2 deletions(-) diff --git a/Orange/widgets/data/owfeaturestatistics.py b/Orange/widgets/data/owfeaturestatistics.py index 2a133b4206c..1f74165b653 100644 --- a/Orange/widgets/data/owfeaturestatistics.py +++ b/Orange/widgets/data/owfeaturestatistics.py @@ -16,9 +16,10 @@ import scipy.sparse as sp from AnyQt.QtCore import Qt, QSize, QRectF, QModelIndex, pyqtSlot, \ QItemSelection, QItemSelectionRange, QItemSelectionModel -from AnyQt.QtGui import QPainter, QColor, QPalette +from AnyQt.QtGui import QPainter, QColor, QPalette, QFontMetrics from AnyQt.QtWidgets import QStyledItemDelegate, QGraphicsScene, QTableView, \ - QHeaderView, QStyle, QStyleOptionViewItem + QHeaderView, QStyle, QStyleOptionViewItem, \ + QGraphicsView, QGraphicsItemGroup import Orange.statistics.util as ut from Orange.data import Table, StringVariable, DiscreteVariable, \ @@ -31,6 +32,8 @@ from Orange.widgets.utils.itemmodels import DomainModel, AbstractSortTableModel from Orange.widgets.utils.signals import Input, Output from Orange.widgets.utils.widgetpreview import WidgetPreview +from Orange.widgets.visualize.utils import CanvasRectangle, CanvasText +from Orange.widgets.visualize.utils.plotutils import wrap_legend_items def _categorical_entropy(x): @@ -773,6 +776,15 @@ def __init__(self): box.layout().addWidget(self.table_view) + self.legend_items = [] + self.legend = QGraphicsScene() + self.legend_view = u = QGraphicsView(self.legend) + u.setRenderHints(QPainter.Antialiasing | QPainter.TextAntialiasing) + u.setFrameStyle(QGraphicsView.NoFrame) + u.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) + u.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff) + box.layout().addWidget(u) + self.color_var_model = DomainModel( valid_types=(ContinuousVariable, DiscreteVariable), placeholder='None', @@ -791,6 +803,10 @@ def __init__(self): def sizeHint(): # pylint: disable=arguments-differ return QSize(1050, 500) + def resizeEvent(self, event): + super().resizeEvent(event) + self.update_legend() + @Inputs.data def set_data(self, data): # Clear outputs and reset widget state @@ -853,6 +869,41 @@ def on_header_click(self, *_): def __color_var_changed(self, *_): if self.model is not None: self.model.set_target_var(self.color_var) + self.update_legend_items() + self.update_legend() + + def update_legend_items(self): + self.legend.clear() + if self.color_var is None or not self.color_var.is_discrete: + self.legend_items = [] + return + self.legend_items = [] + size = QFontMetrics(self.font()).height() + for name, color in zip(self.color_var.values, self.color_var.palette.qcolors): + item = QGraphicsItemGroup() + item.addToGroup( + CanvasRectangle(None, -size / 2, -size / 2, size, size, + Qt.gray, color)) + item.addToGroup( + CanvasText(None, name, size, 0, Qt.AlignVCenter)) + self.legend_items.append(item) + + def update_legend(self): + view = self.legend_view + + if not self.legend_items: + self.legend.clear() + view.hide() + return + + size = QFontMetrics(self.font()).height() + legend = wrap_legend_items( + self.legend_items, + self.width() - 30, size, size * 1.75) + self.legend.addItem(legend) + legend.setPos(15, 0) + view.setFixedHeight(int(legend.boundingRect().height()) + size) + view.show() def on_select(self): selection_indices = list(self.model.mapToSourceRows([ diff --git a/Orange/widgets/data/tests/test_owfeaturestatistics.py b/Orange/widgets/data/tests/test_owfeaturestatistics.py index 547cd2f91e3..7ff76a158f4 100644 --- a/Orange/widgets/data/tests/test_owfeaturestatistics.py +++ b/Orange/widgets/data/tests/test_owfeaturestatistics.py @@ -572,6 +572,39 @@ def test_report(self): self.assertIn("", report_text) self.assertEqual(6, report_text.count("")) # header + 5 rows + def test_color_legend(self): + w = self.widget + data = Table("heart_disease") + self.send_signal(self.widget.Inputs.data, data) + + self.assertIs(w.color_var, data.domain.class_var) + self.assertEqual(len(w.legend_items), 2) + self.assertFalse(w.legend_view.isHidden()) + + w.cb_color_var.setCurrentIndex(4) # age (numeric, no legend) + w.cb_color_var.activated.emit(4) + self.assertEqual(len(w.legend_items), 0) + self.assertTrue(w.legend_view.isHidden()) + + w.cb_color_var.setCurrentIndex(6) # chest pain + w.cb_color_var.activated.emit(6) + self.assertEqual(len(w.legend_items), 4) + self.assertFalse(w.legend_view.isHidden()) + + w.cb_color_var.setCurrentIndex(0) # None + w.cb_color_var.activated.emit(0) + self.assertEqual(len(w.legend_items), 0) + self.assertTrue(w.legend_view.isHidden()) + + # Show + w.cb_color_var.setCurrentIndex(6) # chest pain + w.cb_color_var.activated.emit(6) + + # to check that the legend is hidden when the data is removed + self.send_signal(self.widget.Inputs.data, None) + self.assertEqual(len(w.legend_items), 0) + self.assertTrue(w.legend_view.isHidden()) + class TestSummary(WidgetTest): def setUp(self):