Repository: biolab/orange3

Issue:
Visualizing categorical data in scatter plot with Orange (version > 3.18)
**Describe the bug**
It is not possible to visualize categorical data in Orange, version >3.18. Orange official doc states otherwise, showcasing support for both, continuous as well as discrete-valued attributes. 

**To Reproduce**
![1](https://user-images.githubusercontent.com/60474572/73464649-ed5a8580-4387-11ea-86c9-1d63db11c27a.png)
![2](https://user-images.githubusercontent.com/60474572/73464675-f4819380-4387-11ea-8808-e6581623f9f2.png)
![3](https://user-images.githubusercontent.com/60474572/73464689-f9464780-4387-11ea-9b08-2d941b2e5bab.png)
![4](https://user-images.githubusercontent.com/60474572/73464704-fea39200-4387-11ea-863f-6ecc8ecbbdef.png)

**Orange version:** 
3.20 - 3.24

**Operating system:** 
Win 10 64-bit, Win 8.1 64-bit, 






Discussion:
Thank you for the attention. My idea was not to correct the documentation, but to fix the possibility to visualize Categorical data in Scatter plot in versions >3.18. If there is any way to do this, please share it!
This was done intentionally. Scatter Plot is intended for numeric variables. For discrete, use Mosaic Display, Box Plot or some other visualization.
Thank you!
Hi all, 

Even though it was done intentionally, would it be possible to reconsider this decision?

We found it very useful in industry to quickly visualize important variables through different projects. None of above proposed visualization does this equivalently. Mosaic Display is to complex, while Box Plot is too simplified (e.q. pure bimodal distribution, quickly label/select extreme points). We are currently solving this with Continuize widget, which is not preferred solution.

Regards, Tine
So if I understand this correctly, you wish to use SP to visualize *all* variables, continuous and discrete? IMO, not one visualization is appropriate for all data types. Each type requires its own visualization. You are not doing yourself a favor using a one-fits-all solution (but again, this is just my opinion).

I would use Feature Statistics though, for a quick overview.
I will try to explain :) I do agree, that every type has preferred visualization. On the other hand, every question that arises requires a tool that can successfully answer it. My comment more in direction, that there was a very useful tool, which was omitted and the functionality was lost (I believe there is not widget that can overcome this deficiency - but again, this is just my opinion :). 

Firstly, Feature statistics widget is regularly used - really nice widget to go through variables! But it does not cover functionality we used in SC. We were constantly using SC for visualization and answering questions that arise
- quick overview of 1 variable across projects (where we clearly see distribution, box plots are too simplified at one side and rather complex to explain to everyone)
- desire to see not compressed information or non normal distributions across categorical variable (e.q. small number of data samples)
- quickly label possible outliers, detect one outlying group, etc ...

Living in a world without perfect datasets a quick full overview of data is necessary. It may not be state-of-the-art, but it is found really useful to answer as many questions as possible during our meetings.
I also had similar wishes from some users of Orange Spectroscopy.

I understand the pedagogical motivation for removing discrete variables, but maybe it was used it so much because it was indeed a quick and dirty path to some insight.

And then, there is one use case that none of the current widgets cover and the previous scatterplot did: if you have one discrete and one continuous variable, the scatterplot can give you an idea of the distribution of the continuous variables given the discrete one. 

Maybe the current scatterplot is indeed too limiting. Seeing some users continuizing their data to get the feature back is quite a signal. I'd vote for supporting discrete vars again.
Ok, I see your point. We will raise the issue at Friday's meeting.
"if you have one discrete and one continuous variable, the scatterplot can give you an idea of the distribution of the continuous variables given the discrete one" - what can be more pedagogical than this?
Also, why parallel coordinates plot was removed?
> "if you have one discrete and one continuous variable, the scatterplot can give you an idea of the distribution of the continuous variables given the discrete one" - what can be more pedagogical than this?

*I have seen things you people wouldn't believe.* :) Scatterplot was a mädchen für alles for just about everything. In many cases, there exists a much better, cleaner, more informative, less misleading visualization, but people didn't discover it because they were stuck in the local optimum (=scatter plot).

What @markotoplak wrote was one of the few (if not the only) "legitimate" uses of scatter plot with discrete variables. I'd prefer to see how to expose (or add) better visualizations, and how to make them more suitable for the cases in which experienced users would prefer to use the previous scatter plot. For instance, the @markotoplak's case could be handled by putting dots inside of box plot. Maybe. Or maybe there is a completely different visualization for this. Same for other uses of discrete variables in scatter plots.
@rosenm1973 Parallel coordinates was removed because it was unmaintained and it was crashing all the time. Feel free to bring it back to life. It was moved to Prototypes and the original code can be found somewhere in the history of the repo.
I will be honored to discuss some of my comments in Friday’s meeting :)

Here is my view on latest comments
•	Firstly, would not covering “local optimums” mean Orange direction is going into special tools? This could be the opposite direction from democratization of data mining e.q. data mining for all.
•	Secondly, a statement that scatter plot is more misleading than descriptive statistics is far from commonly accepted according to my knowledge. There is long debate in history which direction is better. Honestly, I doubt the consequences of “I have seen things you people wouldn't believe. :)” from descriptive statistics were costlier than consequences of not visualizing the data.
•	Thirdly, I got the vibe that the Orange is meant to be primarily pedagogical?

Overall, Orange is a fantastic platform. It would be a shame to see people not joining the community just because they did not find the right tool to resolve their must-do challenges (some did not choose their datasets, datasets are usually far from ideal and the time is limited).

Again, many thanks for discussing such an important topic :)

> I will be honored to discuss some of my comments in Friday’s meeting :)

