Skip to content

Commit da254c7

Browse files
committed
Data info displayed in the status bar
1 parent 7dfcb8b commit da254c7

File tree

2 files changed

+44
-67
lines changed

2 files changed

+44
-67
lines changed

Orange/widgets/data/owfeaturestatistics.py

Lines changed: 10 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,11 @@
2525
ContinuousVariable, TimeVariable, Domain, Variable
2626
from Orange.widgets import widget, gui
2727
from Orange.widgets.data.utils.histogram import Histogram
28-
from Orange.widgets.report import plural
2928
from Orange.widgets.settings import ContextSetting, DomainContextHandler
3029
from Orange.widgets.utils.itemmodels import DomainModel, AbstractSortTableModel
3130
from Orange.widgets.utils.signals import Input, Output
3231
from Orange.widgets.utils.widgetpreview import WidgetPreview
32+
from Orange.widgets.utils.state_summary import format_summary_details
3333

3434

3535
def _categorical_entropy(x):
@@ -723,15 +723,6 @@ def __init__(self):
723723

724724
self.data = None # type: Optional[Table]
725725

726-
# Information panel
727-
info_box = gui.vBox(self.controlArea, 'Info')
728-
info_box.setMinimumWidth(200)
729-
self.info_summary = gui.widgetLabel(info_box, wordWrap=True)
730-
self.info_attr = gui.widgetLabel(info_box, wordWrap=True)
731-
self.info_class = gui.widgetLabel(info_box, wordWrap=True)
732-
self.info_meta = gui.widgetLabel(info_box, wordWrap=True)
733-
self.set_info()
734-
735726
# TODO: Implement filtering on the model
736727
# filter_box = gui.vBox(self.controlArea, 'Filter')
737728
# self.filter_text = gui.lineEdit(
@@ -756,6 +747,9 @@ def __init__(self):
756747
gui.rubber(self.controlArea)
757748
gui.auto_send(self.buttonsArea, self, "auto_commit")
758749

750+
self.info.set_input_summary(self.info.NoInput)
751+
self.info.set_output_summary(self.info.NoOutput)
752+
759753
# Main area
760754
self.model = FeatureStatisticsTableModel(parent=self)
761755
self.table_view = FeatureStatisticsTableView(self.model, parent=self)
@@ -792,11 +786,14 @@ def set_data(self, data):
792786
self.data = data
793787

794788
if data is not None:
789+
self.info.set_input_summary(len(data),
790+
format_summary_details(data))
795791
self.color_var_model.set_domain(data.domain)
796792
self.color_var = None
797793
if self.data.domain.class_vars:
798794
self.color_var = self.data.domain.class_vars[0]
799795
else:
796+
self.info.set_input_summary(self.info.NoInput)
800797
self.color_var_model.set_domain(None)
801798
self.color_var = None
802799
self.model.set_data(data)
@@ -807,7 +804,6 @@ def set_data(self, data):
807804
# self._filter_table_variables()
808805
self.__color_var_changed()
809806

810-
self.set_info()
811807
self.commit()
812808

813809
def __restore_selection(self):
@@ -843,62 +839,6 @@ def __color_var_changed(self, *_):
843839
if self.model is not None:
844840
self.model.set_target_var(self.color_var)
845841

