From f53deb1f6fdab1de5051c4cca1a2b7bf0ebceea8 Mon Sep 17 00:00:00 2001 From: Matej Klemen Date: Tue, 31 Jul 2018 01:13:26 +0200 Subject: [PATCH 1/8] owrocanalysis: show thresholds on mouse-over --- Orange/widgets/evaluate/owrocanalysis.py | 49 ++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/Orange/widgets/evaluate/owrocanalysis.py b/Orange/widgets/evaluate/owrocanalysis.py index 9a9ca117960..a1ffbd343d5 100644 --- a/Orange/widgets/evaluate/owrocanalysis.py +++ b/Orange/widgets/evaluate/owrocanalysis.py @@ -336,6 +336,7 @@ def __init__(self): self._plot_curves = {} self._rocch = None self._perf_line = None + self._tooltip_cache = None box = gui.vBox(self.controlArea, "Plot") tbox = gui.vBox(box, "Target Class") @@ -445,6 +446,7 @@ def clear(self): self._plot_curves = {} self._rocch = None self._perf_line = None + self._tooltip_cache = None def _initialize(self, results): names = getattr(results, "learner_names", None) @@ -526,6 +528,7 @@ def _setup_plot(self): graphics = curve.merge() curve = graphics.curve self.plot.addItem(graphics.curve_item) + graphics.curve_item.scene().sigMouseMoved.connect(self._on_mouse_moved) if self.display_convex_curve: self.plot.addItem(graphics.hull_item) @@ -553,6 +556,7 @@ def _setup_plot(self): self.plot.addItem(graphics.curve_item) self.plot.addItem(graphics.confint_item) + graphics.curve_item.scene().sigMouseMoved.connect(self._on_mouse_moved) hull_curves = [curve.avg_vertical.hull for curve in selected] @@ -561,6 +565,7 @@ def _setup_plot(self): graphics = curve.avg_threshold() self.plot.addItem(graphics.curve_item) self.plot.addItem(graphics.confint_item) + graphics.curve_item.scene().sigMouseMoved.connect(self._on_mouse_moved) hull_curves = [curve.avg_threshold.hull for curve in selected] @@ -601,6 +606,50 @@ def _setup_plot(self): warning = "All ROC curves are undefined" self.warning(warning) + def _on_mouse_moved(self, pos): + curves = self.plot.curves + for i in self.selected_classifiers: + sp = curves[i].childItems()[0] + act_pos = sp.mapFromScene(pos) + pts = sp.pointsAt(act_pos) + + curve_data = self.curve_data(self.target_index, i) + + if self.roc_averaging == OWROCAnalysis.Merge: + curve_data = curve_data.merged + elif self.roc_averaging == OWROCAnalysis.Vertical: + curve_data = curve_data.avg_vertical + elif OWROCAnalysis.Threshold: + curve_data = curve_data.avg_threshold + elif OWROCAnalysis.NoAveraging: + # currently not implemented + return + + if len(pts) > 0: + mouse_pt = pts[0].pos() + + if self._tooltip_cache: + if numpy.linalg.norm(mouse_pt - self._tooltip_cache.pos()) < 10e-6: + return + else: + self.plot.removeItem(self._tooltip_cache) + self._tooltip_cache = None + + curve_pts = curve_data.points + + # Find closest point on curve and display it + idx_closest = numpy.argmin([numpy.linalg.norm(mouse_pt - [curve_pts.fpr[idx], curve_pts.tpr[idx]]) + for idx in range(len(curve_pts.thresholds))]) + + thresh = curve_pts.thresholds[idx_closest] + if not numpy.isnan(thresh): + item = pg.TextItem(text="{:.3f}".format(thresh), color=(0, 0, 0)) + item.setPos(curve_pts.fpr[idx_closest], curve_pts.tpr[idx_closest]) + item.show() + self.plot.addItem(item) + self._tooltip_cache = item + return + def _on_target_changed(self): self.plot.clear() self._setup_plot() From 05fd5738fe1c66a666177b835d2a2b00aeef306a Mon Sep 17 00:00:00 2001 From: Matej Klemen Date: Wed, 1 Aug 2018 00:34:52 +0200 Subject: [PATCH 2/8] owrocanalysis: fixes for showing ROC thresholds * fix crash when 'Show convex ROC curves' is enabled * register mouse event only once * use QToolTip instead of TextItem for displaying thresholds * fix newly caused pylint warning --- Orange/widgets/evaluate/owrocanalysis.py | 35 ++++++++++++------------ 1 file changed, 18 insertions(+), 17 deletions(-) diff --git a/Orange/widgets/evaluate/owrocanalysis.py b/Orange/widgets/evaluate/owrocanalysis.py index a1ffbd343d5..dc8443e48ab 100644 --- a/Orange/widgets/evaluate/owrocanalysis.py +++ b/Orange/widgets/evaluate/owrocanalysis.py @@ -10,8 +10,8 @@ import numpy import sklearn.metrics as skl_metrics -from AnyQt.QtWidgets import QListView, QLabel, QGridLayout, QFrame, QAction -from AnyQt.QtGui import QColor, QPen, QBrush, QPainter, QPalette, QFont +from AnyQt.QtWidgets import QListView, QLabel, QGridLayout, QFrame, QAction, QToolTip +from AnyQt.QtGui import QColor, QPen, QBrush, QPainter, QPalette, QFont, QCursor from AnyQt.QtCore import Qt import pyqtgraph as pg @@ -396,6 +396,7 @@ def __init__(self): self.plotview = pg.GraphicsView(background="w") self.plotview.setFrameStyle(QFrame.StyledPanel) + self.plotview.scene().sigMouseMoved.connect(self._on_mouse_moved) self.plot = pg.PlotItem(enableMenu=False) self.plot.setMouseEnabled(False, False) @@ -528,7 +529,6 @@ def _setup_plot(self): graphics = curve.merge() curve = graphics.curve self.plot.addItem(graphics.curve_item) - graphics.curve_item.scene().sigMouseMoved.connect(self._on_mouse_moved) if self.display_convex_curve: self.plot.addItem(graphics.hull_item) @@ -556,7 +556,6 @@ def _setup_plot(self): self.plot.addItem(graphics.curve_item) self.plot.addItem(graphics.confint_item) - graphics.curve_item.scene().sigMouseMoved.connect(self._on_mouse_moved) hull_curves = [curve.avg_vertical.hull for curve in selected] @@ -565,7 +564,6 @@ def _setup_plot(self): graphics = curve.avg_threshold() self.plot.addItem(graphics.curve_item) self.plot.addItem(graphics.confint_item) - graphics.curve_item.scene().sigMouseMoved.connect(self._on_mouse_moved) hull_curves = [curve.avg_threshold.hull for curve in selected] @@ -607,7 +605,7 @@ def _setup_plot(self): self.warning(warning) def _on_mouse_moved(self, pos): - curves = self.plot.curves + curves = [crv for crv in self.plot.curves if isinstance(crv, pg.PlotCurveItem)] for i in self.selected_classifiers: sp = curves[i].childItems()[0] act_pos = sp.mapFromScene(pos) @@ -619,9 +617,9 @@ def _on_mouse_moved(self, pos): curve_data = curve_data.merged elif self.roc_averaging == OWROCAnalysis.Vertical: curve_data = curve_data.avg_vertical - elif OWROCAnalysis.Threshold: + elif self.roc_averaging == OWROCAnalysis.Threshold: curve_data = curve_data.avg_threshold - elif OWROCAnalysis.NoAveraging: + elif self.roc_averaging == OWROCAnalysis.NoAveraging: # currently not implemented return @@ -629,25 +627,28 @@ def _on_mouse_moved(self, pos): mouse_pt = pts[0].pos() if self._tooltip_cache: - if numpy.linalg.norm(mouse_pt - self._tooltip_cache.pos()) < 10e-6: + if numpy.linalg.norm(mouse_pt - self._tooltip_cache) < 10e-6: + if not QToolTip.isVisible(): + tt_loc = QCursor.pos() + QToolTip.showText(tt_loc, QToolTip.text()) + return else: - self.plot.removeItem(self._tooltip_cache) self._tooltip_cache = None curve_pts = curve_data.points # Find closest point on curve and display it - idx_closest = numpy.argmin([numpy.linalg.norm(mouse_pt - [curve_pts.fpr[idx], curve_pts.tpr[idx]]) - for idx in range(len(curve_pts.thresholds))]) + idx_closest = numpy.argmin( + [numpy.linalg.norm(mouse_pt - [curve_pts.fpr[idx], curve_pts.tpr[idx]]) + for idx in range(len(curve_pts.thresholds))]) thresh = curve_pts.thresholds[idx_closest] if not numpy.isnan(thresh): - item = pg.TextItem(text="{:.3f}".format(thresh), color=(0, 0, 0)) - item.setPos(curve_pts.fpr[idx_closest], curve_pts.tpr[idx_closest]) - item.show() - self.plot.addItem(item) - self._tooltip_cache = item + tt_loc = QCursor.pos() + QToolTip.showText(tt_loc, "Threshold: {:.3f}".format(thresh)) + self._tooltip_cache = [curve_pts.fpr[idx_closest], + curve_pts.tpr[idx_closest]] return def _on_target_changed(self): From fb1591d5f1c7a63f5381eff37475e128d29253a4 Mon Sep 17 00:00:00 2001 From: Matej Klemen Date: Wed, 1 Aug 2018 03:09:53 +0200 Subject: [PATCH 3/8] owrocanalysis: fix index mixup --- Orange/widgets/evaluate/owrocanalysis.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/Orange/widgets/evaluate/owrocanalysis.py b/Orange/widgets/evaluate/owrocanalysis.py index dc8443e48ab..e2e45e97516 100644 --- a/Orange/widgets/evaluate/owrocanalysis.py +++ b/Orange/widgets/evaluate/owrocanalysis.py @@ -606,12 +606,12 @@ def _setup_plot(self): def _on_mouse_moved(self, pos): curves = [crv for crv in self.plot.curves if isinstance(crv, pg.PlotCurveItem)] - for i in self.selected_classifiers: + for i in range(len(self.selected_classifiers)): sp = curves[i].childItems()[0] act_pos = sp.mapFromScene(pos) pts = sp.pointsAt(act_pos) - curve_data = self.curve_data(self.target_index, i) + curve_data = self.curve_data(self.target_index, self.selected_classifiers[i]) if self.roc_averaging == OWROCAnalysis.Merge: curve_data = curve_data.merged @@ -625,7 +625,6 @@ def _on_mouse_moved(self, pos): if len(pts) > 0: mouse_pt = pts[0].pos() - if self._tooltip_cache: if numpy.linalg.norm(mouse_pt - self._tooltip_cache) < 10e-6: if not QToolTip.isVisible(): From 8b2efa73c0fcb537edd0a00493c960c284c72215 Mon Sep 17 00:00:00 2001 From: Matej Klemen Date: Thu, 2 Aug 2018 23:28:40 +0200 Subject: [PATCH 4/8] owrocanalysis: batch 2 of fixes for showing ROC thresholds * removed dangerous retrieval of ROC curve data * changed tooltip caching so that it works smoother * fixed bug where cache value was not correct and would not reset properly --- Orange/widgets/evaluate/owrocanalysis.py | 41 +++++++++++++----------- 1 file changed, 23 insertions(+), 18 deletions(-) diff --git a/Orange/widgets/evaluate/owrocanalysis.py b/Orange/widgets/evaluate/owrocanalysis.py index e2e45e97516..7312d085943 100644 --- a/Orange/widgets/evaluate/owrocanalysis.py +++ b/Orange/widgets/evaluate/owrocanalysis.py @@ -605,37 +605,42 @@ def _setup_plot(self): self.warning(warning) def _on_mouse_moved(self, pos): - curves = [crv for crv in self.plot.curves if isinstance(crv, pg.PlotCurveItem)] - for i in range(len(self.selected_classifiers)): - sp = curves[i].childItems()[0] - act_pos = sp.mapFromScene(pos) - pts = sp.pointsAt(act_pos) - - curve_data = self.curve_data(self.target_index, self.selected_classifiers[i]) + target = self.target_index + selected = self.selected_classifiers + curves = [(clf_idx, self.plot_curves(target, clf_idx)) + for clf_idx in selected] # type: List[Tuple[int, plot_curves]] + for clf_idx, crv in curves: if self.roc_averaging == OWROCAnalysis.Merge: - curve_data = curve_data.merged + curve = crv.merge() elif self.roc_averaging == OWROCAnalysis.Vertical: - curve_data = curve_data.avg_vertical + curve = crv.avg_vertical() elif self.roc_averaging == OWROCAnalysis.Threshold: - curve_data = curve_data.avg_threshold - elif self.roc_averaging == OWROCAnalysis.NoAveraging: - # currently not implemented + curve = crv.avg_threshold() + else: + # currently not implemented for 'Show Individual Curves' return + sp = curve.curve_item.childItems()[0] # type: pg.ScatterPlotItem + act_pos = sp.mapFromScene(pos) + pts = sp.pointsAt(act_pos) + if len(pts) > 0: mouse_pt = pts[0].pos() if self._tooltip_cache: - if numpy.linalg.norm(mouse_pt - self._tooltip_cache) < 10e-6: + cache_pt, cache_thresh, cache_clf, cache_ave = self._tooltip_cache + if numpy.linalg.norm(mouse_pt - cache_pt) < 10e-6 and clf_idx == cache_clf \ + and cache_ave == self.roc_averaging: if not QToolTip.isVisible(): - tt_loc = QCursor.pos() - QToolTip.showText(tt_loc, QToolTip.text()) + QToolTip.showText(QCursor.pos(), + "Threshold: {:.3f}".format(cache_thresh)) return else: + QToolTip.showText(QCursor.pos(), "") self._tooltip_cache = None - curve_pts = curve_data.points + curve_pts = curve.curve.points # Find closest point on curve and display it idx_closest = numpy.argmin( @@ -646,8 +651,8 @@ def _on_mouse_moved(self, pos): if not numpy.isnan(thresh): tt_loc = QCursor.pos() QToolTip.showText(tt_loc, "Threshold: {:.3f}".format(thresh)) - self._tooltip_cache = [curve_pts.fpr[idx_closest], - curve_pts.tpr[idx_closest]] + self._tooltip_cache = ([curve_pts.fpr[idx_closest], curve_pts.tpr[idx_closest]], + thresh, clf_idx, self.roc_averaging) return def _on_target_changed(self): From 57f6b855b1510b1040111c8b9a7d837cf11e974a Mon Sep 17 00:00:00 2001 From: Matej Klemen Date: Tue, 7 Aug 2018 01:37:03 +0200 Subject: [PATCH 5/8] owrocanalysis: vectorize calculation of closest point to cursor and minor consistency fix --- Orange/widgets/evaluate/owrocanalysis.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/Orange/widgets/evaluate/owrocanalysis.py b/Orange/widgets/evaluate/owrocanalysis.py index 7312d085943..a3a2d7efb1b 100644 --- a/Orange/widgets/evaluate/owrocanalysis.py +++ b/Orange/widgets/evaluate/owrocanalysis.py @@ -642,15 +642,14 @@ def _on_mouse_moved(self, pos): curve_pts = curve.curve.points - # Find closest point on curve and display it - idx_closest = numpy.argmin( - [numpy.linalg.norm(mouse_pt - [curve_pts.fpr[idx], curve_pts.tpr[idx]]) - for idx in range(len(curve_pts.thresholds))]) + roc_points = numpy.column_stack((curve_pts.fpr, curve_pts.tpr)) + diff = numpy.subtract(roc_points, mouse_pt) + # Find closest point on curve and display the tooltip there + idx_closest = numpy.argmin(numpy.linalg.norm(diff, axis=1)) thresh = curve_pts.thresholds[idx_closest] if not numpy.isnan(thresh): - tt_loc = QCursor.pos() - QToolTip.showText(tt_loc, "Threshold: {:.3f}".format(thresh)) + QToolTip.showText(QCursor.pos(), "Threshold: {:.3f}".format(thresh)) self._tooltip_cache = ([curve_pts.fpr[idx_closest], curve_pts.tpr[idx_closest]], thresh, clf_idx, self.roc_averaging) return From ddddded7b74666fa70aba9f881ff1b1f1233ba6d Mon Sep 17 00:00:00 2001 From: Matej Klemen Date: Fri, 10 Aug 2018 14:17:15 +0200 Subject: [PATCH 6/8] owrocanalysis: improve tooltips for overlapping points --- Orange/widgets/evaluate/owrocanalysis.py | 36 ++++++++++++++++-------- 1 file changed, 24 insertions(+), 12 deletions(-) diff --git a/Orange/widgets/evaluate/owrocanalysis.py b/Orange/widgets/evaluate/owrocanalysis.py index a3a2d7efb1b..7edee4d3011 100644 --- a/Orange/widgets/evaluate/owrocanalysis.py +++ b/Orange/widgets/evaluate/owrocanalysis.py @@ -609,6 +609,8 @@ def _on_mouse_moved(self, pos): selected = self.selected_classifiers curves = [(clf_idx, self.plot_curves(target, clf_idx)) for clf_idx in selected] # type: List[Tuple[int, plot_curves]] + valid_thresh, valid_clf = [], [] + pt, ave_mode = None, self.roc_averaging for clf_idx, crv in curves: if self.roc_averaging == OWROCAnalysis.Merge: @@ -629,30 +631,40 @@ def _on_mouse_moved(self, pos): mouse_pt = pts[0].pos() if self._tooltip_cache: cache_pt, cache_thresh, cache_clf, cache_ave = self._tooltip_cache - if numpy.linalg.norm(mouse_pt - cache_pt) < 10e-6 and clf_idx == cache_clf \ + curr_thresh, curr_clf = [], [] + if numpy.linalg.norm(mouse_pt - cache_pt) < 10e-6 \ and cache_ave == self.roc_averaging: - if not QToolTip.isVisible(): - QToolTip.showText(QCursor.pos(), - "Threshold: {:.3f}".format(cache_thresh)) - - return + mask = numpy.equal(cache_clf, clf_idx) + curr_thresh = numpy.compress(mask, cache_thresh).tolist() + curr_clf = numpy.compress(mask, cache_clf).tolist() else: QToolTip.showText(QCursor.pos(), "") self._tooltip_cache = None - curve_pts = curve.curve.points + if curr_thresh: + valid_thresh.append(*curr_thresh) + valid_clf.append(*curr_clf) + pt = cache_pt + continue + curve_pts = curve.curve.points roc_points = numpy.column_stack((curve_pts.fpr, curve_pts.tpr)) diff = numpy.subtract(roc_points, mouse_pt) - # Find closest point on curve and display the tooltip there + # Find closest point on curve and save the corresponding threshold idx_closest = numpy.argmin(numpy.linalg.norm(diff, axis=1)) thresh = curve_pts.thresholds[idx_closest] if not numpy.isnan(thresh): - QToolTip.showText(QCursor.pos(), "Threshold: {:.3f}".format(thresh)) - self._tooltip_cache = ([curve_pts.fpr[idx_closest], curve_pts.tpr[idx_closest]], - thresh, clf_idx, self.roc_averaging) - return + valid_thresh.append(thresh) + valid_clf.append(clf_idx) + pt = [curve_pts.fpr[idx_closest], curve_pts.tpr[idx_closest]] + + if valid_thresh: + clf_names = self.classifier_names + msg = "Thresholds:\n" + "\n".join(["({:s}) {:.3f}".format(clf_names[i], thresh) + for i, thresh in zip(valid_clf, valid_thresh)]) + QToolTip.showText(QCursor.pos(), msg) + self._tooltip_cache = (pt, valid_thresh, valid_clf, ave_mode) def _on_target_changed(self): self.plot.clear() From 835f9026221076fc5966e0e6446239cfb5ae3a4f Mon Sep 17 00:00:00 2001 From: Matej Klemen Date: Mon, 20 Aug 2018 02:14:59 +0200 Subject: [PATCH 7/8] tests: move mouseMove from test_combobox.py to utils --- Orange/widgets/tests/utils.py | 16 +++++++++++++++- Orange/widgets/utils/tests/test_combobox.py | 16 ++-------------- 2 files changed, 17 insertions(+), 15 deletions(-) diff --git a/Orange/widgets/tests/utils.py b/Orange/widgets/tests/utils.py index c02e352aaf6..cd502ce1991 100644 --- a/Orange/widgets/tests/utils.py +++ b/Orange/widgets/tests/utils.py @@ -2,8 +2,10 @@ import warnings import contextlib -from AnyQt.QtCore import Qt, QObject, QEventLoop, QTimer, QLocale +from AnyQt.QtCore import Qt, QObject, QEventLoop, QTimer, QLocale, QPoint from AnyQt.QtTest import QTest +from AnyQt.QtGui import QMouseEvent +from AnyQt.QtWidgets import QApplication class EventSpy(QObject): @@ -303,3 +305,15 @@ def wrap(*args, **kwargs): return result return wrap return wrapper + + +def mouseMove(widget, pos=QPoint(), delay=-1): # pragma: no-cover + # Like QTest.mouseMove, but functional without QCursor.setPos + if pos.isNull(): + pos = widget.rect().center() + me = QMouseEvent(QMouseEvent.MouseMove, pos, widget.mapToGlobal(pos), + Qt.NoButton, Qt.MouseButtons(0), Qt.NoModifier) + if delay > 0: + QTest.qWait(delay) + + QApplication.sendEvent(widget, me) diff --git a/Orange/widgets/utils/tests/test_combobox.py b/Orange/widgets/utils/tests/test_combobox.py index 8ac1974deda..a90d1861ee1 100644 --- a/Orange/widgets/utils/tests/test_combobox.py +++ b/Orange/widgets/utils/tests/test_combobox.py @@ -2,11 +2,11 @@ import unittest -from AnyQt.QtCore import Qt, QPoint, QRect -from AnyQt.QtGui import QMouseEvent +from AnyQt.QtCore import Qt, QRect from AnyQt.QtWidgets import QListView, QApplication from AnyQt.QtTest import QTest, QSignalSpy from Orange.widgets.tests.base import GuiTest +from Orange.widgets.tests.utils import mouseMove from Orange.widgets.utils import combobox @@ -133,15 +133,3 @@ def test_popup_util(self): geom, QRect(0, 500, 100, 20), screen ) self.assertEqual(g4, QRect(0, 500 - 400, 100, 400)) - - -def mouseMove(widget, pos=QPoint(), delay=-1): # pragma: no-cover - # Like QTest.mouseMove, but functional without QCursor.setPos - if pos.isNull(): - pos = widget.rect().center() - me = QMouseEvent(QMouseEvent.MouseMove, pos, widget.mapToGlobal(pos), - Qt.NoButton, Qt.MouseButtons(0), Qt.NoModifier) - if delay > 0: - QTest.qWait(delay) - - QApplication.sendEvent(widget, me) From 099ee74d35e11228951f83a31afb36bf6839f347 Mon Sep 17 00:00:00 2001 From: Matej Klemen Date: Mon, 20 Aug 2018 02:22:01 +0200 Subject: [PATCH 8/8] tests: test tooltips for ROC analysis --- .../evaluate/tests/test_owrocanalysis.py | 60 +++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/Orange/widgets/evaluate/tests/test_owrocanalysis.py b/Orange/widgets/evaluate/tests/test_owrocanalysis.py index b7891dd5860..1ac5aab28e3 100644 --- a/Orange/widgets/evaluate/tests/test_owrocanalysis.py +++ b/Orange/widgets/evaluate/tests/test_owrocanalysis.py @@ -4,6 +4,8 @@ import copy import numpy as np +from AnyQt.QtWidgets import QToolTip + import Orange.data import Orange.evaluation import Orange.classification @@ -12,6 +14,7 @@ from Orange.widgets.evaluate.owrocanalysis import OWROCAnalysis from Orange.widgets.evaluate.tests.base import EvaluateTest from Orange.widgets.tests.base import WidgetTest +from Orange.widgets.tests.utils import mouseMove class TestROC(unittest.TestCase): @@ -156,3 +159,60 @@ def test_nan_input(self): self.assertTrue(self.widget.Error.invalid_results.is_shown()) self.send_signal(self.widget.Inputs.evaluation_results, None) self.assertFalse(self.widget.Error.invalid_results.is_shown()) + + def test_tooltips(self): + data_in = Orange.data.Table("titanic") + res = Orange.evaluation.TestOnTrainingData( + data=data_in, + learners=[Orange.classification.KNNLearner(), + Orange.classification.LogisticRegressionLearner()], + store_data=True + ) + + self.send_signal(self.widget.Inputs.evaluation_results, res) + self.widget.roc_averaging = OWROCAnalysis.Merge + self.widget.target_index = 0 + self.widget.selected_classifiers = [0, 1] + vb = self.widget.plot.getViewBox() + vb.childTransform() # Force pyqtgraph to update transforms + + curve = self.widget.plot_curves(self.widget.target_index, 0) + curve_merge = curve.merge() + view = self.widget.plotview + item = curve_merge.curve_item # type: pg.PlotCurveItem + + # no tooltips to be shown + pos = item.mapToScene(0.0, 1.0) + pos = view.mapFromScene(pos) + mouseMove(view.viewport(), pos) + self.assertIs(self.widget._tooltip_cache, None) + + # test single point + pos = item.mapToScene(0.22504, 0.45400) + pos = view.mapFromScene(pos) + mouseMove(view.viewport(), pos) + shown_thresh = self.widget._tooltip_cache[1] + self.assertTrue(QToolTip.isVisible()) + np.testing.assert_almost_equal(shown_thresh, [0.40000], decimal=5) + + pos = item.mapToScene(0.0, 0.0) + pos = view.mapFromScene(pos) + # test overlapping points + mouseMove(view.viewport(), pos) + shown_thresh = self.widget._tooltip_cache[1] + self.assertTrue(QToolTip.isVisible()) + np.testing.assert_almost_equal(shown_thresh, [1.8, 1.89336], decimal=5) + + # test that cache is invalidated when changing averaging mode + self.widget.roc_averaging = OWROCAnalysis.Threshold + self.widget._replot() + mouseMove(view.viewport(), pos) + shown_thresh = self.widget._tooltip_cache[1] + self.assertTrue(QToolTip.isVisible()) + np.testing.assert_almost_equal(shown_thresh, [1, 1]) + + # test nan thresholds + self.widget.roc_averaging = OWROCAnalysis.Vertical + self.widget._replot() + mouseMove(view.viewport(), pos) + self.assertIs(self.widget._tooltip_cache, None)