Not much of an honour, but I'm happy you feel this way. :)

We'll probably revive discrete variables in scatter plots, but it may not happen soon. @VesnaT is busy and she said she can look into it after a month or so. A lot of code was removed and reverting this is not trivial because new code may expect to find numeric variables only, so this needs to be done carefully (which disqualifies the other candidate for the job -- myself.)

Our reservations about misuse of the widget will be adressed by the widget showing a (discrete) warning in cases where other visualizations may be more appropriate.
+1 for reintroducing this feature, and optionally adding a short section in the documentation of scatterplot to other visualizations that might be more usefull for non numeric data
I do agree that there is value in this feature. 

Resolving patch:
diff --git a/Orange/widgets/visualize/owscatterplot.py b/Orange/widgets/visualize/owscatterplot.py
--- a/Orange/widgets/visualize/owscatterplot.py
+++ b/Orange/widgets/visualize/owscatterplot.py
@@ -2,6 +2,7 @@
 from xml.sax.saxutils import escape
 
 import numpy as np
+from AnyQt.QtWidgets import QGroupBox, QPushButton
 from scipy.stats import linregress
 from sklearn.neighbors import NearestNeighbors
 from sklearn.metrics import r2_score
@@ -11,15 +12,17 @@
 
 import pyqtgraph as pg
 
-from Orange.data import Table, Domain, DiscreteVariable, Variable, \
-    ContinuousVariable
+from orangewidget.utils.combobox import ComboBoxSearch
+
+from Orange.data import Table, Domain, DiscreteVariable, Variable
 from Orange.data.sql.table import SqlTable, AUTO_DL_LIMIT
 from Orange.preprocess.score import ReliefF, RReliefF
 
-from Orange.widgets import gui
+from Orange.widgets import gui, report
 from Orange.widgets.io import MatplotlibFormat, MatplotlibPDFFormat
 from Orange.widgets.settings import (
     Setting, ContextSetting, SettingProvider, IncompatibleContext)
+from Orange.widgets.utils import get_variable_values_sorted
 from Orange.widgets.utils.itemmodels import DomainModel
 from Orange.widgets.utils.widgetpreview import WidgetPreview
 from Orange.widgets.visualize.owscatterplotgraph import OWScatterPlotBase, \
@@ -85,7 +88,7 @@ def score_heuristic(self):
         assert self.attr_color is not None
         master_domain = self.master.data.domain
         vars = [v for v in chain(master_domain.variables, master_domain.metas)
-                if v is not self.attr_color and v.is_continuous]
+                if v is not self.attr_color and v.is_primitive()]
         domain = Domain(attributes=vars, class_vars=self.attr_color)
         data = self.master.data.transform(domain)
         relief = ReliefF if isinstance(domain.class_var, DiscreteVariable) \