846-
def _format_variables_string(self, variables):
847-
agg = []
848-
for var_type_name, var_type in [
849-
('categorical', DiscreteVariable),
850-
('numeric', ContinuousVariable),
851-
('time', TimeVariable),
852-
('string', StringVariable)
853-
]:
854-
# Disable pylint here because a `TimeVariable` is also a
855-
# `ContinuousVariable`, and should be labelled as such. That is why
856-
# it is necessary to check the type this way instead of using
857-
# `isinstance`, which would fail in the above case
858-
var_type_list = [v for v in variables if type(v) is var_type] # pylint: disable=unidiomatic-typecheck
859-
if var_type_list:
860-
shown = var_type in self.model.HIDDEN_VAR_TYPES
861-
agg.append((
862-
'%d %s%s' % (len(var_type_list), var_type_name, ['', ' (not shown)'][shown]),
863-
len(var_type_list)
864-
))
865-
866-
if not agg:
867-
return 'No variables'
868-
869-
attrs, counts = list(zip(*agg))
870-
if len(attrs) > 1:
871-
var_string = ', '.join(attrs[:-1]) + ' and ' + attrs[-1]
872-
else:
873-
var_string = attrs[0]
874-
return plural('%s variable{s}' % var_string, sum(counts))
875-
876-
def set_info(self):
877-
if self.data is not None:
878-
self.info_summary.setText('<b>%s</b> contains %s with %s' % (
879-
self.data.name,
880-
plural('{number} instance{s}', self.model.n_instances),
881-
plural('{number} feature{s}', self.model.n_attributes)
882-
))
883-
884-
self.info_attr.setText(
885-
'<b>Attributes:</b><br>%s' %
886-
self._format_variables_string(self.data.domain.attributes)
887-
)
888-
self.info_class.setText(
889-
'<b>Class variables:</b><br>%s' %
890-
self._format_variables_string(self.data.domain.class_vars)
891-
)
892-
self.info_meta.setText(
893-
'<b>Metas:</b><br>%s' %
894-
self._format_variables_string(self.data.domain.metas)
895-
)
896-
else:
897-
self.info_summary.setText('No data on input.')
898-
self.info_attr.setText('')
899-
self.info_class.setText('')
900-
self.info_meta.setText('')
901-
902842
def on_select(self):
903843
self.selected_rows = list(self.model.mapToSourceRows([
904844
i.row() for i in self.table_view.selectionModel().selectedRows()
@@ -909,12 +849,15 @@ def commit(self):
909849
# self.selected_rows can be list or numpy.array, thus
910850
# pylint: disable=len-as-condition
911851
if not len(self.selected_rows):
852+
self.info.set_output_summary(self.info.NoOutput)
912853
self.Outputs.reduced_data.send(None)
913854
self.Outputs.statistics.send(None)
914855
return
915856

916857
# Send a table with only selected columns to output
917858
variables = self.model.variables[self.selected_rows]
859+
self.info.set_output_summary(len(self.data[:, variables]),
860+
format_summary_details(self.data[:, variables]))
918861
self.Outputs.reduced_data.send(self.data[:, variables])
919862

920863
# Send the statistics of the selected variables to ouput

Orange/widgets/data/tests/test_owfeaturestatistics.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
1+
#pylint: disable=unsubscriptable-object
12
import datetime
23
import unittest
34
from collections import namedtuple
45
from functools import partial
56
from itertools import chain
67
from typing import List
8+
from unittest.mock import Mock
79

810
import numpy as np
911
from AnyQt.QtCore import QItemSelection, QItemSelectionRange, \
@@ -15,6 +17,7 @@
1517
from Orange.widgets.tests.utils import simulate, table_dense_sparse
1618
from Orange.widgets.data.owfeaturestatistics import \
1719
OWFeatureStatistics
20+
from Orange.widgets.utils.state_summary import format_summary_details
1821

1922
VarDataPair = namedtuple('VarDataPair', ['variable', 'data'])
2023

@@ -463,6 +466,37 @@ def test_restores_previous_selection(self):
463466
self.send_signal(self.widget.Inputs.data, self.data1)
464467
self.assertEqual(len(self.widget.selected_rows), 2)
465468

469+
class TestSummary(WidgetTest):
470+
def setUp(self):
471+
self.widget = self.create_widget(OWFeatureStatistics)
472+
self.data = make_table(
473+
[continuous_full, continuous_missing],
474+
target=[rgb_full, rgb_missing], metas=[ints_full, ints_missing]
475+
)
476+
self.select_rows = partial(select_rows, widget=self.widget)
477+
478+
def test_summary(self):
479+
"""Check if the status bar is updated when data is received"""
480+
data = self.data
481+
input_sum = self.widget.info.set_input_summary = Mock()
482+
output_sum = self.widget.info.set_output_summary = Mock()
483+
484+
self.send_signal(self.widget.Inputs.data, data)
485+
input_sum.assert_called_with(len(data), format_summary_details(data))
486+
487+
self.select_rows([0, 2])
488+
self.widget.unconditional_commit()
489+
output = self.get_output(self.widget.Outputs.reduced_data)
490+
output_sum.assert_called_with(len(output), format_summary_details(output))
491+
492+
input_sum.reset_mock()
493+
output_sum.reset_mock()
494+
self.send_signal(self.widget.Inputs.data, None)
495+
input_sum.assert_called_once()
496+
self.assertEqual(input_sum.call_args[0][0].brief, "")
497+
output_sum.assert_called_once()
498+
self.assertEqual(output_sum.call_args[0][0].brief, "")
499+
466500

467501
if __name__ == "__main__":
468502
unittest.main()

0 commit comments

Comments
 (0)