Repository: biolab/orange3-educational (language: python)

Issue:
[ENH] Polynomial Regression: Allow fixing the intercept to 0
##### Issue

Resolves #93 and shows some great love for this venerable old widget.

~~Requires https://github.com/biolab/orange3/pull/4958, commit https://github.com/biolab/orange3/pull/4958/commits/cb5a551bbf070c3b6be599cb23ba6428348360df.~~

##### Description of changes

See commit messages.

##### Includes
- [X] Code changes
- [ ] Tests
- [ ] Documentation



Discussion / hints:
# [Codecov](https://codecov.io/gh/biolab/orange3-educational/pull/95?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=biolab) Report
> Merging [#95](https://codecov.io/gh/biolab/orange3-educational/pull/95?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=biolab) (46caeb5) into [master](https://codecov.io/gh/biolab/orange3-educational/commit/6d4f6fc5d1ce970aa1903e59ff313f0cfe19f0d7?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=biolab) (6d4f6fc) will **decrease** coverage by `0.72%`.
> The diff coverage is `74.62%`.

[![Impacted file tree graph](https://codecov.io/gh/biolab/orange3-educational/pull/95/graphs/tree.svg?width=650&height=150&src=pr&token=fBv7iR2NHR&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=biolab)](https://codecov.io/gh/biolab/orange3-educational/pull/95?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=biolab)

```diff
@@            Coverage Diff             @@
##           master      #95      +/-   ##
==========================================
- Coverage   87.65%   86.93%   -0.73%     
==========================================
  Files          20       20              
  Lines        2893     2932      +39     
  Branches      351      363      +12     
==========================================
+ Hits         2536     2549      +13     
- Misses        325      342      +17     
- Partials       32       41       +9     
```


| [Impacted Files](https://codecov.io/gh/biolab/orange3-educational/pull/95?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=biolab) | Coverage Δ | |
|---|---|---|
| [...trib/educational/widgets/owpolynomialregression.py](https://codecov.io/gh/biolab/orange3-educational/pull/95/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=biolab#diff-b3JhbmdlY29udHJpYi9lZHVjYXRpb25hbC93aWRnZXRzL293cG9seW5vbWlhbHJlZ3Jlc3Npb24ucHk=) | `87.68% <74.62%> (-8.95%)` | :arrow_down: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/biolab/orange3-educational/pull/95?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=biolab).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=biolab)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/biolab/orange3-educational/pull/95?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=biolab). Last update [6d4f6fc...46caeb5](https://codecov.io/gh/biolab/orange3-educational/pull/95?src=pr&el=lastupdated&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=biolab). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=biolab).


Fix patch:
diff --git a/orangecontrib/educational/widgets/owpolynomialclassification.py b/orangecontrib/educational/widgets/owpolynomialclassification.py
index 16ca808..717499c 100644
--- a/orangecontrib/educational/widgets/owpolynomialclassification.py
+++ b/orangecontrib/educational/widgets/owpolynomialclassification.py
@@ -1,79 +1,79 @@
-from Orange.widgets.utils import itemmodels
-from math import isnan
-from os import path
 import copy
+from colorsys import rgb_to_hsv, hsv_to_rgb
+from xml.sax.saxutils import escape
+from itertools import chain
 
 import numpy as np
-import scipy.sparse as sp
 from scipy.interpolate import splprep, splev
 from scipy.ndimage.filters import gaussian_filter
 
-from AnyQt.QtCore import Qt
-from AnyQt.QtGui import QPixmap, QColor, QIcon
+from AnyQt.QtCore import Qt, QRectF, QObject, QPointF, QEvent
+from AnyQt.QtGui import QPalette, QPen, QFont, QCursor, QColor, QBrush
+from AnyQt.QtWidgets import QGraphicsSceneMouseEvent, QGraphicsTextItem
 
-from Orange.base import Learner as InputLearner
-from Orange.data import (
-    ContinuousVariable, Table, Domain, StringVariable, DiscreteVariable)
-from Orange.widgets import settings, gui
+import pyqtgraph as pg
+
+from Orange.data import \
+    Table, Domain, ContinuousVariable, StringVariable, DiscreteVariable
+from Orange.base import Learner
+from Orange.classification import \
+    LogisticRegressionLearner, RandomForestLearner, TreeLearner
+from Orange.preprocess.transformation import Indicator
+
+from Orange.widgets import gui
+from Orange.widgets.settings import \
+    DomainContextHandler, Setting, SettingProvider, ContextSetting
+from Orange.widgets.utils.colorpalettes import DiscretePalette
+from Orange.widgets.utils.itemmodels import DomainModel
 from Orange.widgets.utils.owlearnerwidget import OWBaseLearner
-from Orange.classification import (
-    LogisticRegressionLearner,
-    RandomForestLearner,
-    TreeLearner
-)
+from Orange.widgets.visualize.owscatterplotgraph import OWScatterPlotBase
+from Orange.widgets.visualize.utils.plotutils import SymbolItemSample
 from Orange.widgets.widget import Msg, Input, Output
-from orangewidget.report import report
-
 
 from orangecontrib.educational.widgets.utils.polynomialtransform \
     import PolynomialTransform
-from orangecontrib.educational.widgets.utils.color_transform \
-    import rgb_hash_brighter, rgb_to_hex
 from orangecontrib.educational.widgets.utils.contour import Contour
-from orangecontrib.educational.widgets.highcharts import Highchart
 
+GRID_SIZE = 60
 
-class Scatterplot(Highchart):
-    """
-    Scatterplot extends Highchart and just defines some defaults:
-    * disable scroll-wheel zooming,
-    * disable all points selection
-    * set cursor for series to move
-    * adds javascript for contour
-    """
 
-    def __init__(self, **kwargs):
-        with open(path.join(path.dirname(__file__), 'resources', 'highcharts-contour.js'),
-                  encoding='utf-8') as f:
-            contour_js = f.read()
+class HoverEventDelegate(QObject):
+    def __init__(self, delegate, parent=None):
+        super().__init__(parent)
+        self.delegate = delegate
 
-        super().__init__(enable_zoom=False,
-                         enable_select='',
-                         javascript=contour_js,
-                         **kwargs)
+    def eventFilter(self, obj, event):
+        return isinstance(event, QGraphicsSceneMouseEvent) \
+               and self.delegate(event)
 
-    def remove_contours(self):
-        self.evalJS("""
-            for(i=chart.series.length - 1; i >= 0; i--){
-                if(chart.series[i].type == "spline")
-                {
-                    chart.series[i].remove(false);
-                }
-            }""")
 
-    def add_series(self, series):
-        for i, s in enumerate(series):
-            self.exposeObject('series%d' % i, series[i])
-            self.evalJS("chart.addSeries(series%d, false);" % i)
+class PolynomialPlot(OWScatterPlotBase):
+    # Like OWScatterPlotBase, but with reversed legend, so the target class
+    # is first and "others" is seecond.
+    @staticmethod
+    def _make_pen(color, width):
+        p = QPen(color, width)
+        p.setCosmetic(True)
+        return p
 
-    def redraw_series(self):
-        self.evalJS("chart.redraw();")
+    def _update_colored_legend(self, legend, labels, _):
+        if self.scatterplot_item is None or not self.palette:
+            return
+        colors = self.palette.values_to_colors(np.arange(len(labels)))
+        for color, label in reversed(list(zip(colors, labels))):
+            color = QColor(*color)
+            pen = self._make_pen(color.darker(self.DarkerValue), 1.5)
+            color.setAlpha(self.alpha_value)
+            brush = QBrush(color)
+            legend.addItem(
+                SymbolItemSample(pen=pen, brush=brush, size=10, symbol="o"),
+                escape(label))
 
 
 class OWPolynomialClassification(OWBaseLearner):
     name = "Polynomial Classification"
-    description = "Widget that demonstrates classification in two classes " \
-                  "with polynomial expansion of attributes."
+    description = "Widget that demonstrates classification " \
+                  "with polynomial expansion of variables."
     keywords = ["polynomial classification", "classification", "class",
                 "classification visualization", "polynomial features"]
     icon = "icons/polynomialclassification.svg"
@@ -81,428 +81,389 @@ class OWPolynomialClassification(OWBaseLearner):
     resizing_enabled = True
     priority = 600
 
-    # inputs and outputs
     class Inputs(OWBaseLearner.Inputs):
-        learner = Input("Learner", InputLearner)
+        learner = Input("Learner", Learner)
 
     class Outputs(OWBaseLearner.Outputs):
         coefficients = Output("Coefficients", Table, default=True)
         data = Output("Data", Table)
 
-    # data attributes
-    data = None
-    selected_data = None
-    probabilities_grid = None
-    xv = None
-    yv = None
-
-    # learners
     LEARNER = LogisticRegressionLearner
-    learner_other = None
-    default_preprocessor = PolynomialTransform
-
-    learner_name = settings.Setting("Polynomial Classification")
-
-    # widget properties
-    attr_x = settings.Setting('')
-    attr_y = settings.Setting('')
-    target_class = settings.Setting('')
-    degree = settings.Setting(1)
-    legend_enabled = settings.Setting(True)
-    contours_enabled = settings.Setting(False)
-    contour_step = settings.Setting(0.1)
-
-    graph_name = 'scatter'
-
-    # settings
-    grid_size = 25
-    contour_color = "#1f1f1f"
-
-    # layout elements
-    options_box = None
-    cbx = None
-    cby = None
-    degree_spin = None
-    plot_properties_box = None
-    contours_enabled_checkbox = None
-    legend_enabled_checkbox = None
-    contour_step_slider = None
-    scatter = None
-    target_class_combobox = None
-    x_var_model = None
-    y_var_model = None
+
+    settingsHandler = DomainContextHandler(
+        match_values=DomainContextHandler.MATCH_VALUES_CLASS)
+    graph = SettingProvider(PolynomialPlot)
+
+    learner_name = Setting("Polynomial Classification")
+    attr_x = ContextSetting(None)
+    attr_y = ContextSetting(None)
+    target_class = Setting("")
+    degree = Setting(1)
+    contours_enabled = Setting(True)
+
+    graph_name = 'graph'
 
     class Error(OWBaseLearner.Error):
-        to_few_features = Msg(
-            "Polynomial classification requires at least two numeric features")
-        no_class = Msg("Data must have a single discrete class attribute")
-        all_none_data = Msg("One of the features has no defined values")
-        no_classifier = Msg("Learner must be a classifier")
+        num_features = Msg("Data must contain at least two numeric variables.")
+        no_class = Msg("Data must have a single target attribute.")
+        no_nonnan_data = Msg("No points with defined values.")
+        same_variable = Msg("Select two different variables.")
+
+    def __init__(self, *args, **kwargs):
+        # Some attributes must be created before super().__init__
+        self._add_graph()
+        self.var_model = DomainModel(valid_types=(ContinuousVariable, ))
+
+        super().__init__(*args, **kwargs)
+        self.input_learner = None
+        self.data = None
+
+        self.learner = None
+        self.selected_data = None
+        self.probabilities_grid = None
+        self.xv = None
+        self.yv = None
+        self.contours = []
+        self.init_learner()
 
     def add_main_layout(self):
-        # var models
-        self.x_var_model = itemmodels.VariableListModel()
-        self.y_var_model = itemmodels.VariableListModel()
-
-        # options box
-        self.options_box = gui.widgetBox(self.controlArea, "Options")
-        opts = dict(
-            widget=self.options_box, master=self, orientation=Qt.Horizontal)
-        opts_combo = dict(opts, **dict(sendSelectedValue=True))
-        self.cbx = gui.comboBox(
-            value='attr_x', label='X: ', callback=self.apply, **opts_combo)
-        self.cby = gui.comboBox(
-            value='attr_y', label='Y: ', callback=self.apply, **opts_combo)
-        self.target_class_combobox = gui.comboBox(
-            value='target_class', label='Target: ',
-            callback=self.apply, **opts_combo)
-        self.degree_spin = gui.spin(
-            value='degree', label='Polynomial expansion:',
-            minv=1, maxv=5, step=1, callback=self.init_learner,
-            alignment=Qt.AlignRight, controlWidth=70, **opts)
-
-        self.cbx.setModel(self.x_var_model)
-        self.cby.setModel(self.y_var_model)
-
-        # plot properties box
-        self.plot_properties_box = gui.widgetBox(
-            self.controlArea, "Plot Properties")
-        self.legend_enabled_checkbox = gui.checkBox(
-            self.plot_properties_box, self, 'legend_enabled',
-            label="Show legend", callback=self.replot)
-        self.contours_enabled_checkbox = gui.checkBox(
-            self.plot_properties_box, self, 'contours_enabled',
-            label="Show contours", callback=self.plot_contour)
-        self.contour_step_slider = gui.spin(
-            self.plot_properties_box, self, 'contour_step',
-            minv=0.10, maxv=0.50, step=0.05, callback=self.plot_contour,
-            label='Contour step:', decimals=2, spinType=float,
-            alignment=Qt.AlignRight, controlWidth=70)
+        box = gui.widgetBox(self.controlArea, "Variables")
+        gui.comboBox(
+            box, self, "attr_x", model=self.var_model,
+            callback=self._on_attr_changed)
+        gui.comboBox(
+            box, self, "attr_y", model=self.var_model,
+            callback=self._on_attr_changed)
+        gui.spin(
+            box, self, value='degree', label='Polynomial expansion:',
+            minv=1, maxv=5, step=1, alignment=Qt.AlignRight, controlWidth=70,
+            callback=self._on_degree_changed)
+        gui.comboBox(
+            box, self, 'target_class', label='Target:',
+            orientation=Qt.Horizontal, sendSelectedValue=True,
+            callback=self._on_target_changed)
+
+        box = gui.widgetBox(self.controlArea, box=True)
+        gui.checkBox(
+            box, self.graph, 'show_legend',"Show legend",
+            callback=self.graph.update_legend_visibility)
+        gui.checkBox(
+            box, self, 'contours_enabled', label="Show contours",
+            callback=self.plot_contour)
 
         gui.rubber(self.controlArea)
 
-        # chart
-        self.scatter = Scatterplot(
-            xAxis_gridLineWidth=0, yAxis_gridLineWidth=0,
-            xAxis_startOnTick=False, xAxis_endOnTick=False,
-            yAxis_startOnTick=False, yAxis_endOnTick=False,
-            xAxis_lineWidth=0, yAxis_lineWidth=0,
-            yAxis_tickWidth=1, title_text='', tooltip_shared=False)
-
-        # Just render an empty chart so it shows a nice 'No data to display'
-        self.scatter.chart()
-        self.mainArea.layout().addWidget(self.scatter)
+    def add_bottom_buttons(self):
+        pass
 
+    def _on_degree_changed(self):
         self.init_learner()
+        self.apply()
+
+    def _on_attr_changed(self):
+        self.select_data()
+        self.apply()
+
+    def _on_target_changed(self):
+        self.select_data()
+        self.apply()
 
+    ##############################
+    # Input signal-related stuff
     @Inputs.learner
     def set_learner(self, learner):
-        """
-        Function is sets learner when learner is changed on input
-        """
-        self.learner_other = learner
+        self.input_learner = learner
         self.init_learner()
 
     def set_preprocessor(self, preprocessor):
-        """
-        Function adds preprocessor when it changed on input
-        """
         self.preprocessors = [preprocessor] if preprocessor else []
         self.init_learner()
 
+    @Inputs.data
     def set_data(self, data):
-        """
-        Function receives data from input and init part of widget if data
-        satisfy. Otherwise set empty plot and notice
-        user about that
-
-        Parameters
-        ----------
-        data : Table
-            Input data
-        """
-
-        def reset_combos():
-            self.x_var_model[:] = []
-            self.y_var_model[:] = []
-            self.target_class_combobox.clear()
-
-        def init_combos():
-            """
-            function initialize the combos with attributes
-            """
-            reset_combos()
-
-            c_vars = [var for var in data.domain.variables if var.is_continuous]
-
-            self.x_var_model[:] = c_vars
-            self.y_var_model[:] = c_vars
-
-            for i, var in enumerate(data.domain.class_var.values):
-                pix_map = QPixmap(60, 60)
-                color = tuple(data.domain.class_var.colors[i].tolist())
-                pix_map.fill(QColor(*color))
-                self.target_class_combobox.addItem(QIcon(pix_map), var)
+        combo = self.controls.target_class
 
+        self.closeContext()
         self.Error.clear()
-
-        # clear variables
+        combo.clear()
+        self.var_model.set_domain(None)
+        self.data = self.selected_data = None
         self.xv = None
         self.yv = None
         self.probabilities_grid = None
 
-        if data is None or len(data) == 0:
-            self.data = None
-            reset_combos()
-            self.set_empty_plot()
-        elif sum(True for var in data.domain.attributes
-                 if isinstance(var, ContinuousVariable)) < 2:
-            self.data = None
-            reset_combos()
-            self.Error.to_few_features()
-            self.set_empty_plot()
-        elif (data.domain.class_var is None or
-              data.domain.class_var.is_continuous or
-              sum(line.get_class() == None for line in data) == len(data) or
-              len(data.domain.class_var.values) < 2):
-            self.data = None
-            reset_combos()
+        if not data:
+            return
+        domain = data.domain
+        if domain.class_var is None or domain.class_var.is_continuous:
             self.Error.no_class()
-            self.set_empty_plot()
-        else:
-            self.data = data
-            init_combos()
-            self.attr_x = self.cbx.itemText(0)
-            self.attr_y = self.cbx.itemText(1)
-            self.target_class = self.target_class_combobox.itemText(0)
+            return
+        if sum(var.is_continuous
+                 for var in chain(domain.variables, domain.metas)) < 2:
+            self.Error.num_features()
+            return
 
-        self.apply()
+        self.data = data
+        non_empty = np.bincount(data.Y[np.isfinite(data.Y)].astype(int)) > 0
+        values = np.array(domain.class_var.values)[non_empty]
+        combo.addItems(values.tolist())
+        self.var_model.set_domain(self.data.domain)
+        self.attr_x, self.attr_y = self.var_model[:2]
+        self.target_class = combo.itemText(0)
+
+        hide_attrs = len(self.var_model) == 2
+        self.controls.attr_x.setHidden(hide_attrs)
+        self.controls.attr_y.setHidden(hide_attrs)
+
+        self.openContext(self.data)
+        self.select_data()
 
     def init_learner(self):
-        """
-        Function init learner and add preprocessors to learner
-        """
-        if self.learner_other is not None and \
-            self.learner_other.__class__.__name__ == "LinearRegressionLearner":
-            # in case that learner is a Linear Regression
-            self.learner = None
-            self.Error.no_classifier()
-        else:
-            self.learner = (copy.deepcopy(self.learner_other) or
-                            self.LEARNER(penalty='l2', C=1e10))
-            self.learner.preprocessors = (
-                [self.default_preprocessor(self.degree)] +
-                list(self.preprocessors or []) +
-                list(self.learner.preprocessors or []))
-            self.Error.no_classifier.clear()
+        self.learner = copy.copy(self.input_learner
+                                 or self.LEARNER(penalty='l2', C=1e10))
+        self.learner.preprocessors = (
+            [PolynomialTransform(self.degree)] +
+            list(self.preprocessors or []) +
+            list(self.learner.preprocessors or []))
+        self.send_learner()
+
+    def handleNewSignals(self):
         self.apply()
 
-    def set_empty_plot(self):
-        """
-        Function inits empty plot
-        """
-        self.scatter.clear()
-
-    def replot(self):
-        """
-        This function performs complete replot of the graph
-        """
-        if self.data is None or self.selected_data is None:
-            self.set_empty_plot()
+    def select_data(self):
+        """Put the two selected columns in a new Orange.data.Table"""
+        self.Error.no_nonnan_data.clear()
+        self.Error.same_variable.clear()
+
+        attr_x, attr_y = self.attr_x, self.attr_y
+        if self.attr_x is self.attr_y:
+            self.selected_data = None
+            self.Error.same_variable()
             return
 
-        attr_x = self.data.domain[self.attr_x]
-        attr_y = self.data.domain[self.attr_y]
-        data_x = [v[0] for v in self.data[:, attr_x] if not isnan(v[0])]
-        data_y = [v[0] for v in self.data[:, attr_y] if not isnan(v[0])]
-        min_x = min(data_x)
-        max_x = max(data_x)
-        min_y = min(data_y)
-        max_y = max(data_y)
-        # just in cas that diff is 0
-        diff_x = (max_x - min_x) if abs(max_x - min_x) > 0.001 else 0.1
-        diff_y = (max_y - min_y) if abs(max_y - min_y) > 0.001 else 0.1
-        min_x, max_x = min_x - 0.03 * diff_x, max_x + 0.03 * diff_x
-        min_y, max_y = min_y - 0.03 * diff_y, max_y + 0.03 * diff_y
-
-        options = dict(series=[])
-
-        # gradient and contour
-        options['series'] += self.plot_gradient_and_contour(
-            min_x, max_x, min_y, max_y)
-
-        sd = self.selected_data
-        # data points
-        options['series'] += [
-            dict(
-                data=[list(p.attributes())
-                      for p in sd
-                      if (p.metas[0] == _class and
-                          all(v is not None for v in p.attributes()))],
-                type="scatter",
-                zIndex=10,
-                color=rgb_to_hex(tuple(
-                    sd.domain.metas[0].colors[_class].tolist())),
-                showInLegend=True,
-                name=sd.domain.metas[0].values[_class])
-            for _class in range(len(sd.domain.metas[0].values))]
-
-        # add nan values as a gray dots
-        options['series'] += [
-            dict(
-                data=[list(p.attributes())
-                      for p in sd
-                      if np.isnan(p.metas[0])],
-                type="scatter",
-                zIndex=10,
-                color=rgb_to_hex((160, 160, 160)),
-                showInLegend=False)]
-
-        cls_domain = sd.domain.metas[0]
-
-        target_idx = cls_domain.values.index(self.target_class)
-        target_color = tuple(cls_domain.colors[target_idx].tolist())
-        other_color = (tuple(cls_domain.colors[(target_idx + 1) % 2].tolist())
-                       if len(cls_domain.values) == 2 else (170, 170, 170))
-
-        # highcharts parameters
-        kwargs = dict(
-            xAxis_title_text=attr_x.name,
-            yAxis_title_text=attr_y.name,
-            xAxis_min=min_x,
-            xAxis_max=max_x,
-            yAxis_min=min_y,
-            yAxis_max=max_y,
-            colorAxis=dict(
-                labels=dict(enabled=False),
-                stops=[
-                    [0, rgb_hash_brighter(rgb_to_hex(other_color), 0.5)],
-                    [0.5, '#ffffff'],
-                    [1, rgb_hash_brighter(rgb_to_hex(target_color), 0.5)]],
-                tickInterval=0.2, min=0, max=1),
-            plotOptions_contour_colsize=(max_y - min_y) / 1000,
-            plotOptions_contour_rowsize=(max_x - min_x) / 1000,
-            legend=dict(
-                enabled=self.legend_enabled,
-                layout='vertical',
-                align='right',
-                verticalAlign='top',
-                floating=True,
-                backgroundColor='rgba(255, 255, 255, 0.3)',
-                symbolWidth=0,
-                symbolHeight=0),
-            tooltip_headerFormat="",
-            tooltip_pointFormat="<strong>%s:</strong> {point.x:.2f} <br/>"
-                                "<strong>%s:</strong> {point.y:.2f}" %
-                                (self.attr_x, self.attr_y))
-
-        self.scatter.chart(options, **kwargs)
+        names = [var.name for var in (attr_x, attr_y)]
+        if names == ["x", "y"]:
+            names = [""] * 2
+        for place, name in zip(("bottom", "left"), names):
+            self.graph.plot_widget.getAxis(place).setLabel(name)
+        old_class = self.data.domain.class_var
+        values = old_class.values
+        target_idx = values.index(self.target_class)
+
+        new_class = DiscreteVariable(
+            old_class.name + "'",
+            values=(values[1 - target_idx] if len(values) == 2 else 'Others',
+                    self.target_class),
+            compute_value=Indicator(old_class, target_idx))
+        new_class.palette = DiscretePalette(
+            "indicator", "indicator",
+            [[64, 64, 64], list(old_class.palette.palette[target_idx])])
+
+        domain = Domain([attr_x, attr_y], new_class, [old_class])
+
+        self.selected_data = self.data.transform(domain)
+        valid_data = \
+            np.flatnonzero(
+                np.all(
+                    np.isfinite(self.selected_data.X),
+                    axis=1)
+            )
+        if not valid_data.size:
+            self.Error.no_nonnan_data()
+            self.selected_data = None
+        else:
+            self.selected_data = self.selected_data[valid_data]
+
+
+    def apply(self):
+        self.update_model()
+        self.send_model()
+        self.send_coefficients()
+        self.send_data()
+
+        self.graph.reset_graph()
+        self.graph.plot_widget.addItem(self.contour_label)  # Re-add the label
+        self.plot_gradient()
         self.plot_contour()
 
-    def plot_gradient_and_contour(self, x_from, x_to, y_from, y_to):
-        """
-        Function constructs series for gradient and contour
-
-        Parameters
-        ----------
-        x_from : float
-            Min grid x value
-        x_to : float
-            Max grid x value
-        y_from : float
-            Min grid y value
-        y_to : float
-            Max grid y value
-
-        Returns
-        -------
-        list
-            List containing series with background gradient and contour
-        """
-
-        # grid for gradient
-        x = np.linspace(x_from, x_to, self.grid_size)
-        y = np.linspace(y_from, y_to, self.grid_size)
+    def update_model(self):
+        self.Error.fitting_failed.clear()
+        self.model = None
+        self.probabilities_grid = None
+        if self.selected_data is not None and self.learner is not None:
+            try:
+                self.model = self.learner(self.selected_data)
+                self.model.name = self.learner_name
+            except Exception as e:
+                self.Error.fitting_failed(str(e))
+
+    ##############################
+    # Graph and its contents
+    def _add_graph(self):
+        self.graph = PolynomialPlot(self)
+        self.graph.plot_widget.setCursor(QCursor(Qt.CrossCursor))
+        self.mainArea.layout().addWidget(self.graph.plot_widget)
+        self.graph.point_width = 1
+
+        axis_color = self.palette().color(QPalette.Text)
+        axis_pen = QPen(axis_color)
+
+        tickfont = QFont(self.font())
+        tickfont.setPixelSize(max(int(tickfont.pixelSize() * 2 // 3), 11))
+
+        for pos in ("bottom", "left"):
+            axis = self.graph.plot_widget.getAxis(pos)
+            axis.setPen(axis_pen)
+            axis.setTickFont(tickfont)
+            axis.show()
+
+        self._hover_delegate = HoverEventDelegate(self.help_event)
+        self.graph.plot_widget.scene().installEventFilter(self._hover_delegate)
+
+        self.contour_label = label = QGraphicsTextItem()
+        label.setFlag(QGraphicsTextItem.ItemIgnoresTransformations)
+        self.graph.plot_widget.addItem(label)
+        label.hide()
+        # I'm not proud of this and will brew a coffee to the person who
+        # improves it (and comes to claim it)
+        self.graph.plot_widget.scene().leaveEvent = lambda *_: label.hide()
+
+    @staticmethod
+    def _contour_pen(value, hovered):
+        return pg.mkPen(0.2,
+            width=1 + 2 * hovered + (value == 0.5),
+            style=Qt.SolidLine)
+            # Alternative:
+            # Qt.SolidLine if hovered or value == 0.5 else Qt.DashDotLine
+
+    def help_event(self, event):
+        if self.probabilities_grid is None:
+            return
+
+        pos = event.scenePos()
+        pos = self.graph.plot_widget.mapToView(pos)
+
+        # The mouse hover width is a bit larger for easier hovering, but
+        # consequently more than one line can be hovered. Pick the middle one.
+        hovereds = [item for item in self.contours
+                    if item.mouseShape().contains(pos)]
+        hovered = hovereds[len(hovereds) // 2] if hovereds else None
+        # Set the pen for all lines, hovered and not hovered (any longer)
+        for item in self.contours:
+            item.setPen(self._contour_pen(item.value, item is hovered))
+
+        # Set the probability for the textitem at mouse position.
+        # If there is a hovered line - this acts as a line label.
+        # Otherwise, take the probability from the grid
+        label = self.contour_label
+        if hovered:
+            prob = hovered.value
+        else:
+            min_x, step_x, min_y, step_y = self.probabilities_grid_dimensions
+            prob = self.probabilities_grid.T[
+                int(np.clip(round((pos.x() - min_x) * step_x), 0, GRID_SIZE - 1)),
+                int(np.clip(round((pos.y() - min_y) * step_y), 0, GRID_SIZE - 1))]
+        prob_lab = f"{round(prob, 3):.3g}"
+        if "." not in prob_lab:
+            prob_lab += ".0"  # Showing just 0 or 1 looks ugly
+        label.setHtml(prob_lab)
+        font = label.font()
+        font.setBold(hovered is not None)
+        label.setFont(font)
+
+        # Position the label above and left from mouse; looks nices
+        rect = label.boundingRect()
+        spos = event.scenePos()
+        x, y = spos.x() - rect.width(), spos.y() - rect.height()
+        label.setPos(self.graph.plot_widget.mapToView(QPointF(x, y)))
+
+        label.show()
+        return True
+
+    def changeEvent(self, ev):
+        # This hides the label if the user alt-tabs out of the window
+        if ev.type() == QEvent.ActivationChange and not self.isActiveWindow():
+            self.contour_label.hide()
+        super().changeEvent(ev)
+
+    def plot_gradient(self):
+        if not self.model:
+            self.probabilities_grid = None
+            return
+
+        (min_x, max_x), (min_y, max_y) = self.graph.view_box.viewRange()
+        x = np.linspace(min_x, max_x, GRID_SIZE)
+        y = np.linspace(min_y, max_y, GRID_SIZE)
         self.xv, self.yv = np.meshgrid(x, y)
 
-        # parameters to predict from grid
         attr = np.hstack((self.xv.reshape((-1, 1)), self.yv.reshape((-1, 1))))
-        attr_data = Table.from_numpy(
-            self.selected_data.domain, attr,
-            np.array([[None]] * len(attr)),
-            np.array([[None]] * len(attr))
-        )
-
-        # results
-        self.probabilities_grid = self.model(attr_data, 1)[:, 0]\
+        nil = np.full((len(attr), 1), np.nan)
+        attr_data = Table.from_numpy(self.selected_data.domain, attr, nil, nil)
+
+        self.probabilities_grid = self.model(attr_data, 1)[:, 1] \
             .reshape(self.xv.shape)
+        self.probabilities_grid_dimensions = (
+            min_x, GRID_SIZE / (max_x - min_x),
+            min_y, GRID_SIZE / (max_y - min_y))
+
+        if not self._treelike:
+            self.probabilities_grid = self.blur_grid(self.probabilities_grid)
+
+        bitmap = self.probabilities_grid.copy()
+        bitmap *= 255
+        bitmap = bitmap.astype(np.uint8)
+
+        class_var = self.selected_data.domain.class_var
+        h1, s1, v1 = rgb_to_hsv(*class_var.colors[1] / 255)
+        palette = np.vstack((
+            np.linspace([h1, 0, 0.8], [h1, 0, 1], 128),
+            np.linspace([h1, 0, 1], [h1, s1 * 0.5, 0.7 + 0.3 * v1], 128)
+        ))
+        palette = 255 * np.array([hsv_to_rgb(*col) for col in palette])
+        palette = palette.astype(int)
+
+        density_img = pg.ImageItem(bitmap.T, lut=palette)
 
-        blurred = self.blur_grid(self.probabilities_grid)
+        density_img.setRect(QRectF(min_x, min_y,
+                                   max_x - min_x, max_y - min_y))
+        density_img.setZValue(-1)
+        self.graph.plot_widget.addItem(density_img, ignoreBounds=True)
 
-        is_tree = type(self.learner) in [RandomForestLearner, TreeLearner]
-        return self.plot_gradient(self.xv, self.yv,
-                                  self.probabilities_grid
-                                  if is_tree else blurred)
+    def remove_contours(self):
+        while self.contours:
+            self.graph.plot_widget.removeItem(self.contours.pop())
 
-    def plot_gradient(self, x, y, grid):
-        """
-        Function constructs background gradient
-        """
-        return [dict(data=[[x[j, k], y[j, k], grid[j, k]] for j in range(len(x))
-                           for k in range(y.shape[1])],
-                     grid_width=self.grid_size,
-                     type="contour")]
+    @property
+    def _treelike(self):
+        return isinstance(self.learner, (RandomForestLearner, TreeLearner))
 
     def plot_contour(self):
-        """
-        Function constructs contour lines
-        """
-        self.scatter.remove_contours()
-        if not self.data:
+        self.remove_contours()
+        if self.probabilities_grid is None or not self.contours_enabled:
             return
-        if self.contours_enabled:
-            is_tree = type(self.learner) in [RandomForestLearner, TreeLearner]
-            # tree does not need smoothing
-            contour = Contour(
-                self.xv, self.yv, self.probabilities_grid
-                if is_tree else self.blur_grid(self.probabilities_grid))
-            contour_lines = contour.contours(
-                np.hstack(
-                    (np.arange(0.5, 0, - self.contour_step)[::-1],
-                     np.arange(0.5 + self.contour_step, 1, self.contour_step))))
-            # we want to have contour for 0.5
-
-            series = []
-            count = 0
-            for key, value in contour_lines.items():
-                for line in value:
-                    if (len(line) > self.degree and
-                                type(self.learner) not in
-                                [RandomForestLearner, TreeLearner]):
-                        # if less than degree interpolation fails
-                        tck, u = splprep(
-                            [list(x) for x in zip(*reversed(line))],
-                            s=0.001, k=self.degree,
-                            per=(len(line)
-                                 if np.allclose(line[0], line[-1])
-                                 else 0))
-                        new_int = np.arange(0, 1.01, 0.01)
-                        interpol_line = np.array(splev(new_int, tck)).T.tolist()
-                    else:
-                        interpol_line = line
-
-                    series.append(dict(data=self.labeled(interpol_line, count),
-                                       color=self.contour_color,
-                                       type="spline",
-                                       lineWidth=0.5,
-                                       showInLegend=False,
-                                       marker=dict(enabled=False),
-                                       name="%g" % round(key, 2),
-                                       enableMouseTracking=False
-                                       ))
-                    count += 1
-            self.scatter.add_series(series)
-        self.scatter.redraw_series()
+
+        contour = Contour(self.xv, self.yv, self.probabilities_grid)
+        contour_lines = contour.contours(np.arange(0.1, 1, 0.1))
+        for key, value in contour_lines.items():
+            for line in value:
+                if len(line) > self.degree and not self._treelike:
+                    tck, u = splprep(
+                        [list(x) for x in zip(*reversed(line))],
+                        s=0.001, k=self.degree,
+                        per=(len(line) if np.allclose(line[0], line[-1])
+                             else 0))
+                    new_int = np.arange(0, 1.01, 0.01)
+                    interpol_line = np.array(splev(new_int, tck)).T.tolist()
+                else:
+                    interpol_line = line
+                contour = pg.PlotCurveItem(
+                    *np.array(list(interpol_line)).T,
+                    pen=self._contour_pen(key, False))
+                # The hover region can be narrowed by calling setClickable
+                # (with False, to keep it unclickable)
+                contour.value = key
+                self.graph.plot_widget.addItem(contour)
+                self.contours.append(contour)
 
     @staticmethod
     def blur_grid(grid):
@@ -511,158 +472,67 @@ def blur_grid(grid):
                                                        (grid < 0.55)]
         return filtered
 
-    @staticmethod
-    def labeled(data, no):
-        """
-        Function labels data with contour levels
-        """
-        point = (no * 5) # to avoid points on same positions
-        point += (1 if point == 0 else 0)
-        point %= len(data)
-
-        data[point] = dict(
-            x=data[point][0],
-            y=data[point][1],
-            dataLabels=dict(
-                enabled=True,
-                format="{series.name}",
-                verticalAlign='middle',
-                style=dict(
-                    fontWeight="normal",
-                    color=OWPolynomialClassification.contour_color,
-                    textShadow=False
-                )))
-        return data
-
-    def select_data(self):
-        """
-        Function takes two selected columns from data table and merge them
-        in new Orange.data.Table
-
-        Returns
-        -------
-        Table
-            Table with selected columns
-        """
-        self.Error.clear()
-
-        attr_x = self.data.domain[self.attr_x]
-        attr_y = self.data.domain[self.attr_y]
-        cols = []
-        for attr in (attr_x, attr_y):
-            subset = self.data[:, attr]
-            cols.append(subset.X if not sp.issparse(subset.X) else subset.X.toarray())
-        x = np.column_stack(cols)
-        y_c = self.data.Y[:, None] if not sp.issparse(self.data.Y) else self.data.Y.toarray()
-
-        if np.isnan(x).all(axis=0).any():
-            self.Error.all_none_data()
-            return None
+    # The following methods are required by OWScatterPlotBase
+    def get_coordinates_data(self):
+        if not self.selected_data:
+            return None, None
+        return self.selected_data.X.T
 
-        cls_domain = self.data.domain.class_var
-        target_idx = cls_domain.values.index(self.target_class)
-        other_value = cls_domain.values[(target_idx + 1) % 2]
+    def get_color_data(self):
+        return self.selected_data and self.selected_data.Y
 
-        class_domain = [DiscreteVariable(
-            name="Transformed " + self.data.domain.class_var.name,
-            values=(self.target_class, 'Others'
-            if len(cls_domain.values) > 2 else other_value))]
+    def get_palette(self):
+        return self.selected_data and self.selected_data.domain.class_var.palette
 
-        domain = Domain(
-            [attr_x, attr_y],
-            class_domain,
-            [self.data.domain.class_var])
-        y = [(0 if d.get_class().value == self.target_class else 1)
-             for d in self.data]
+    def get_color_labels(self):
+        return self.selected_data and self.selected_data.domain.class_var.values
 
-        return Table.from_numpy(domain, x, y, y_c)
+    def is_continuous_color(self):
+        return False
 
-    def apply(self):
-        """
-        Applies leaner and sends new model and coefficients
-        """
-        self.send_learner()
-        self.update_model()
-        self.send_coefficients()
-        if any(a is None for a in (self.data, self.model)):
-            self.set_empty_plot()
-        else:
-            self.replot()
-        self.send_data()
+    get_size_data = get_shape_data = get_shape_labels = \
+        get_subset_mask = get_label_data = get_tooltip = selection_changed = \
+        lambda *_: None
 
+    ##############################
+    # Output signal-related stuff
     def send_learner(self):
-        """
-        Function sends learner on widget's output
-        """
         if self.learner is not None:
             self.learner.name = self.learner_name
         self.Outputs.learner.send(self.learner)
 
-    def update_model(self):
-        """
-        Function sends model on widget's output
-        """
-        self.Error.fitting_failed.clear()
-        self.model = None
-        if self.data is not None and self.learner is not None:
-            self.selected_data = self.select_data()
-            if self.selected_data is not None:
-                try:
-                    self.model = self.learner(self.selected_data)
-                    self.model.name = self.learner_name
-                    self.model.instances = self.selected_data
-                except Exception as e:
-                    self.Error.fitting_failed(str(e))
-
+    def send_model(self):
         self.Outputs.model.send(self.model)
 
     def send_coefficients(self):
-        """
-        Function sends coefficients on widget's output if model has them
-        """
-
-        if (self.model is not None and
-                isinstance(self.learner, LogisticRegressionLearner) and
-                hasattr(self.model, 'skl_model')):
-            model = self.model.skl_model
-            domain = Domain(
-                [ContinuousVariable("coef")], metas=[StringVariable("name")])
-            coefficients = (model.intercept_.tolist() +
-                            model.coef_[0].tolist())
-
-            data = self.model.instances
-            for preprocessor in self.learner.preprocessors:
-                data = preprocessor(data)
-            names = ["Intercept"] + [x.name for x in data.domain.attributes]
-
-            coefficients_table = Table.from_list(
-                domain, list(zip(coefficients, names)))
-            self.Outputs.coefficients.send(coefficients_table)
-        else:
+        if (self.model is None
+                or not isinstance(self.learner, LogisticRegressionLearner)
+                or not hasattr(self.model, 'skl_model')):
             self.Outputs.coefficients.send(None)
-
-    def send_data(self):
-        """
-        Function sends data on widget's output
-        """
-        if self.data is not None:
-            data = self.selected_data
-            self.Outputs.data.send(data)
             return
-        self.Outputs.data.send(None)
 
-    def add_bottom_buttons(self):
-        pass
+        model = self.model.skl_model
+        domain = Domain(
+            [ContinuousVariable("coef")], metas=[StringVariable("name")])
+        coefficients = model.intercept_.tolist() + model.coef_[0].tolist()
+        names = ["Intercept"] + [x.name for x in self.model.domain.attributes]
+        coefficients_table = Table.from_list(
+            domain, list(zip(coefficients, names)))
+        self.Outputs.coefficients.send(coefficients_table)
+
+    def send_data(self):
+        if self.selected_data is None:
+            self.Outputs.data.send(None)
+        else:
+            expanded = PolynomialTransform(self.degree)(self.selected_data)
+            self.Outputs.data.send(expanded)
 
     def send_report(self):
-        if self.data is None:
+        if self.selected_data is None:
             return
-        caption = report.render_items_vert((
-             ("Polynomial Expansion", self.degree),
-        ))
-        self.report_plot(self.scatter)
-        if caption:
-            self.report_caption(caption)
+        name = "" if self.degree == 1 \
+            else f"Model with polynomial expansion {self.degree}"
+        self.report_plot(name=name, plot=self.graph.plot_widget)
 
 
 if __name__ == "__main__":
diff --git a/setup.py b/setup.py
index 5903577..a633303 100644
--- a/setup.py
+++ b/setup.py
@@ -33,7 +33,7 @@
 ]
 
 INSTALL_REQUIRES = [
-    'Orange3 >=3.27.0',
+    'Orange3 >=3.27.1',
     'BeautifulSoup4',
     'numpy'
 ]
diff --git a/tox.ini b/tox.ini
index 6017af0..2484edc 100644
--- a/tox.ini
+++ b/tox.ini
@@ -27,7 +27,7 @@ deps =
     pyqt5==5.12.*
     pyqtwebengine==5.12.*
     oldest: scikit-learn~=0.22.0
-    oldest: orange3==3.27.0
+    oldest: orange3==3.27.1
     latest: git+git://github.com/biolab/orange3.git#egg=orange3
     latest: git+git://github.com/biolab/orange-canvas-core.git#egg=orange-canvas-core
     latest: git+git://github.com/biolab/orange-widget-base.git#egg=orange-widget-base


Test patch:
diff --git a/orangecontrib/educational/widgets/tests/test_owpolynomialclassification.py b/orangecontrib/educational/widgets/tests/test_owpolynomialclassification.py
index cc2928d..ef69357 100644
--- a/orangecontrib/educational/widgets/tests/test_owpolynomialclassification.py
+++ b/orangecontrib/educational/widgets/tests/test_owpolynomialclassification.py
@@ -1,544 +1,255 @@
-from functools import reduce
 import unittest
+from unittest.mock import Mock
 
 import numpy as np
 import scipy.sparse as sp
 from numpy.testing import assert_array_equal
 
-from Orange.regression import LinearRegressionLearner
-from Orange.widgets.tests.base import WidgetTest
 from Orange.data import Table, ContinuousVariable, Domain, DiscreteVariable
-from Orange.classification import (
-    LogisticRegressionLearner,
-    TreeLearner,
-    RandomForestLearner, SVMLearner)
+from Orange.classification import \
+    LogisticRegressionLearner, TreeLearner, SVMLearner
 from Orange.preprocess.preprocess import Continuize, Discretize
+from Orange.widgets.tests.base import WidgetTest
+
 from orangecontrib.educational.widgets.owpolynomialclassification import \
-    OWPolynomialClassification
+    OWPolynomialClassification, GRID_SIZE
 from orangecontrib.educational.widgets.utils.polynomialtransform import \
     PolynomialTransform
 
 
-class TestOWPolynomialClassification(WidgetTest):
-
+class TestOWPolynomialClassificationNoGrid(WidgetTest):
+    # Tests with mocked computation of probability grid and contours
+    # These are never tested and just slow everything down.
     def setUp(self):
-        self.widget = self.create_widget(OWPolynomialClassification)  # type: OWPolynomialClassification
-        self.iris = Table.from_file("iris")
-
-    def test_add_main_layout(self):
-        """
-        With this test we check if everything is ok on widget init
-        """
-        w = self.widget
-
-        # add main layout is called when function is initialized
-
-        # check just if it is initialize on widget start
-        self.assertIsNotNone(w.options_box)
-        self.assertIsNotNone(w.cbx)
-        self.assertIsNotNone(w.cby)
-        self.assertIsNotNone(w.target_class_combobox)
-        self.assertIsNotNone(w.degree_spin)
-        self.assertIsNotNone(w.plot_properties_box)
-        self.assertIsNotNone(w.contours_enabled_checkbox)
-        self.assertIsNotNone(w.contour_step_slider)
-        self.assertIsNotNone(w.scatter)
-
-        # default learner must be logistic regression
-        self.assertEqual(w.LEARNER.name, LogisticRegressionLearner.name)
+        # type: OWPolynomialClassification
+        self.widget = self.create_widget(OWPolynomialClassification)
 
-        # widget have to be resizable
-        self.assertTrue(w.resizing_enabled)
+        def plot_gradient():
+            self.widget.probabilities_grid = self.widget.model and np.zeros((GRID_SIZE, GRID_SIZE))
 
-        # learner should be Logistic regression
-        self.assertTrue(isinstance(w.learner, LogisticRegressionLearner))
+        self.widget.plot_gradient = plot_gradient
+        self.widget.plot_contour = Mock()
 
-        # preprocessor should be PolynomialTransform
-        self.assertEqual(
-            type(w.default_preprocessor), type(PolynomialTransform))
-
-        # check if there is learner on output
-        self.assertEqual(self.get_output(w.Outputs.learner), w.learner)
+        self.iris = Table.from_file("iris")
 
-        # model and coefficients should be none because of no data
+    def test_init(self):
+        w = self.widget
+        self.assertIsInstance(w.learner, LogisticRegressionLearner)
+        self.assertIsInstance(self.get_output(w.Outputs.learner),
+                              LogisticRegressionLearner)
         self.assertIsNone(self.get_output(w.Outputs.model))
         self.assertIsNone(self.get_output(w.Outputs.coefficients))
 
-        # this parameters are none because no plot should be called
-        self.assertIsNone(w.xv)
-        self.assertIsNone(w.yv)
-        self.assertIsNone(w.probabilities_grid)
-
-    def test_set_learner_empty(self):
-        """
-        Test if learner is set correctly when no learner provided
-        """
-        w = self.widget
-
-        # check if empty
-        self.assertEqual(w.learner_other, None)
-        self.assertTrue(isinstance(w.learner, LogisticRegressionLearner))
-        self.assertTrue(isinstance(w.learner, w.LEARNER))
-        self.assertEqual(
-            type(self.get_output(w.Outputs.learner)), type(LogisticRegressionLearner()))
-
     def test_set_learner(self):
-        """
-        Test if learner is set correctly
-        """
         w = self.widget
+        self.assertIsInstance(w.learner, LogisticRegressionLearner)
+        self.assertIsInstance(self.get_output(w.Outputs.learner),
+                              LogisticRegressionLearner)
 
-        learner = TreeLearner()
+        self.send_signal(w.Inputs.learner, SVMLearner())
+        self.assertIsInstance(self.get_output(w.Outputs.learner), SVMLearner)
 
-        self.send_signal(w.Inputs.learner, learner)
-        # check if learners set correctly
-        self.assertEqual(w.learner_other, learner)
-        self.assertEqual(type(w.learner), type(learner))
-        self.assertEqual(type(self.get_output(w.Outputs.learner)), type(learner))
-
-        # after learner is removed there should be LEARNER used
         self.send_signal(w.Inputs.learner, None)
-        self.assertEqual(w.learner_other, None)
-        self.assertTrue(isinstance(w.learner, LogisticRegressionLearner))
-        self.assertTrue(isinstance(w.learner, w.LEARNER))
-        self.assertEqual(
-            type(self.get_output(w.Outputs.learner)), type(LogisticRegressionLearner()))
+        self.assertIsInstance(w.learner, LogisticRegressionLearner)
+        self.assertIsInstance(self.get_output(w.Outputs.learner),
+                              LogisticRegressionLearner)
 
     def test_set_preprocessor(self):
-        """
-        Test preprocessor set
-        """
         w = self.widget
 
         preprocessor = Continuize()
-
-        # check if empty
-        self.assertIn(w.preprocessors, [[], None])
-
         self.send_signal(w.Inputs.preprocessor, preprocessor)
 
-        # check preprocessor is set
-        self.assertEqual(w.preprocessors, [preprocessor])
-        self.assertIn(preprocessor, self.get_output(w.Outputs.learner).preprocessors)
-
-        # remove preprocessor
-        self.send_signal(w.Inputs.preprocessor, None)
+        for learner in (None, SVMLearner(), None):
+            self.send_signal(w.Inputs.learner, learner)
 
-        self.assertIn(w.preprocessors, [[], None])
-        self.assertNotIn(preprocessor, self.get_output(w.Outputs.learner).preprocessors)
+            self.assertEqual(w.preprocessors, [preprocessor])
+            preprocessors = self.get_output(w.Outputs.learner).preprocessors
+            self.assertIn(preprocessor, preprocessors)
+            self.assertTrue(isinstance(pp, PolynomialTransform)
+                            for pp in preprocessors)
 
-        # change preprocessor
-        preprocessor = Continuize()
-        self.send_signal(w.Inputs.preprocessor, preprocessor)
+            self.send_signal(w.Inputs.preprocessor, None)
+            self.assertIn(w.preprocessors, [[], None])
+            self.assertNotIn(preprocessor, self.get_output(w.Outputs.learner).preprocessors)
+            self.assertTrue(isinstance(pp, PolynomialTransform)
+                            for pp in preprocessors)
 
-        # check preprocessor is set
-        self.assertEqual(w.preprocessors, [preprocessor])
-        self.assertIn(preprocessor, self.get_output(w.Outputs.learner).preprocessors)
+            self.send_signal(w.Inputs.preprocessor, preprocessor)
+            self.assertEqual(w.preprocessors, [preprocessor])
+            self.assertIn(preprocessor, self.get_output(w.Outputs.learner).preprocessors)
+            self.assertTrue(isinstance(pp, PolynomialTransform)
+                            for pp in preprocessors)
 
     def test_set_data(self):
-        """
-        Test widget behavior when data set
-        """
         w = self.widget
-
-        num_continuous_attributes = sum(
-            True for var in self.iris.domain.attributes
-            if isinstance(var, ContinuousVariable))
+        attr0, attr1 = self.iris.domain.attributes[:2]
+        class_vals = self.iris.domain.class_var.values
 
         self.send_signal(w.Inputs.data, self.iris[::15])
 
-        # widget does not have any problems with that data set so
-        # everything should be fine
-        self.assertEqual(w.cbx.count(), num_continuous_attributes)
-        self.assertEqual(w.cby.count(), num_continuous_attributes)
-        self.assertEqual(
-            w.target_class_combobox.count(),
-            len(self.iris.domain.class_var.values))
-        self.assertEqual(w.cbx.currentText(), self.iris.domain[0].name)
-        self.assertEqual(w.cby.currentText(), self.iris.domain[1].name)
-        self.assertEqual(
-            w.target_class_combobox.currentText(),
-            self.iris.domain.class_var.values[0])
-
-        self.assertEqual(w.attr_x, self.iris.domain[0].name)
-        self.assertEqual(w.attr_y, self.iris.domain[1].name)
-        self.assertEqual(w.target_class, self.iris.domain.class_var.values[0])
-
-        # change showed attributes
-        w.attr_x = self.iris.domain[1].name
-        w.attr_y = self.iris.domain[2].name
-        w.target_class = self.iris.domain.class_var.values[1]
-
-        self.assertEqual(w.cbx.currentText(), self.iris.domain[1].name)
-        self.assertEqual(w.cby.currentText(), self.iris.domain[2].name)
-        self.assertEqual(
-            w.target_class_combobox.currentText(),
-            self.iris.domain.class_var.values[1])
-
-        self.assertEqual(w.attr_x, self.iris.domain[1].name)
-        self.assertEqual(w.attr_y, self.iris.domain[2].name)
-        self.assertEqual(w.target_class, self.iris.domain.class_var.values[1])
+        self.assertEqual(w.var_model.rowCount(), 4)
+        self.assertEqual(w.controls.target_class.count(), len(class_vals))
+        self.assertIs(w.attr_x, attr0)
+        self.assertIs(w.attr_y, attr1)
+        self.assertEqual(w.target_class, class_vals[0])
 
         # remove data set
         self.send_signal(w.Inputs.data, None)
-        self.assertEqual(w.cbx.count(), 0)
-        self.assertEqual(w.cby.count(), 0)
-        self.assertEqual(w.target_class_combobox.count(), 0)
+        self.assertEqual(w.var_model.rowCount(), 0)
+        self.assertEqual(w.controls.target_class.count(), 0)
+
+    def _set_iris(self):
+        # Set some data so the widget goes up and test can check that it is
+        # torn down when erroneous data is received
+        w = self.widget
+        self.send_signal(w.Inputs.data, self.iris)
+        self.assert_all_up()
+
+    def assert_all_up(self):
+        w = self.widget
+        self.assertNotEqual(len(w.var_model), 0)
+        self.assertNotEqual(w.controls.target_class.count(), 0)
+        self.assertIsNotNone(w.data)
+        self.assertIsNotNone(w.selected_data)
+        self.assertIsNotNone(w.model)
+        self.assertIsNotNone(w.probabilities_grid)
+
+    def assert_all_down(self):
+        w = self.widget
+        self.assertEqual(len(w.var_model), 0)
+        self.assertEqual(w.controls.target_class.count(), 0)
+        self.assertIsNone(w.data)
+        self.assertIsNone(w.selected_data)
+        self.assertIsNone(w.model)
+        self.assertIsNone(w.probabilities_grid)
 
     def test_set_data_no_class(self):
-        """
-        Test widget on data with no class
-        """
         w = self.widget
 
-        table_no_class = Table.from_list(
-            Domain([ContinuousVariable("x"), ContinuousVariable("y")]),
-            [[1, 2], [2, 3]])
+        self._set_iris()
+
+        table_no_class = self.iris.transform(Domain(self.iris.domain.attributes, []))
         self.send_signal(w.Inputs.data, table_no_class)
+        self.assert_all_down()
 
-        self.assertEqual(w.cbx.count(), 0)
-        self.assertEqual(w.cby.count(), 0)
-        self.assertEqual(w.target_class_combobox.count(), 0)
-        self.assertTrue(w.Error.no_class.is_shown())
+    def test_set_data_regression(self):
+        w = self.widget
+        self._set_iris()
+
+        table_regr = Table.from_numpy(
+            Domain(self.iris.domain.attributes[:3],
+                   self.iris.domain.attributes[3]),
+            self.iris.X[:, :3], self.iris.X[:, 3])
+        self.send_signal(w.Inputs.data, table_regr)
+        self.assert_all_down()
 
     def test_set_data_one_class(self):
-        """
-        Test widget on data with one class variable
-        """
         w = self.widget
+        self._set_iris()
 
-        table_one_class = Table.from_list(
-            Domain([ContinuousVariable("x"), ContinuousVariable("y")],
+        table_one_class = Table.from_numpy(
+            Domain(self.iris.domain.attributes,
                    DiscreteVariable("a", values=("k", ))),
-            [[1, 2], [2, 3]], [0, 0])
+            self.iris.X, np.zeros(150))
         self.send_signal(w.Inputs.data, table_one_class)
 
-        self.assertEqual(w.cbx.count(), 0)
-        self.assertEqual(w.cby.count(), 0)
-        self.assertEqual(w.target_class_combobox.count(), 0)
-        self.assertTrue(w.Error.no_class.is_shown())
+        self.assertEqual(len(w.var_model), 4)
+        self.assertEqual(w.controls.target_class.count(), 1)
+        self.assertIsNotNone(w.data)
+        self.assertIsNotNone(w.selected_data)
+        self.assertIsNone(w.model)
+        self.assertIsNone(w.probabilities_grid)
+        self.assertTrue(w.Error.fitting_failed.is_shown())
 
     def test_set_data_wrong_var_number(self):
-        """
-        Test widget on data with not enough continuous variables
-        """
         w = self.widget
 
-        #
+        self._set_iris()
+
         table_no_enough_cont = Table.from_numpy(
             Domain(
                 [ContinuousVariable("x"),
                  DiscreteVariable("y", values=("a", "b"))],
-                ContinuousVariable("a")),
+                DiscreteVariable("a", values=("a", "b"))),
             [[1, 0], [2, 1]], [0, 0])
         self.send_signal(w.Inputs.data, table_no_enough_cont)
 
-        self.assertEqual(w.cbx.count(), 0)
-        self.assertEqual(w.cby.count(), 0)
-        self.assertEqual(w.target_class_combobox.count(), 0)
-        self.assertTrue(w.Error.to_few_features.is_shown())
-
-    def test_init_learner(self):
-        """
-        Test init
-        """
-        w = self.widget
-
-        learner = TreeLearner()
-
-        # check if empty
-        self.assertTrue(isinstance(w.learner, LogisticRegressionLearner))
-        self.assertTrue(isinstance(w.learner, w.LEARNER))
-        self.assertTrue(
-            reduce(lambda x, y: x or isinstance(y, w.default_preprocessor),
-                   w.learner.preprocessors, False))
-
-        self.send_signal(w.Inputs.learner, learner)
-
-        # check if learners set correctly
-        self.assertEqual(type(w.learner), type(learner))
-
-        # after learner is removed there should be LEARNER used
-        self.send_signal(w.Inputs.learner, None)
-        self.assertTrue(isinstance(w.learner, LogisticRegressionLearner))
-        self.assertTrue(isinstance(w.learner, w.LEARNER))
-        self.assertTrue(
-            reduce(lambda x, y: x or isinstance(y, w.default_preprocessor),
-                   w.learner.preprocessors, False))
-
-        # set it again just in case something goes wrong
-        learner = RandomForestLearner()
-        self.send_signal(w.Inputs.learner, learner)
-
-        self.assertEqual(type(w.learner), type(learner))
-        self.assertTrue(
-            reduce(lambda x, y: x or isinstance(y, w.default_preprocessor),
-                   w.learner.preprocessors, False))
-
-        # change learner this time not from None
-        learner = TreeLearner()
-        self.send_signal(w.Inputs.learner, learner)
-
-        self.assertEqual(type(w.learner), type(learner))
-        self.assertTrue(
-            reduce(lambda x, y: x or isinstance(y, w.default_preprocessor),
-                   w.learner.preprocessors, False))
-
-        # set other preprocessor
-        preprocessor = Discretize
-        # selected this preprocessor because know that not exist in LogReg
-        self.send_signal(w.Inputs.preprocessor, preprocessor())
-
-        self.assertEqual(type(w.learner), type(learner))
-        self.assertTrue(
-            reduce(lambda x, y: x or isinstance(y, w.default_preprocessor),
-                   w.learner.preprocessors, False))
-        self.assertTrue(
-            reduce(lambda x, y: x or isinstance(y, preprocessor),
-                   w.learner.preprocessors, False))
-
-        # remove preprocessor
-        self.send_signal(w.Inputs.preprocessor, None)
-        self.assertEqual(type(w.learner), type(learner))
-        self.assertTrue(
-            reduce(lambda x, y: x or isinstance(y, w.default_preprocessor),
-                   w.learner.preprocessors, False))
-
-        self.assertFalse(reduce(lambda x, y: x or isinstance(y, preprocessor),
-                                w.learner.preprocessors, False))
-
-    def test_replot(self):
-        """
-        Test everything that is possible to test in replot
-        This function tests all replot functions
-        """
-        w = self.widget
-
-        w.replot()
-
-        # test nothing happens when no data
-        self.assertIsNone(w.xv)
-        self.assertIsNone(w.yv)
-        self.assertIsNone(w.probabilities_grid)
-
-        # when data available plot happens
-        self.send_signal(w.Inputs.data, self.iris)
-        self.assertIsNotNone(w.xv)
-        self.assertIsNotNone(w.yv)
-        self.assertIsNotNone(w.probabilities_grid)
-        self.assertTupleEqual(
-            (w.grid_size, w.grid_size), w.probabilities_grid.shape)
-        self.assertTupleEqual((w.grid_size, w.grid_size), w.xv.shape)
-        self.assertTupleEqual((w.grid_size, w.grid_size), w.yv.shape)
-
-        # check that everything works fine when contours enabled/disabled
-        w.contours_enabled_checkbox.click()
-
-        self.assertIsNotNone(w.xv)
-        self.assertIsNotNone(w.yv)
-        self.assertIsNotNone(w.probabilities_grid)
-        self.assertTupleEqual(
-            (w.grid_size, w.grid_size), w.probabilities_grid.shape)
-        self.assertTupleEqual((w.grid_size, w.grid_size), w.xv.shape)
-        self.assertTupleEqual((w.grid_size, w.grid_size), w.yv.shape)
-
-        w.contours_enabled_checkbox.click()
-
-        self.assertIsNotNone(w.xv)
-        self.assertIsNotNone(w.yv)
-        self.assertIsNotNone(w.probabilities_grid)
-        self.assertTupleEqual(
-            (w.grid_size, w.grid_size), w.probabilities_grid.shape)
-        self.assertTupleEqual((w.grid_size, w.grid_size), w.xv.shape)
-        self.assertTupleEqual((w.grid_size, w.grid_size), w.yv.shape)
-
-        # when remove data
-        self.send_signal(w.Inputs.data, None)
-
-        self.assertIsNone(w.xv)
-        self.assertIsNone(w.yv)
-        self.assertIsNone(w.probabilities_grid)
-
-    def test_blur_grid(self):
-        w = self.widget
-
-        self.send_signal(w.Inputs.data, self.iris)
-        # here we can check that 0.5 remains same
-        assert_array_equal(w.probabilities_grid == 0.5,
-                           w.blur_grid(w.probabilities_grid) == 0.5)
+        self.assert_all_down()
+        self.assertTrue(w.Error.num_features.is_shown())
 
     def test_select_data(self):
-        """
-        Check if select data works properly
-        """
         w = self.widget
-
         self.send_signal(w.Inputs.data, self.iris)
 
-        selected_data = w.select_data()
-        self.assertEqual(len(selected_data.domain.attributes), 2)
-        self.assertIsNotNone(selected_data.domain.class_var)
-        self.assertEqual(len(selected_data.domain.metas), 1)
-        # meta with information about real cluster
-        self.assertEqual(len(selected_data), len(self.iris))
+        w.select_data()
+        np.testing.assert_equal(w.selected_data.X, self.iris.X[:, :2])
+        np.testing.assert_equal(w.selected_data.Y, [1] * 50 + [0] * 100)
 
-        # selected data none when one column only Nones
-        data = Table.from_numpy(
-            Domain([ContinuousVariable('a'), ContinuousVariable('b')],
-                   DiscreteVariable('c', values=('a', 'b'))),
-            [[1, None], [1, None]], [0, 1]
-        )
-        self.send_signal(w.Inputs.data, data)
-        selected_data = w.select_data()
-        self.assertIsNone(selected_data)
+        w.attr_x, w.attr_y = self.iris.domain[1:3]
+        w.select_data()
+        np.testing.assert_equal(w.selected_data.X, self.iris.X[:, 1:3])
+        np.testing.assert_equal(w.selected_data.Y, [1] * 50 + [0] * 100)
+        self.assertIsNotNone(w.model)
+        self.assertIsNotNone(w.probabilities_grid)
 
+        # data without valid rows
         data = Table.from_numpy(
             Domain([ContinuousVariable('a'), ContinuousVariable('b')],
                    DiscreteVariable('c', values=('a', 'b'))),
-            [[None, None], [None, None]], [0, 1]
+            [[1, None], [None, 1]], [0, 1]
         )
         self.send_signal(w.Inputs.data, data)
-        selected_data = w.select_data()
-        self.assertIsNone(selected_data)
-
-    def test_send_learner(self):
-        """
-        Test if correct learner on output
-        """
-        w = self.widget
-
-        self.assertEqual(self.get_output(w.Outputs.learner), w.learner)
-        self.assertTrue(isinstance(self.get_output(w.Outputs.learner), w.LEARNER))
-
-        # set new learner
-        learner = TreeLearner
-        self.send_signal(w.Inputs.learner, learner())
-        self.process_events()
-        self.assertEqual(self.get_output(w.Outputs.learner), w.learner)
-        self.assertTrue(isinstance(self.get_output(w.Outputs.learner), learner))
-
-        # back to default learner
-        self.send_signal(w.Inputs.learner, None)
-        self.process_events()
-        self.assertEqual(self.get_output(w.Outputs.learner), w.learner)
-        self.assertTrue(isinstance(self.get_output(w.Outputs.learner), w.LEARNER))
+        w.select_data()
+        self.assertIsNone(w.selected_data)
+        self.assertIsNone(w.model)
+        self.assertIsNone(w.probabilities_grid)
 
     def test_update_model(self):
-        """
-        Function check if correct model is on output
-        """
         w = self.widget
-
-        # when no data
         self.assertIsNone(w.model)
         self.assertIsNone(self.get_output(w.Outputs.model))
 
-        # set data
         self.send_signal(w.Inputs.data, self.iris)
         self.assertIsNotNone(w.model)
         self.assertEqual(w.model, self.get_output(w.Outputs.model))
 
-        # remove data
         self.send_signal(w.Inputs.data, None)
         self.assertIsNone(w.model)
         self.assertIsNone(self.get_output(w.Outputs.model))
 
-    @unittest.skip("Travis fails: TimeoutError")
     def test_send_coefficients(self):
-        """
-        Coefficients are only available if Logistic regression is used
-        """
         w = self.widget
 
-        # none when no data (model not build)
         self.assertIsNone(self.get_output(w.Outputs.coefficients))
 
-        # by default LogisticRegression so coefficients exists
         self.send_signal(w.Inputs.data, self.iris)
 
-        # to check correctness before degree is changed
-        num_coefficients = sum(i + 1 for i in range(w.degree + 1))
-        self.assertEqual(len(self.get_output(w.Outputs.coefficients)), num_coefficients)
-
-        # change degree
-        for j in range(1, 6):
-            w.degree_spin.setValue(j)
-            num_coefficients = sum(i + 1 for i in range(w.degree + 1))
+        for w.degree in range(1, 6):
+            w._on_degree_changed()
+            self.assertEqual(
+                len(self.get_output(w.Outputs.coefficients)),
+                (w.degree + 1) * (w.degree + 2) // 2,
+                f"at degree={w.degree}")
             self.assertEqual(
-                len(self.get_output(w.Outputs.coefficients)), num_coefficients)
+                len(self.get_output(w.Outputs.data).domain.attributes),
+                (w.degree + 1) * (w.degree + 2) // 2 - 1,
+                f"at degree={w.degree}")
 
         # change learner which does not have coefficients
         learner = TreeLearner
         self.send_signal(w.Inputs.learner, learner())
         self.assertIsNone(self.get_output(w.Outputs.coefficients))
+        self.assertIsNotNone(self.get_output(w.Outputs.data))
 
-        # remove learner
         self.send_signal(w.Inputs.learner, None)
-
-        # to check correctness before degree is changed
-        num_coefficients = sum(i + 1 for i in range(w.degree + 1))
-        self.assertEqual(
-            len(self.get_output(w.Outputs.coefficients)), num_coefficients)
-
-        # change degree
-        for j in range(1, 6):
-            w.degree_spin.setValue(j)
-            num_coefficients = sum(i + 1 for i in range(w.degree + 1))
-            self.assertEqual(
-                len(self.get_output(w.Outputs.coefficients)), num_coefficients)
-
-        # manulay set LogisticRegression
-        self.send_signal(w.Inputs.learner, LogisticRegressionLearner())
-
-        # to check correctness before degree is changed
-        num_coefficients = sum(i + 1 for i in range(w.degree + 1))
-        self.assertEqual(len(self.get_output(w.Outputs.coefficients)), num_coefficients)
-
-        # change degree
-        for j in range(1, 6):
-            w.degree_spin.setValue(j)
-            num_coefficients = sum(i + 1 for i in range(w.degree + 1))
-            self.assertEqual(
-                len(self.get_output(w.Outputs.coefficients)), num_coefficients)
-
-    def test_send_data(self):
-        """
-        Check data output signal
-        """
-        w = self.widget
-
-        self.assertIsNone(self.get_output(w.Outputs.data))
-
-        self.send_signal(w.Inputs.data, self.iris)
-
-        # check correct number of attributes
-        for j in range(1, 6):
-            w.degree_spin.setValue(j)
-            self.assertEqual(
-                len(self.get_output(w.Outputs.data).domain.attributes), 2)
-
-        self.assertEqual(len(self.get_output(w.Outputs.data).domain.metas), 1)
-        self.assertIsNotNone(self.get_output(w.Outputs.data).domain.class_var)
-
-        # check again none
-        self.send_signal(w.Inputs.data, None)
-        self.assertIsNone(self.get_output(w.Outputs.data))
-
-    def test_send_report(self):
-        """
-        Just test everything not crashes
-        """
-        w = self.widget
-
-        self.send_signal(w.Inputs.data, self.iris)
-        self.process_events(lambda: getattr(w, w.graph_name).svg())
-        w.send_report()
+        for w.degree in range(1, 6):
+            w._on_degree_changed()
+            self.assertEqual(len(self.get_output(w.Outputs.coefficients)),
+                             (w.degree + 1) * (w.degree + 2) // 2,
+                             f"at degree={w.degree}")
 
     def test_bad_learner(self):
-        """
-        Some learners on input might raise error.
-        GH-38
-        """
         w = self.widget
 
         self.assertFalse(w.Error.fitting_failed.is_shown())
@@ -551,51 +262,7 @@ def test_bad_learner(self):
         self.send_signal(w.Inputs.learner, learner)
         self.assertFalse(w.Error.fitting_failed.is_shown())
 
-    def test_raise_no_classifier_error(self):
-        """
-        Regression learner must raise error
-        """
-        w = self.widget
-
-        # linear regression learner is regression - should raise
-        learner = LinearRegressionLearner()
-        self.send_signal(w.Inputs.learner, learner)
-        self.assertTrue(w.Error.no_classifier.is_shown())
-
-        # make it empty to test if error disappear
-        self.send_signal(w.Inputs.learner, None)
-        self.assertFalse(w.Error.no_classifier.is_shown())
-
-        # test with some other learners
-        learner = LogisticRegressionLearner()
-        self.send_signal(w.Inputs.learner, learner)
-        self.assertFalse(w.Error.no_classifier.is_shown())
-
-        learner = TreeLearner()
-        self.send_signal(w.Inputs.learner, learner)
-        self.assertFalse(w.Error.no_classifier.is_shown())
-
-        learner = RandomForestLearner()
-        self.send_signal(w.Inputs.learner, learner)
-        self.assertFalse(w.Error.no_classifier.is_shown())
-
-        learner = SVMLearner()
-        self.send_signal(w.Inputs.learner, learner)
-        self.assertFalse(w.Error.no_classifier.is_shown())
-
-    def test_no_data_contour(self):
-        """
-        Do not crash when there is no data and one want to draw contours.
-        GH-50
-        """
-        self.widget.contours_enabled_checkbox.click()
-
     def test_sparse(self):
-        """
-        Do not crash on sparse data. Convert used
-        sparse columns to numpy array.
-        GH-52
-        """
         w = self.widget
 
         def send_sparse_data(data):
@@ -616,28 +283,61 @@ def send_sparse_data(data):
 
     def test_non_in_data(self):
         w = self.widget
-        data = Table.from_file("iris")[::15]
-        data.Y[:3] = np.nan
+        self.iris.Y[:10] = np.nan
+        self.iris.X[-4:, 0] = np.nan
 
-        self.send_signal(w.Inputs.data, data)
+        self.send_signal(w.Inputs.data, self.iris)
+        np.testing.assert_equal(w.selected_data.X, self.iris.X[:-4, :2])
 
-        num_continuous_attributes = sum(
-            True for var in self.iris.domain.attributes
-            if isinstance(var, ContinuousVariable))
-        self.assertEqual(w.cbx.count(), num_continuous_attributes)
-        self.assertEqual(w.cby.count(), num_continuous_attributes)
-        self.assertEqual(
-            w.target_class_combobox.count(),
-            len(self.iris.domain.class_var.values))
-        self.assertEqual(w.cbx.currentText(), self.iris.domain[0].name)
-        self.assertEqual(w.cby.currentText(), self.iris.domain[1].name)
-        self.assertEqual(
-            w.target_class_combobox.currentText(),
-            self.iris.domain.class_var.values[0])
-
-        self.assertEqual(w.attr_x, self.iris.domain[0].name)
-        self.assertEqual(w.attr_y, self.iris.domain[1].name)
-        self.assertEqual(w.target_class, self.iris.domain.class_var.values[0])
+    def test_same_variable(self):
+        w = self.widget
+        self.send_signal(w.Inputs.data, self.iris)
+
+        assert w.selected_data is not None
+        assert w.attr_x is w.data.domain[0]
+
+        w.attr_y = w.data.domain[0]
+        w._on_attr_changed()
+        self.assertIsNone(w.selected_data)
+        self.assertTrue(w.Error.same_variable.is_shown())
+
+        w.attr_y = w.data.domain[1]
+        w._on_attr_changed()
+        self.assertIsNotNone(w.selected_data)
+        self.assertFalse(w.Error.same_variable.is_shown())
+
+        self.send_signal(w.Inputs.data, None)
+        self.assertFalse(w.Error.same_variable.is_shown())
+
+        self.send_signal(w.Inputs.data, self.iris)
+        w.attr_y = w.data.domain[0]
+        w._on_attr_changed()
+        self.assertTrue(w.Error.same_variable.is_shown())
+
+        self.send_signal(w.Inputs.data, Table("housing"))
+        self.assertFalse(w.Error.same_variable.is_shown())
+
+
+class TestOWPolynomialClassification(WidgetTest):
+    # Tests that compute the probability grid and contours, so the code is
+    # run at least a few times
+    def setUp(self):
+        # type: OWPolynomialClassification
+        self.widget = self.create_widget(OWPolynomialClassification)
+        self.iris = Table.from_file("iris")
+
+    def test_blur_grid(self):
+        w = self.widget
+
+        self.send_signal(w.Inputs.data, self.iris)
+        # here we can check that 0.5 remains same
+        assert_array_equal(w.probabilities_grid == 0.5,
+                           w.blur_grid(w.probabilities_grid) == 0.5)
+
+    def test_send_report(self):
+        w = self.widget
+        self.send_signal(w.Inputs.data, self.iris)
+        w.send_report()
 
 
 if __name__ == "__main__":