@@ -117,6 +120,7 @@ def axis_items(self):
 class OWScatterPlotGraph(OWScatterPlotBase):
     show_reg_line = Setting(False)
     orthonormal_regression = Setting(False)
+    jitter_continuous = Setting(False)
 
     def __init__(self, scatter_widget, parent):
         super().__init__(scatter_widget, parent)
@@ -137,12 +141,34 @@ def update_colors(self):
         super().update_colors()
         self.update_regression_line()
 
+    def jitter_coordinates(self, x, y):
+        def get_span(attr):
+            if attr.is_discrete:
+                # Assuming the maximal jitter size is 10, a span of 4 will
+                # jitter by 4 * 10 / 100 = 0.4, so there will be no overlap
+                return 4
+            elif self.jitter_continuous:
+                return None  # Let _jitter_data determine the span
+            else:
+                return 0  # No jittering
+        span_x = get_span(self.master.attr_x)
+        span_y = get_span(self.master.attr_y)
+        if self.jitter_size == 0 or (span_x == 0 and span_y == 0):
+            return x, y
+        return self._jitter_data(x, y, span_x, span_y)
+
     def update_axes(self):
-        for axis, title in self.master.get_axes().items():
-            use_time = title is not None and title.is_time
+        for axis, var in self.master.get_axes().items():
+            axis_item = self.plot_widget.plotItem.getAxis(axis)
+            if var and var.is_discrete:
+                ticks = [list(enumerate(get_variable_values_sorted(var)))]
+                axis_item.setTicks(ticks)
+            else:
+                axis_item.setTicks(None)
+            use_time = var and var.is_time
             self.plot_widget.plotItem.getAxis(axis).use_time(use_time)
-            self.plot_widget.setLabel(axis=axis, text=title or "")
-            if title is None:
+            self.plot_widget.setLabel(axis=axis, text=var or "")
+            if not var:
                 self.plot_widget.hideAxis(axis)
 
     @staticmethod
@@ -203,7 +229,8 @@ def update_regression_line(self):
         for line in self.reg_line_items:
             self.plot_widget.removeItem(line)
         self.reg_line_items.clear()
-        if not self.show_reg_line:
+        if not (self.show_reg_line
+                and self.master.can_draw_regresssion_line()):
             return
         x, y = self.master.get_coordinates_data()
         if x is None:
@@ -238,7 +265,7 @@ class Inputs(OWDataProjectionWidget.Inputs):
     class Outputs(OWDataProjectionWidget.Outputs):
         features = Output("Features", AttributeList, dynamic=False)
 
-    settings_version = 4
+    settings_version = 5
     auto_sample = Setting(True)
     attr_x = ContextSetting(None)
     attr_y = ContextSetting(None)
@@ -254,7 +281,6 @@ class Warning(OWDataProjectionWidget.Warning):
         missing_coords = Msg(
             "Plot cannot be displayed because '{}' or '{}' "
             "is missing for all data points.")
-        no_continuous_vars = Msg("Data has no numeric variables.")
 
     class Information(OWDataProjectionWidget.Information):
         sampled_sql = Msg("Large SQL table; showing a sample.")
@@ -262,6 +288,14 @@ class Information(OWDataProjectionWidget.Information):
             "Points with missing '{}' or '{}' are not displayed")
 
     def __init__(self):
+        self.attr_box: QGroupBox = None
+        self.xy_model: DomainModel = None
+        self.cb_attr_x: ComboBoxSearch = None
+        self.cb_attr_y: ComboBoxSearch = None
+        self.vizrank: ScatterPlotVizRank = None
+        self.vizrank_button: QPushButton = None
+        self.sampling: QGroupBox = None
+
         self.sql_data = None  # Orange.data.sql.table.SqlTable
         self.attribute_selection_list = None  # list of Orange.data.Variable
         self.__timer = QTimer(self, interval=1200)
@@ -277,6 +311,7 @@ def _add_controls(self):
         self._add_controls_axis()
         self._add_controls_sampling()
         super()._add_controls()
+        self.gui.add_widget(self.gui.JitterNumericValues, self._effects_box)
         self.gui.add_widgets(
             [self.gui.ShowGridLines,
              self.gui.ToolTipShowsAll,
@@ -300,7 +335,7 @@ def _add_controls_axis(self):
         self.attr_box = gui.vBox(self.controlArea, 'Axes',
                                  spacing=2 if gui.is_macstyle() else 8)
         dmod = DomainModel
-        self.xy_model = DomainModel(dmod.MIXED, valid_types=ContinuousVariable)
+        self.xy_model = DomainModel(dmod.MIXED, valid_types=dmod.PRIMITIVE)
         self.cb_attr_x = gui.comboBox(
             self.attr_box, self, "attr_x", label="Axis x:",
             callback=self.set_attr_from_combo,
@@ -393,11 +428,6 @@ def check_data(self):
                 if self.auto_sample:
                     self.__timer.start()
 
-        if self.data is not None:
-            if not self.data.domain.has_continuous_attributes(True, True):
-                self.Warning.no_continuous_vars()
-                self.data = None
-
         if self.data is not None and (len(self.data) == 0 or
                                       len(self.data.domain.variables) == 0):
             self.data = None
@@ -433,6 +463,12 @@ def _point_tooltip(self, point_id, skip_attrs=()):
                 text = "<b>{}</b><br/><br/>{}".format(text, others)
         return text
 
+    def can_draw_regresssion_line(self):
+        return self.data is not None and \
+               self.data.domain is not None and \
+               self.attr_x.is_continuous and \
+               self.attr_y.is_continuous
+
     def add_data(self, time=0.4):
         if self.data and len(self.data) > 2000:
             self.__timer.stop()
@@ -484,6 +520,7 @@ def handleNewSignals(self):
         if self._domain_invalidated:
             self.graph.update_axes()
             self._domain_invalidated = False
+        self.cb_reg_line.setEnabled(self.can_draw_regresssion_line())
 
     @Inputs.features
     def set_shown_attributes(self, attributes):
@@ -505,6 +542,7 @@ def set_attr_from_combo(self):
         self.xy_changed_manually.emit(self.attr_x, self.attr_y)
 
     def attr_changed(self):
+        self.cb_reg_line.setEnabled(self.can_draw_regresssion_line())
         self.setup_plot()
         self.commit()
 
@@ -528,6 +566,17 @@ def get_widget_name_extension(self):
             return "{} vs {}".format(self.attr_x.name, self.attr_y.name)
         return None
 
+    def _get_send_report_caption(self):
+        return report.render_items_vert((
+            ("Color", self._get_caption_var_name(self.attr_color)),
+            ("Label", self._get_caption_var_name(self.attr_label)),
+            ("Shape", self._get_caption_var_name(self.attr_shape)),
+            ("Size", self._get_caption_var_name(self.attr_size)),
+            ("Jittering", (self.attr_x.is_discrete or
+                           self.attr_y.is_discrete or
+                           self.graph.jitter_continuous) and
+             self.graph.jitter_size)))
+
     @classmethod
     def migrate_settings(cls, settings, version):
         if version < 2 and "selection" in settings and settings["selection"]:
@@ -537,6 +586,10 @@ def migrate_settings(cls, settings, version):
                 settings["auto_commit"] = settings["auto_send_selection"]
             if "selection_group" in settings:
                 settings["selection"] = settings["selection_group"]
+        if version < 5:
+            if "graph" in settings and \
+                    "jitter_continuous" not in settings["graph"]:
+                settings["graph"]["jitter_continuous"] = True
 
     @classmethod
     def migrate_context(cls, context, version):


Test patch:
diff --git a/Orange/widgets/visualize/tests/test_owscatterplot.py b/Orange/widgets/visualize/tests/test_owscatterplot.py
--- a/Orange/widgets/visualize/tests/test_owscatterplot.py
+++ b/Orange/widgets/visualize/tests/test_owscatterplot.py
@@ -79,17 +79,6 @@ def test_score_heuristics(self):
         self.assertEqual([x.name for x in vizrank.score_heuristic()],
                          list("abcd"))
 
-    def test_score_heuristics_no_disc(self):
-        domain = Domain([ContinuousVariable(c) for c in "abc"] +
-                        [DiscreteVariable("d", values="abcdefghij")],
-                        DiscreteVariable("e", values="ab"))
-        a = np.arange(10).reshape((10, 1))
-        data = Table(domain, np.hstack([a, a, a, a]), a >= 5)
-        self.send_signal(self.widget.Inputs.data, data)
-        vizrank = ScatterPlotVizRank(self.widget)
-        self.assertEqual([x.name for x in vizrank.score_heuristic()],
-                         list("abc"))
-
     def test_optional_combos(self):
         domain = self.data.domain
         d1 = Domain(domain.attributes[:2], domain.class_var,
@@ -149,6 +138,18 @@ def test_data_column_infs(self):
         attr_x = self.widget.controls.attr_x
         simulate.combobox_activate_item(attr_x, "b")
 
+    def test_regression_line(self):
+        """It is possible to draw the line only for pair of continuous attrs"""
+        self.send_signal(self.widget.Inputs.data, self.data)
+        self.assertTrue(self.widget.cb_reg_line.isEnabled())
+        self.assertIsNone(self.widget.graph.reg_line_item)
+        self.widget.cb_reg_line.setChecked(True)
+        self.assertIsNotNone(self.widget.graph.reg_line_item)
+        self.widget.cb_attr_y.activated.emit(4)
+        self.widget.cb_attr_y.setCurrentIndex(4)
+        self.assertFalse(self.widget.cb_reg_line.isEnabled())
+        self.assertIsNone(self.widget.graph.reg_line_item)
+
     def test_points_combo_boxes(self):
         """Check Point box combo models and values"""
         self.send_signal(self.widget.Inputs.data, self.data)
@@ -269,8 +270,8 @@ def test_points_selection(self):
         self.assertEqual(len(selected_data), 50)
 
         # Changing the dataset should clear selection
-        heart = Table("heart_disease")
-        self.send_signal(self.widget.Inputs.data, heart)
+        titanic = Table("titanic")
+        self.send_signal(self.widget.Inputs.data, titanic)
         selected_data = self.get_output(self.widget.Outputs.selected_data)
         self.assertIsNone(selected_data)
 
@@ -406,7 +407,7 @@ def assert_vizrank_enabled(data, is_enabled):
 
     def test_vizrank_nonprimitives(self):
         """VizRank does not try to include non primitive attributes"""
-        data = Table("brown-selected")
+        data = Table("zoo")
         self.send_signal(self.widget.Inputs.data, data)
         with patch("Orange.widgets.visualize.owscatterplot.ReliefF",
                    new=lambda *_1, **_2: lambda data: np.arange(len(data))):
@@ -444,7 +445,7 @@ def test_vizrank_enabled_constant_data(self):
         self.widget.vizrank.button.click()
 
     def test_vizrank_enabled_two_features(self):
-        self.send_signal(self.widget.Inputs.data, self.data[:, 2:])
+        self.send_signal(self.widget.Inputs.data, self.data[:, :2])
         self.assertFalse(self.widget.vizrank_button.isEnabled())
         self.assertEqual(self.widget.vizrank_button.toolTip(),
                          "Not enough features for ranking")
@@ -485,56 +486,56 @@ def test_auto_send_selection(self):
         self.assertIsInstance(output, Table)
 
     def test_color_is_optional(self):
-        heart = Table("heart_disease")
-        age, rest_sbp, max_hr, cholesterol, gender, narrowing = \
-            [heart.domain[x]
-             for x in ["age", "rest SBP", "max HR", "cholesterol", "gender",
-                       "diameter narrowing"]]
+        zoo = Table("zoo")
+        backbone, breathes, airborne, type = \
+            [zoo.domain[x] for x in ["backbone", "breathes", "airborne", "type"]]
+        default_x, default_y, default_color = \
+            zoo.domain[0], zoo.domain[1], zoo.domain.class_var
         attr_x = self.widget.controls.attr_x
         attr_y = self.widget.controls.attr_y
         attr_color = self.widget.controls.attr_color
 
         # Send dataset, ensure defaults are what we expect them to be
-        self.send_signal(self.widget.Inputs.data, heart)
-        self.assertEqual(attr_x.currentText(), age.name)
-        self.assertEqual(attr_y.currentText(), rest_sbp.name)
-        self.assertEqual(attr_color.currentText(), narrowing.name)
+        self.send_signal(self.widget.Inputs.data, zoo)
+        self.assertEqual(attr_x.currentText(), default_x.name)
+        self.assertEqual(attr_y.currentText(), default_y.name)
+        self.assertEqual(attr_color.currentText(), default_color.name)
         # Select different values
-        simulate.combobox_activate_item(attr_x, max_hr.name)
-        simulate.combobox_activate_item(attr_y, cholesterol.name)
-        simulate.combobox_activate_item(attr_color, gender.name)
+        simulate.combobox_activate_item(attr_x, backbone.name)
+        simulate.combobox_activate_item(attr_y, breathes.name)
+        simulate.combobox_activate_item(attr_color, airborne.name)
 
         # Send compatible dataset, values should not change
-        heart2 = heart[:, (cholesterol, gender, max_hr, narrowing)]
-        self.send_signal(self.widget.Inputs.data, heart2)
-        simulate.combobox_activate_item(attr_x, max_hr.name)
-        simulate.combobox_activate_item(attr_y, cholesterol.name)
-        simulate.combobox_activate_item(attr_color, gender.name)
+        zoo2 = zoo[:, (backbone, breathes, airborne, type)]
+        self.send_signal(self.widget.Inputs.data, zoo2)
+        self.assertEqual(attr_x.currentText(), backbone.name)
+        self.assertEqual(attr_y.currentText(), breathes.name)
+        self.assertEqual(attr_color.currentText(), airborne.name)
 
         # Send dataset without color variable
         # x and y should remain, color reset to default
-        heart3 = heart[:, (age, max_hr, cholesterol, narrowing)]
-        self.send_signal(self.widget.Inputs.data, heart3)
-        simulate.combobox_activate_item(attr_x, max_hr.name)
-        simulate.combobox_activate_item(attr_y, cholesterol.name)
-        self.assertEqual(attr_color.currentText(), narrowing.name)
+        zoo3 = zoo[:, (backbone, breathes, type)]
+        self.send_signal(self.widget.Inputs.data, zoo3)
+        self.assertEqual(attr_x.currentText(), backbone.name)
+        self.assertEqual(attr_y.currentText(), breathes.name)
+        self.assertEqual(attr_color.currentText(), default_color.name)
 
         # Send dataset without x
-        # y and color should be the same as with heart
-        heart4 = heart[:, (age, rest_sbp, cholesterol, narrowing)]
-        self.send_signal(self.widget.Inputs.data, heart4)
-        self.assertEqual(attr_x.currentText(), age.name)
-        self.assertEqual(attr_y.currentText(), rest_sbp.name)
-        self.assertEqual(attr_color.currentText(), narrowing.name)
-
-        # Send dataset compatible with heart2 and heart3
-        # Color should reset to one in heart3, as it was used more
+        # y and color should be the same as with zoo
+        zoo4 = zoo[:, (default_x, default_y, breathes, airborne, type)]
+        self.send_signal(self.widget.Inputs.data, zoo4)
+        self.assertEqual(attr_x.currentText(), default_x.name)
+        self.assertEqual(attr_y.currentText(), default_y.name)
+        self.assertEqual(attr_color.currentText(), default_color.name)
+
+        # Send dataset compatible with zoo2 and zoo3
+        # Color should reset to one in zoo3, as it was used more
         # recently
-        heart5 = heart[:, (age, max_hr, cholesterol, gender, narrowing)]
-        self.send_signal(self.widget.Inputs.data, heart5)
-        simulate.combobox_activate_item(attr_x, max_hr.name)
-        simulate.combobox_activate_item(attr_y, cholesterol.name)
-        self.assertEqual(attr_color.currentText(), narrowing.name)
+        zoo5 = zoo[:, (default_x, backbone, breathes, airborne, type)]
+        self.send_signal(self.widget.Inputs.data, zoo5)
+        self.assertEqual(attr_x.currentText(), backbone.name)
+        self.assertEqual(attr_y.currentText(), breathes.name)
+        self.assertEqual(attr_color.currentText(), type.name)
 
     def test_handle_metas(self):
         """
@@ -582,6 +583,11 @@ def test_opacity_warning(self):
         self.send_signal(w.Inputs.data_subset, None)
         self.assertFalse(w.Warning.transparent_subset.is_shown())
 
+    def test_jittering(self):
+        self.send_signal(self.widget.Inputs.data, self.data)
+        self.widget.graph.controls.jitter_continuous.setChecked(True)
+        self.widget.graph.controls.jitter_size.setValue(1)
+
     def test_metas_zero_column(self):
         """
         Prevent crash when metas column is zero.
@@ -603,10 +609,10 @@ def test_tooltip(self):
         self.send_signal(self.widget.Inputs.data, data)
         widget = self.widget
         graph = widget.graph
-
-        widget.attr_x = data.domain["age"]
-        widget.attr_y = data.domain["max HR"]
         scatterplot_item = graph.scatterplot_item
+
+        widget.controls.attr_x = data.domain["chest pain"]
+        widget.controls.attr_y = data.domain["cholesterol"]
         all_points = scatterplot_item.points()
 
         event = MagicMock()
@@ -621,8 +627,8 @@ def test_tooltip(self):
                 self.assertTrue(graph.help_event(event))
                 (_, text), _ = show_text.call_args
                 self.assertIn("age = {}".format(data[42, "age"]), text)
-                self.assertIn("max HR = {}".format(data[42, "max HR"]), text)
-                self.assertNotIn("gender = {}".format(data[42, "gender"]), text)
+                self.assertIn("gender = {}".format(data[42, "gender"]), text)
+                self.assertNotIn("max HR = {}".format(data[42, "max HR"]), text)
                 self.assertNotIn("others", text)
 
                 # Show all attributes
@@ -667,7 +673,7 @@ def prepare_data():
 
         def assert_equal(data, max):
             self.send_signal(self.widget.Inputs.data, data)
-            pen_data, _ = self.widget.graph.get_colors()
+            pen_data, brush_data = self.widget.graph.get_colors()
             self.assertEqual(max, len(np.unique([id(p) for p in pen_data])), )
 
         assert_equal(prepare_data(), MAX_COLORS)
@@ -676,18 +682,6 @@ def assert_equal(data, max):
         data.Y[42] = np.nan
         assert_equal(data, MAX_COLORS + 1)
 
-    def test_change_data(self):
-        self.send_signal(self.widget.Inputs.data, self.data)
-        self.send_signal(self.widget.Inputs.data, Table("titanic"))
-        self.assertTrue(self.widget.Warning.no_continuous_vars.is_shown())
-        self.assertIsNone(self.widget.data)
-        self.assertIsNone(self.get_output(self.widget.Outputs.annotated_data))
-        self.send_signal(self.widget.Inputs.data, self.data)
-        self.assertFalse(self.widget.Warning.no_continuous_vars.is_shown())
-        self.assertIs(self.widget.data, self.data)
-        self.assertIsNotNone(
-            self.get_output(self.widget.Outputs.annotated_data))
-
     def test_invalidated_same_features(self):
         self.widget.setup_plot = Mock()
         # send data and set default features
@@ -841,6 +835,7 @@ def test_regression_line_coeffs(self):
                        [0, 1], [1, 3], [2, 5]], dtype=np.float)
         colors = np.array([0, 0, 0, 0, 1, 1, 1], dtype=np.float)
         widget.get_coordinates_data = lambda: xy.T
+        widget.can_draw_regresssion_line = lambda: True
         widget.get_color_data = lambda: colors
         widget.is_continuous_color = lambda: False
         graph.palette = DefaultRGBColors
@@ -990,6 +985,7 @@ def test_update_regression_line_calls_add_line(self):
                          [0, 1], [1, 3], [2, 5]], dtype=np.float).T
         colors = np.array([0, 0, 0, 0, 1, 1, 1], dtype=np.float)
         widget.get_coordinates_data = lambda: (x, y)
+        widget.can_draw_regresssion_line = lambda: True
         widget.get_color_data = lambda: colors
         widget.is_continuous_color = lambda: False
         graph.palette = DefaultRGBColors