{
    "file_diffs": [
        {
            "old_file_content": "from itertools import chain\nfrom xml.sax.saxutils import escape\n\nimport numpy as np\nfrom scipy.stats import linregress\nfrom sklearn.neighbors import NearestNeighbors\nfrom sklearn.metrics import r2_score\n\nfrom AnyQt.QtCore import Qt, QTimer, QPointF, Signal\nfrom AnyQt.QtGui import QColor, QFont\nfrom AnyQt.QtWidgets import QGroupBox, QPushButton\n\nimport pyqtgraph as pg\n\nfrom orangewidget.utils.combobox import ComboBoxSearch\n\nfrom Orange.data import Table, Domain, DiscreteVariable, Variable\nfrom Orange.data.sql.table import SqlTable, AUTO_DL_LIMIT\nfrom Orange.preprocess.score import ReliefF, RReliefF\n\nfrom Orange.widgets import gui, report\nfrom Orange.widgets.io import MatplotlibFormat, MatplotlibPDFFormat\nfrom Orange.widgets.settings import (\n    Setting, ContextSetting, SettingProvider, IncompatibleContext)\nfrom Orange.widgets.utils import get_variable_values_sorted\nfrom Orange.widgets.utils.itemmodels import DomainModel\nfrom Orange.widgets.utils.widgetpreview import WidgetPreview\nfrom Orange.widgets.visualize.owscatterplotgraph import OWScatterPlotBase, \\\n    ScatterBaseParameterSetter\nfrom Orange.widgets.visualize.utils import VizRankDialogAttrPair\nfrom Orange.widgets.visualize.utils.customizableplot import Updater\nfrom Orange.widgets.visualize.utils.widget import OWDataProjectionWidget\nfrom Orange.widgets.widget import AttributeList, Msg, Input, Output\n\n\nclass ScatterPlotVizRank(VizRankDialogAttrPair):\n    captionTitle = \"Score Plots\"\n    minK = 10\n    attr_color = None\n\n    def __init__(self, master):\n        super().__init__(master)\n        self.attr_color = self.master.attr_color\n\n    def initialize(self):\n        self.attr_color = self.master.attr_color\n        super().initialize()\n\n    def check_preconditions(self):\n        self.Information.add_message(\n            \"color_required\", \"Color variable is not selected\")\n        self.Information.color_required.clear()\n        if not super().check_preconditions():\n            return False\n        if not self.attr_color:\n            self.Information.color_required()\n            return False\n        return True\n\n    def iterate_states(self, initial_state):\n        # If we put initialization of `self.attrs` to `initialize`,\n        # `score_heuristic` would be run on every call to `set_data`.\n        if initial_state is None:  # on the first call, compute order\n            self.attrs = self.score_heuristic()\n        yield from super().iterate_states(initial_state)\n\n    def compute_score(self, state):\n        # pylint: disable=invalid-unary-operand-type\n        attrs = [self.attrs[i] for i in state]\n        data = self.master.data\n        data = data.transform(Domain(attrs, self.attr_color))\n        data = data[~np.isnan(data.X).any(axis=1) & ~np.isnan(data.Y).T]\n        if len(data) < self.minK:\n            return None\n        n_neighbors = min(self.minK, len(data) - 1)\n        knn = NearestNeighbors(n_neighbors=n_neighbors).fit(data.X)\n        ind = knn.kneighbors(return_distance=False)\n        if data.domain.has_discrete_class:\n            return -np.sum(data.Y[ind] == data.Y.reshape(-1, 1)) / \\\n                   n_neighbors / len(data.Y)\n        else:\n            return -r2_score(data.Y, np.mean(data.Y[ind], axis=1)) * \\\n                   (len(data.Y) / len(self.master.data))\n\n    def bar_length(self, score):\n        return max(0, -score)\n\n    def score_heuristic(self):\n        assert self.attr_color is not None\n        master_domain = self.master.data.domain\n        vars = [v for v in chain(master_domain.variables, master_domain.metas)\n                if v is not self.attr_color and v.is_primitive()]\n        domain = Domain(attributes=vars, class_vars=self.attr_color)\n        data = self.master.data.transform(domain)\n        relief = ReliefF if isinstance(domain.class_var, DiscreteVariable) \\\n            else RReliefF\n        weights = relief(n_iterations=100, k_nearest=self.minK)(data)\n        attrs = sorted(zip(weights, domain.attributes),\n                       key=lambda x: (-x[0], x[1].name))\n        return [a for _, a in attrs]\n\n\nclass ParameterSetter(ScatterBaseParameterSetter):\n    DEFAULT_LINE_WIDTH = 3\n    DEFAULT_LINE_ALPHA = 255\n\n    def __init__(self, master):\n        super().__init__(master)\n        self.reg_line_label_font = QFont()\n        self.reg_line_settings = {\n            Updater.WIDTH_LABEL: self.DEFAULT_LINE_WIDTH,\n            Updater.ALPHA_LABEL: self.DEFAULT_LINE_ALPHA,\n            Updater.STYLE_LABEL: Updater.DEFAULT_LINE_STYLE,\n        }\n\n    def update_setters(self):\n        super().update_setters()\n        self.initial_settings[self.LABELS_BOX].update({\n            self.AXIS_TITLE_LABEL: self.FONT_SETTING,\n            self.AXIS_TICKS_LABEL: self.FONT_SETTING,\n            self.LINE_LAB_LABEL: self.FONT_SETTING\n        })\n        self.initial_settings[self.PLOT_BOX] = {}\n        self.initial_settings[self.PLOT_BOX][self.LINE_LABEL] = {\n            Updater.WIDTH_LABEL: (range(1, 10), self.DEFAULT_LINE_WIDTH),\n            Updater.ALPHA_LABEL: (range(0, 255, 5), self.DEFAULT_LINE_ALPHA),\n            Updater.STYLE_LABEL: (list(Updater.LINE_STYLES),\n                                  Updater.DEFAULT_LINE_STYLE),\n        }\n\n        def update_lines(**settings):\n            self.reg_line_settings.update(**settings)\n            Updater.update_inf_lines(self.reg_line_items,\n                                     **self.reg_line_settings)\n            self.master.update_reg_line_label_colors()\n\n        def update_line_label(**settings):\n            self.reg_line_label_font = \\\n                Updater.change_font(self.reg_line_label_font, settings)\n            Updater.update_label_font(self.reg_line_label_items,\n                                      self.reg_line_label_font)\n\n        self._setters[self.LABELS_BOX][self.LINE_LAB_LABEL] = update_line_label\n        self._setters[self.PLOT_BOX] = {self.LINE_LABEL: update_lines}\n\n    @property\n    def axis_items(self):\n        return [value[\"item\"] for value in\n                self.master.plot_widget.plotItem.axes.values()]\n\n    @property\n    def reg_line_items(self):\n        return self.master.reg_line_items\n\n    @property\n    def reg_line_label_items(self):\n        return [line.label for line in self.master.reg_line_items\n                if hasattr(line, \"label\")]\n\n\nclass OWScatterPlotGraph(OWScatterPlotBase):\n    show_reg_line = Setting(False)\n    orthonormal_regression = Setting(False)\n    jitter_continuous = Setting(False)\n\n    def __init__(self, scatter_widget, parent):\n        super().__init__(scatter_widget, parent)\n        self.parameter_setter = ParameterSetter(self)\n        self.reg_line_items = []\n\n    def clear(self):\n        super().clear()\n        self.reg_line_items.clear()\n\n    def update_coordinates(self):\n        super().update_coordinates()\n        self.update_axes()\n        # Don't update_regression line here: update_coordinates is always\n        # followed by update_point_props, which calls update_colors\n\n    def update_colors(self):\n        super().update_colors()\n        self.update_regression_line()\n\n    def jitter_coordinates(self, x, y):\n        def get_span(attr):\n            if attr.is_discrete:\n                # Assuming the maximal jitter size is 10, a span of 4 will\n                # jitter by 4 * 10 / 100 = 0.4, so there will be no overlap\n                return 4\n            elif self.jitter_continuous:\n                return None  # Let _jitter_data determine the span\n            else:\n                return 0  # No jittering\n        span_x = get_span(self.master.attr_x)\n        span_y = get_span(self.master.attr_y)\n        if self.jitter_size == 0 or (span_x == 0 and span_y == 0):\n            return x, y\n        return self._jitter_data(x, y, span_x, span_y)\n\n    def update_axes(self):\n        for axis, var in self.master.get_axes().items():\n            axis_item = self.plot_widget.plotItem.getAxis(axis)\n            if var and var.is_discrete:\n                ticks = [list(enumerate(get_variable_values_sorted(var)))]\n                axis_item.setTicks(ticks)\n            else:\n                axis_item.setTicks(None)\n            use_time = var and var.is_time\n            self.plot_widget.plotItem.getAxis(axis).use_time(use_time)\n            self.plot_widget.setLabel(axis=axis, text=var or \"\")\n            if not var:\n                self.plot_widget.hideAxis(axis)\n\n    @staticmethod\n    def _orthonormal_line(x, y, color, width, style=Qt.SolidLine):\n        # https://en.wikipedia.org/wiki/Deming_regression, with δ=0.\n        pen = pg.mkPen(color=color, width=width, style=style)\n        xm = np.mean(x)\n        ym = np.mean(y)\n        sxx, sxy, _, syy = np.cov(x, y, ddof=1).flatten()\n\n        if sxy != 0:  # also covers sxx != 0 and syy != 0\n            slope = (syy - sxx + np.sqrt((syy - sxx) ** 2 + 4 * sxy ** 2)) \\\n                    / (2 * sxy)\n            intercept = ym - slope * xm\n            xmin = x.min()\n            return pg.InfiniteLine(\n                QPointF(xmin, xmin * slope + intercept),\n                np.degrees(np.arctan(slope)),\n                pen)\n        elif (sxx == 0) == (syy == 0):  # both zero or non-zero -> can't draw\n            return None\n        elif sxx != 0:\n            return pg.InfiniteLine(QPointF(x.min(), ym), 0, pen)\n        else:\n            return pg.InfiniteLine(QPointF(xm, y.min()), 90, pen)\n\n    @staticmethod\n    def _regression_line(x, y, color, width, style=Qt.SolidLine):\n        min_x, max_x = np.min(x), np.max(x)\n        if min_x == max_x:\n            return None\n        slope, intercept, rvalue, _, _ = linregress(x, y)\n        angle = np.degrees(np.arctan(slope))\n        start_y = min_x * slope + intercept\n        rotate = 135 < angle % 360 < 315\n        l_opts = dict(color=color, position=abs(rotate - 0.85),\n                      rotateAxis=(1, 0), movable=True)\n        reg_line_item = pg.InfiniteLine(\n            pos=QPointF(min_x, start_y), angle=angle,\n            pen=pg.mkPen(color=color, width=width, style=style),\n            label=f\"r = {rvalue:.2f}\", labelOpts=l_opts)\n        if rotate:\n            reg_line_item.label.angle = 180\n            reg_line_item.label.updateTransform()\n        return reg_line_item\n\n    def _add_line(self, x, y, color):\n        width = self.parameter_setter.reg_line_settings[Updater.WIDTH_LABEL]\n        alpha = self.parameter_setter.reg_line_settings[Updater.ALPHA_LABEL]\n        style = self.parameter_setter.reg_line_settings[Updater.STYLE_LABEL]\n        style = Updater.LINE_STYLES[style]\n        color.setAlpha(alpha)\n        if self.orthonormal_regression:\n            line = self._orthonormal_line(x, y, color, width, style)\n        else:\n            line = self._regression_line(x, y, color, width, style)\n        if line is None:\n            return\n        self.plot_widget.addItem(line)\n        self.reg_line_items.append(line)\n\n        if hasattr(line, \"label\"):\n            Updater.update_label_font(\n                [line.label], self.parameter_setter.reg_line_label_font\n            )\n\n    def update_reg_line_label_colors(self):\n        for line in self.reg_line_items:\n            if hasattr(line, \"label\"):\n                color = 0.0 if self.class_density \\\n                    else line.pen.color().darker(175)\n                line.label.setColor(color)\n\n    def update_density(self):\n        super().update_density()\n        self.update_reg_line_label_colors()\n\n    def update_regression_line(self):\n        for line in self.reg_line_items:\n            self.plot_widget.removeItem(line)\n        self.reg_line_items.clear()\n        if not (self.show_reg_line\n                and self.master.can_draw_regresssion_line()):\n            return\n        x, y = self.master.get_coordinates_data()\n        if x is None:\n            return\n        self._add_line(x, y, QColor(\"#505050\"))\n        if self.master.is_continuous_color() or self.palette is None:\n            return\n        c_data = self.master.get_color_data()\n        if c_data is None:\n            return\n        c_data = c_data.astype(int)\n        for val in range(c_data.max() + 1):\n            mask = c_data == val\n            if mask.sum() > 1:\n                self._add_line(x[mask], y[mask], self.palette[val].darker(135))\n        self.update_reg_line_label_colors()\n\n\nclass OWScatterPlot(OWDataProjectionWidget):\n    \"\"\"Scatterplot visualization with explorative analysis and intelligent\n    data visualization enhancements.\"\"\"\n\n    name = 'Scatter Plot'\n    description = \"Interactive scatter plot visualization with \" \\\n                  \"intelligent data visualization enhancements.\"\n    icon = \"icons/ScatterPlot.svg\"\n    priority = 140\n    keywords = []\n\n    class Inputs(OWDataProjectionWidget.Inputs):\n        features = Input(\"Features\", AttributeList)\n\n    class Outputs(OWDataProjectionWidget.Outputs):\n        features = Output(\"Features\", AttributeList, dynamic=False)\n\n    settings_version = 5\n    auto_sample = Setting(True)\n    attr_x = ContextSetting(None)\n    attr_y = ContextSetting(None)\n    tooltip_shows_all = Setting(True)\n\n    GRAPH_CLASS = OWScatterPlotGraph\n    graph = SettingProvider(OWScatterPlotGraph)\n    embedding_variables_names = None\n\n    xy_changed_manually = Signal(Variable, Variable)\n\n    class Warning(OWDataProjectionWidget.Warning):\n        missing_coords = Msg(\n            \"Plot cannot be displayed because '{}' or '{}' \"\n            \"is missing for all data points.\")\n\n    class Information(OWDataProjectionWidget.Information):\n        sampled_sql = Msg(\"Large SQL table; showing a sample.\")\n        missing_coords = Msg(\n            \"Points with missing '{}' or '{}' are not displayed\")\n\n    def __init__(self):\n        self.attr_box: QGroupBox = None\n        self.xy_model: DomainModel = None\n        self.cb_attr_x: ComboBoxSearch = None\n        self.cb_attr_y: ComboBoxSearch = None\n        self.vizrank: ScatterPlotVizRank = None\n        self.vizrank_button: QPushButton = None\n        self.sampling: QGroupBox = None\n\n        self.sql_data = None  # Orange.data.sql.table.SqlTable\n        self.attribute_selection_list = None  # list of Orange.data.Variable\n        self.__timer = QTimer(self, interval=1200)\n        self.__timer.timeout.connect(self.add_data)\n        super().__init__()\n\n        # manually register Matplotlib file writers\n        self.graph_writers = self.graph_writers.copy()\n        for w in [MatplotlibFormat, MatplotlibPDFFormat]:\n            self.graph_writers.append(w)\n\n    def _add_controls(self):\n        self._add_controls_axis()\n        self._add_controls_sampling()\n        super()._add_controls()\n        self.gui.add_widget(self.gui.JitterNumericValues, self._effects_box)\n        self.gui.add_widgets(\n            [self.gui.ShowGridLines,\n             self.gui.ToolTipShowsAll,\n             self.gui.RegressionLine],\n            self._plot_box)\n        gui.checkBox(\n            self._plot_box, self,\n            value=\"graph.orthonormal_regression\",\n            label=\"Treat variables as independent\",\n            callback=self.graph.update_regression_line,\n            tooltip=\n            \"If checked, fit line to group (minimize distance from points);\\n\"\n            \"otherwise fit y as a function of x (minimize vertical distances)\",\n            disabledBy=self.cb_reg_line)\n\n    def _add_controls_axis(self):\n        common_options = dict(\n            labelWidth=50, orientation=Qt.Horizontal, sendSelectedValue=True,\n            contentsLength=12, searchable=True\n        )\n        self.attr_box = gui.vBox(self.controlArea, 'Axes',\n                                 spacing=2 if gui.is_macstyle() else 8)\n        dmod = DomainModel\n        self.xy_model = DomainModel(dmod.MIXED, valid_types=dmod.PRIMITIVE)\n        self.cb_attr_x = gui.comboBox(\n            self.attr_box, self, \"attr_x\", label=\"Axis x:\",\n            callback=self.set_attr_from_combo,\n            model=self.xy_model, **common_options,\n        )\n        self.cb_attr_y = gui.comboBox(\n            self.attr_box, self, \"attr_y\", label=\"Axis y:\",\n            callback=self.set_attr_from_combo,\n            model=self.xy_model, **common_options,\n        )\n        vizrank_box = gui.hBox(self.attr_box)\n        self.vizrank, self.vizrank_button = ScatterPlotVizRank.add_vizrank(\n            vizrank_box, self, \"Find Informative Projections\", self.set_attr)\n\n    def _add_controls_sampling(self):\n        self.sampling = gui.auto_commit(\n            self.controlArea, self, \"auto_sample\", \"Sample\", box=\"Sampling\",\n            callback=self.switch_sampling, commit=lambda: self.add_data(1))\n        self.sampling.setVisible(False)\n\n    @property\n    def effective_variables(self):\n        return [self.attr_x, self.attr_y] if self.attr_x and self.attr_y else []\n\n    @property\n    def effective_data(self):\n        eff_var = self.effective_variables\n        if eff_var and self.attr_x.name == self.attr_y.name:\n            eff_var = [self.attr_x]\n        return self.data.transform(Domain(eff_var))\n\n    def _vizrank_color_change(self):\n        self.vizrank.initialize()\n        err_msg = \"\"\n        if self.data is None:\n            err_msg = \"No data on input\"\n        elif self.data.is_sparse():\n            err_msg = \"Data is sparse\"\n        elif len(self.xy_model) < 3:\n            err_msg = \"Not enough features for ranking\"\n        elif self.attr_color is None:\n            err_msg = \"Color variable is not selected\"\n        elif np.isnan(self.data.get_column_view(\n                self.attr_color)[0].astype(float)).all():\n            err_msg = \"Color variable has no values\"\n        self.vizrank_button.setEnabled(not err_msg)\n        self.vizrank_button.setToolTip(err_msg)\n\n    @OWDataProjectionWidget.Inputs.data\n    def set_data(self, data):\n        super().set_data(data)\n        self._vizrank_color_change()\n\n        def findvar(name, iterable):\n            \"\"\"Find a Orange.data.Variable in `iterable` by name\"\"\"\n            for el in iterable:\n                if isinstance(el, Variable) and el.name == name:\n                    return el\n            return None\n\n        # handle restored settings from  < 3.3.9 when attr_* were stored\n        # by name\n        if isinstance(self.attr_x, str):\n            self.attr_x = findvar(self.attr_x, self.xy_model)\n        if isinstance(self.attr_y, str):\n            self.attr_y = findvar(self.attr_y, self.xy_model)\n        if isinstance(self.attr_label, str):\n            self.attr_label = findvar(self.attr_label, self.gui.label_model)\n        if isinstance(self.attr_color, str):\n            self.attr_color = findvar(self.attr_color, self.gui.color_model)\n        if isinstance(self.attr_shape, str):\n            self.attr_shape = findvar(self.attr_shape, self.gui.shape_model)\n        if isinstance(self.attr_size, str):\n            self.attr_size = findvar(self.attr_size, self.gui.size_model)\n\n    def check_data(self):\n        super().check_data()\n        self.__timer.stop()\n        self.sampling.setVisible(False)\n        self.sql_data = None\n        if isinstance(self.data, SqlTable):\n            if self.data.approx_len() < 4000:\n                self.data = Table(self.data)\n            else:\n                self.Information.sampled_sql()\n                self.sql_data = self.data\n                data_sample = self.data.sample_time(0.8, no_cache=True)\n                data_sample.download_data(2000, partial=True)\n                self.data = Table(data_sample)\n                self.sampling.setVisible(True)\n                if self.auto_sample:\n                    self.__timer.start()\n\n        if self.data is not None and (len(self.data) == 0 or\n                                      len(self.data.domain.variables) == 0):\n            self.data = None\n\n    def get_embedding(self):\n        self.valid_data = None\n        if self.data is None:\n            return None\n\n        x_data = self.get_column(self.attr_x, filter_valid=False)\n        y_data = self.get_column(self.attr_y, filter_valid=False)\n        if x_data is None or y_data is None:\n            return None\n\n        self.Warning.missing_coords.clear()\n        self.Information.missing_coords.clear()\n        self.valid_data = np.isfinite(x_data) & np.isfinite(y_data)\n        if self.valid_data is not None and not np.all(self.valid_data):\n            msg = self.Information if np.any(self.valid_data) else self.Warning\n            msg.missing_coords(self.attr_x.name, self.attr_y.name)\n        return np.vstack((x_data, y_data)).T\n\n    # Tooltip\n    def _point_tooltip(self, point_id, skip_attrs=()):\n        point_data = self.data[point_id]\n        xy_attrs = (self.attr_x, self.attr_y)\n        text = \"<br/>\".join(\n            escape('{} = {}'.format(var.name, point_data[var]))\n            for var in xy_attrs)\n        if self.tooltip_shows_all:\n            others = super()._point_tooltip(point_id, skip_attrs=xy_attrs)\n            if others:\n                text = \"<b>{}</b><br/><br/>{}\".format(text, others)\n        return text\n\n    def can_draw_regresssion_line(self):\n        return self.data is not None and \\\n               self.data.domain is not None and \\\n               self.attr_x.is_continuous and \\\n               self.attr_y.is_continuous\n\n    def add_data(self, time=0.4):\n        if self.data and len(self.data) > 2000:\n            self.__timer.stop()\n            return\n        data_sample = self.sql_data.sample_time(time, no_cache=True)\n        if data_sample:\n            data_sample.download_data(2000, partial=True)\n            data = Table(data_sample)\n            self.data = Table.concatenate((self.data, data), axis=0)\n            self.handleNewSignals()\n\n    def init_attr_values(self):\n        super().init_attr_values()\n        data = self.data\n        domain = data.domain if data and len(data) else None\n        self.xy_model.set_domain(domain)\n        self.attr_x = self.xy_model[0] if self.xy_model else None\n        self.attr_y = self.xy_model[1] if len(self.xy_model) >= 2 \\\n            else self.attr_x\n\n    def switch_sampling(self):\n        self.__timer.stop()\n        if self.auto_sample and self.sql_data:\n            self.add_data()\n            self.__timer.start()\n\n    @OWDataProjectionWidget.Inputs.data_subset\n    def set_subset_data(self, subset_data):\n        self.warning()\n        if isinstance(subset_data, SqlTable):\n            if subset_data.approx_len() < AUTO_DL_LIMIT:\n                subset_data = Table(subset_data)\n            else:\n                self.warning(\"Data subset does not support large Sql tables\")\n                subset_data = None\n        super().set_subset_data(subset_data)\n\n    # called when all signals are received, so the graph is updated only once\n    def handleNewSignals(self):\n        self.attr_box.setEnabled(True)\n        self.vizrank.setEnabled(True)\n        if self.attribute_selection_list and self.data is not None and \\\n                self.data.domain is not None and \\\n                all(attr in self.data.domain for attr\n                        in self.attribute_selection_list):\n            self.attr_x, self.attr_y = self.attribute_selection_list[:2]\n            self.attr_box.setEnabled(False)\n            self.vizrank.setEnabled(False)\n        super().handleNewSignals()\n        if self._domain_invalidated:\n            self.graph.update_axes()\n            self._domain_invalidated = False\n        self.cb_reg_line.setEnabled(self.can_draw_regresssion_line())\n\n    @Inputs.features\n    def set_shown_attributes(self, attributes):\n        if attributes and len(attributes) >= 2:\n            self.attribute_selection_list = attributes[:2]\n            self._invalidated = self._invalidated \\\n                or self.attr_x != attributes[0] \\\n                or self.attr_y != attributes[1]\n        else:\n            self.attribute_selection_list = None\n\n    def set_attr(self, attr_x, attr_y):\n        if attr_x != self.attr_x or attr_y != self.attr_y:\n            self.attr_x, self.attr_y = attr_x, attr_y\n            self.attr_changed()\n\n    def set_attr_from_combo(self):\n        self.attr_changed()\n        self.xy_changed_manually.emit(self.attr_x, self.attr_y)\n\n    def attr_changed(self):\n        self.cb_reg_line.setEnabled(self.can_draw_regresssion_line())\n        self.setup_plot()\n        self.commit.deferred()\n\n    def get_axes(self):\n        return {\"bottom\": self.attr_x, \"left\": self.attr_y}\n\n    def colors_changed(self):\n        super().colors_changed()\n        self._vizrank_color_change()\n\n    @gui.deferred\n    def commit(self):\n        super().commit()\n        self.send_features()\n\n    def send_features(self):\n        features = [attr for attr in [self.attr_x, self.attr_y] if attr]\n        self.Outputs.features.send(AttributeList(features) or None)\n\n    def get_widget_name_extension(self):\n        if self.data is not None:\n            return \"{} vs {}\".format(self.attr_x.name, self.attr_y.name)\n        return None\n\n    def _get_send_report_caption(self):\n        return report.render_items_vert((\n            (\"Color\", self._get_caption_var_name(self.attr_color)),\n            (\"Label\", self._get_caption_var_name(self.attr_label)),\n            (\"Shape\", self._get_caption_var_name(self.attr_shape)),\n            (\"Size\", self._get_caption_var_name(self.attr_size)),\n            (\"Jittering\", (self.attr_x.is_discrete or\n                           self.attr_y.is_discrete or\n                           self.graph.jitter_continuous) and\n             self.graph.jitter_size)))\n\n    @classmethod\n    def migrate_settings(cls, settings, version):\n        if version < 2 and \"selection\" in settings and settings[\"selection\"]:\n            settings[\"selection_group\"] = [(a, 1) for a in settings[\"selection\"]]\n        if version < 3:\n            if \"auto_send_selection\" in settings:\n                settings[\"auto_commit\"] = settings[\"auto_send_selection\"]\n            if \"selection_group\" in settings:\n                settings[\"selection\"] = settings[\"selection_group\"]\n        if version < 5:\n            if \"graph\" in settings and \\\n                    \"jitter_continuous\" not in settings[\"graph\"]:\n                settings[\"graph\"][\"jitter_continuous\"] = True\n\n    @classmethod\n    def migrate_context(cls, context, version):\n        values = context.values\n        if version < 3:\n            values[\"attr_color\"] = values[\"graph\"][\"attr_color\"]\n            values[\"attr_size\"] = values[\"graph\"][\"attr_size\"]\n            values[\"attr_shape\"] = values[\"graph\"][\"attr_shape\"]\n            values[\"attr_label\"] = values[\"graph\"][\"attr_label\"]\n        if version < 4:\n            if values[\"attr_x\"][1] % 100 == 1 or values[\"attr_y\"][1] % 100 == 1:\n                raise IncompatibleContext()\n\n\nif __name__ == \"__main__\":  # pragma: no cover\n    table = Table(\"iris\")\n    WidgetPreview(OWScatterPlot).run(set_data=table,\n                                     set_subset_data=table[:30])\n",
            "new_file_content": "from itertools import chain\nfrom xml.sax.saxutils import escape\n\nimport numpy as np\nfrom scipy.stats import linregress\nfrom sklearn.neighbors import NearestNeighbors\nfrom sklearn.metrics import r2_score\n\nfrom AnyQt.QtCore import Qt, QTimer, QPointF, Signal\nfrom AnyQt.QtGui import QColor, QFont\nfrom AnyQt.QtWidgets import QGroupBox, QPushButton\n\nimport pyqtgraph as pg\n\nfrom orangewidget.utils.combobox import ComboBoxSearch\n\nfrom Orange.data import Table, Domain, DiscreteVariable, Variable\nfrom Orange.data.sql.table import SqlTable, AUTO_DL_LIMIT\nfrom Orange.preprocess.score import ReliefF, RReliefF\n\nfrom Orange.widgets import gui, report\nfrom Orange.widgets.io import MatplotlibFormat, MatplotlibPDFFormat\nfrom Orange.widgets.settings import (\n    Setting, ContextSetting, SettingProvider, IncompatibleContext)\nfrom Orange.widgets.utils import get_variable_values_sorted\nfrom Orange.widgets.utils.itemmodels import DomainModel\nfrom Orange.widgets.utils.widgetpreview import WidgetPreview\nfrom Orange.widgets.visualize.owscatterplotgraph import OWScatterPlotBase, \\\n    ScatterBaseParameterSetter\nfrom Orange.widgets.visualize.utils import VizRankDialogAttrPair\nfrom Orange.widgets.visualize.utils.customizableplot import Updater\nfrom Orange.widgets.visualize.utils.widget import OWDataProjectionWidget\nfrom Orange.widgets.widget import AttributeList, Msg, Input, Output\n\n\nclass ScatterPlotVizRank(VizRankDialogAttrPair):\n    captionTitle = \"Score Plots\"\n    minK = 10\n    attr_color = None\n\n    def __init__(self, master):\n        super().__init__(master)\n        self.attr_color = self.master.attr_color\n\n    def initialize(self):\n        self.attr_color = self.master.attr_color\n        super().initialize()\n\n    def check_preconditions(self):\n        self.Information.add_message(\n            \"color_required\", \"Color variable is not selected\")\n        self.Information.color_required.clear()\n        if not super().check_preconditions():\n            return False\n        if not self.attr_color:\n            self.Information.color_required()\n            return False\n        return True\n\n    def iterate_states(self, initial_state):\n        # If we put initialization of `self.attrs` to `initialize`,\n        # `score_heuristic` would be run on every call to `set_data`.\n        if initial_state is None:  # on the first call, compute order\n            self.attrs = self.score_heuristic()\n        yield from super().iterate_states(initial_state)\n\n    def compute_score(self, state):\n        # pylint: disable=invalid-unary-operand-type\n        attrs = [self.attrs[i] for i in state]\n        data = self.master.data\n        data = data.transform(Domain(attrs, self.attr_color))\n        data = data[~np.isnan(data.X).any(axis=1) & ~np.isnan(data.Y).T]\n        if len(data) < self.minK:\n            return None\n        n_neighbors = min(self.minK, len(data) - 1)\n        knn = NearestNeighbors(n_neighbors=n_neighbors).fit(data.X)\n        ind = knn.kneighbors(return_distance=False)\n        if data.domain.has_discrete_class:\n            return -np.sum(data.Y[ind] == data.Y.reshape(-1, 1)) / \\\n                   n_neighbors / len(data.Y)\n        else:\n            return -r2_score(data.Y, np.mean(data.Y[ind], axis=1)) * \\\n                   (len(data.Y) / len(self.master.data))\n\n    def bar_length(self, score):\n        return max(0, -score)\n\n    def score_heuristic(self):\n        assert self.attr_color is not None\n        master_domain = self.master.data.domain\n        vars = [v for v in chain(master_domain.variables, master_domain.metas)\n                if v is not self.attr_color and v.is_primitive()]\n        domain = Domain(attributes=vars, class_vars=self.attr_color)\n        data = self.master.data.transform(domain)\n        relief = ReliefF if isinstance(domain.class_var, DiscreteVariable) \\\n            else RReliefF\n        weights = relief(n_iterations=100, k_nearest=self.minK)(data)\n        attrs = sorted(zip(weights, domain.attributes),\n                       key=lambda x: (-x[0], x[1].name))\n        return [a for _, a in attrs]\n\n\nclass ParameterSetter(ScatterBaseParameterSetter):\n    DEFAULT_LINE_WIDTH = 3\n    DEFAULT_LINE_ALPHA = 255\n\n    def __init__(self, master):\n        super().__init__(master)\n        self.reg_line_label_font = QFont()\n        self.reg_line_settings = {\n            Updater.WIDTH_LABEL: self.DEFAULT_LINE_WIDTH,\n            Updater.ALPHA_LABEL: self.DEFAULT_LINE_ALPHA,\n            Updater.STYLE_LABEL: Updater.DEFAULT_LINE_STYLE,\n        }\n\n    def update_setters(self):\n        super().update_setters()\n        self.initial_settings[self.LABELS_BOX].update({\n            self.AXIS_TITLE_LABEL: self.FONT_SETTING,\n            self.AXIS_TICKS_LABEL: self.FONT_SETTING,\n            self.LINE_LAB_LABEL: self.FONT_SETTING\n        })\n        self.initial_settings[self.PLOT_BOX] = {}\n        self.initial_settings[self.PLOT_BOX][self.LINE_LABEL] = {\n            Updater.WIDTH_LABEL: (range(1, 10), self.DEFAULT_LINE_WIDTH),\n            Updater.ALPHA_LABEL: (range(0, 255, 5), self.DEFAULT_LINE_ALPHA),\n            Updater.STYLE_LABEL: (list(Updater.LINE_STYLES),\n                                  Updater.DEFAULT_LINE_STYLE),\n        }\n\n        def update_lines(**settings):\n            self.reg_line_settings.update(**settings)\n            Updater.update_inf_lines(self.reg_line_items,\n                                     **self.reg_line_settings)\n            self.master.update_reg_line_label_colors()\n\n        def update_line_label(**settings):\n            self.reg_line_label_font = \\\n                Updater.change_font(self.reg_line_label_font, settings)\n            Updater.update_label_font(self.reg_line_label_items,\n                                      self.reg_line_label_font)\n\n        self._setters[self.LABELS_BOX][self.LINE_LAB_LABEL] = update_line_label\n        self._setters[self.PLOT_BOX] = {self.LINE_LABEL: update_lines}\n\n    @property\n    def axis_items(self):\n        return [value[\"item\"] for value in\n                self.master.plot_widget.plotItem.axes.values()]\n\n    @property\n    def reg_line_items(self):\n        return self.master.reg_line_items\n\n    @property\n    def reg_line_label_items(self):\n        return [line.label for line in self.master.reg_line_items\n                if hasattr(line, \"label\")]\n\n\nclass OWScatterPlotGraph(OWScatterPlotBase):\n    show_reg_line = Setting(False)\n    orthonormal_regression = Setting(False)\n    jitter_continuous = Setting(False)\n\n    def __init__(self, scatter_widget, parent):\n        super().__init__(scatter_widget, parent)\n        self.parameter_setter = ParameterSetter(self)\n        self.reg_line_items = []\n\n    def clear(self):\n        super().clear()\n        self.reg_line_items.clear()\n\n    def update_coordinates(self):\n        super().update_coordinates()\n        self.update_axes()\n        # Don't update_regression line here: update_coordinates is always\n        # followed by update_point_props, which calls update_colors\n\n    def update_colors(self):\n        super().update_colors()\n        self.update_regression_line()\n\n    def jitter_coordinates(self, x, y):\n        def get_span(attr):\n            if attr.is_discrete:\n                # Assuming the maximal jitter size is 10, a span of 4 will\n                # jitter by 4 * 10 / 100 = 0.4, so there will be no overlap\n                return 4\n            elif self.jitter_continuous:\n                return None  # Let _jitter_data determine the span\n            else:\n                return 0  # No jittering\n        span_x = get_span(self.master.attr_x)\n        span_y = get_span(self.master.attr_y)\n        if self.jitter_size == 0 or (span_x == 0 and span_y == 0):\n            return x, y\n        return self._jitter_data(x, y, span_x, span_y)\n\n    def update_axes(self):\n        for axis, var in self.master.get_axes().items():\n            axis_item = self.plot_widget.plotItem.getAxis(axis)\n            if var and var.is_discrete:\n                ticks = [list(enumerate(get_variable_values_sorted(var)))]\n                axis_item.setTicks(ticks)\n            else:\n                axis_item.setTicks(None)\n            use_time = var and var.is_time\n            self.plot_widget.plotItem.getAxis(axis).use_time(use_time)\n            self.plot_widget.setLabel(axis=axis, text=var or \"\")\n            if not var:\n                self.plot_widget.hideAxis(axis)\n\n    @staticmethod\n    def _orthonormal_line(x, y, color, width, style=Qt.SolidLine):\n        # https://en.wikipedia.org/wiki/Deming_regression, with δ=0.\n        pen = pg.mkPen(color=color, width=width, style=style)\n        xm = np.mean(x)\n        ym = np.mean(y)\n        sxx, sxy, _, syy = np.cov(x, y, ddof=1).flatten()\n\n        if sxy != 0:  # also covers sxx != 0 and syy != 0\n            slope = (syy - sxx + np.sqrt((syy - sxx) ** 2 + 4 * sxy ** 2)) \\\n                    / (2 * sxy)\n            intercept = ym - slope * xm\n            xmin = x.min()\n            return pg.InfiniteLine(\n                QPointF(xmin, xmin * slope + intercept),\n                np.degrees(np.arctan(slope)),\n                pen)\n        elif (sxx == 0) == (syy == 0):  # both zero or non-zero -> can't draw\n            return None\n        elif sxx != 0:\n            return pg.InfiniteLine(QPointF(x.min(), ym), 0, pen)\n        else:\n            return pg.InfiniteLine(QPointF(xm, y.min()), 90, pen)\n\n    @staticmethod\n    def _regression_line(x, y, color, width, style=Qt.SolidLine):\n        min_x, max_x = np.min(x), np.max(x)\n        if min_x == max_x:\n            return None\n        slope, intercept, rvalue, _, _ = linregress(x, y)\n        angle = np.degrees(np.arctan(slope))\n        start_y = min_x * slope + intercept\n        rotate = 135 < angle % 360 < 315\n        l_opts = dict(color=color, position=abs(rotate - 0.85),\n                      rotateAxis=(1, 0), movable=True)\n        reg_line_item = pg.InfiniteLine(\n            pos=QPointF(min_x, start_y), angle=angle,\n            pen=pg.mkPen(color=color, width=width, style=style),\n            label=f\"r = {rvalue:.2f}\", labelOpts=l_opts)\n        if rotate:\n            reg_line_item.label.angle = 180\n            reg_line_item.label.updateTransform()\n        return reg_line_item\n\n    def _add_line(self, x, y, color):\n        width = self.parameter_setter.reg_line_settings[Updater.WIDTH_LABEL]\n        alpha = self.parameter_setter.reg_line_settings[Updater.ALPHA_LABEL]\n        style = self.parameter_setter.reg_line_settings[Updater.STYLE_LABEL]\n        style = Updater.LINE_STYLES[style]\n        color.setAlpha(alpha)\n        if self.orthonormal_regression:\n            line = self._orthonormal_line(x, y, color, width, style)\n        else:\n            line = self._regression_line(x, y, color, width, style)\n        if line is None:\n            return\n        self.plot_widget.addItem(line)\n        self.reg_line_items.append(line)\n\n        if hasattr(line, \"label\"):\n            Updater.update_label_font(\n                [line.label], self.parameter_setter.reg_line_label_font\n            )\n\n    def update_reg_line_label_colors(self):\n        for line in self.reg_line_items:\n            if hasattr(line, \"label\"):\n                color = 0.0 if self.class_density \\\n                    else line.pen.color().darker(175)\n                line.label.setColor(color)\n\n    def update_density(self):\n        super().update_density()\n        self.update_reg_line_label_colors()\n\n    def update_regression_line(self):\n        for line in self.reg_line_items:\n            self.plot_widget.removeItem(line)\n        self.reg_line_items.clear()\n        if not (self.show_reg_line\n                and self.master.can_draw_regresssion_line()):\n            return\n        x, y = self.master.get_coordinates_data()\n        if x is None:\n            return\n        self._add_line(x, y, QColor(\"#505050\"))\n        if self.master.is_continuous_color() or self.palette is None:\n            return\n        c_data = self.master.get_color_data()\n        if c_data is None:\n            return\n        c_data = c_data.astype(int)\n        for val in range(c_data.max() + 1):\n            mask = c_data == val\n            if mask.sum() > 1:\n                self._add_line(x[mask], y[mask], self.palette[val].darker(135))\n        self.update_reg_line_label_colors()\n\n\nclass OWScatterPlot(OWDataProjectionWidget):\n    \"\"\"Scatterplot visualization with explorative analysis and intelligent\n    data visualization enhancements.\"\"\"\n\n    name = 'Scatter Plot'\n    description = \"Interactive scatter plot visualization with \" \\\n                  \"intelligent data visualization enhancements.\"\n    icon = \"icons/ScatterPlot.svg\"\n    priority = 140\n    keywords = []\n\n    class Inputs(OWDataProjectionWidget.Inputs):\n        features = Input(\"Features\", AttributeList)\n\n    class Outputs(OWDataProjectionWidget.Outputs):\n        features = Output(\"Features\", AttributeList, dynamic=False)\n\n    settings_version = 5\n    auto_sample = Setting(True)\n    attr_x = ContextSetting(None)\n    attr_y = ContextSetting(None)\n    tooltip_shows_all = Setting(True)\n\n    GRAPH_CLASS = OWScatterPlotGraph\n    graph = SettingProvider(OWScatterPlotGraph)\n    embedding_variables_names = None\n\n    xy_changed_manually = Signal(Variable, Variable)\n\n    class Warning(OWDataProjectionWidget.Warning):\n        missing_coords = Msg(\n            \"Plot cannot be displayed because '{}' or '{}' \"\n            \"is missing for all data points.\")\n\n    class Information(OWDataProjectionWidget.Information):\n        sampled_sql = Msg(\"Large SQL table; showing a sample.\")\n        missing_coords = Msg(\n            \"Points with missing '{}' or '{}' are not displayed\")\n\n    def __init__(self):\n        self.attr_box: QGroupBox = None\n        self.xy_model: DomainModel = None\n        self.cb_attr_x: ComboBoxSearch = None\n        self.cb_attr_y: ComboBoxSearch = None\n        self.vizrank: ScatterPlotVizRank = None\n        self.vizrank_button: QPushButton = None\n        self.sampling: QGroupBox = None\n        self._xy_invalidated: bool = True\n\n        self.sql_data = None  # Orange.data.sql.table.SqlTable\n        self.attribute_selection_list = None  # list of Orange.data.Variable\n        self.__timer = QTimer(self, interval=1200)\n        self.__timer.timeout.connect(self.add_data)\n        super().__init__()\n\n        # manually register Matplotlib file writers\n        self.graph_writers = self.graph_writers.copy()\n        for w in [MatplotlibFormat, MatplotlibPDFFormat]:\n            self.graph_writers.append(w)\n\n    def _add_controls(self):\n        self._add_controls_axis()\n        self._add_controls_sampling()\n        super()._add_controls()\n        self.gui.add_widget(self.gui.JitterNumericValues, self._effects_box)\n        self.gui.add_widgets(\n            [self.gui.ShowGridLines,\n             self.gui.ToolTipShowsAll,\n             self.gui.RegressionLine],\n            self._plot_box)\n        gui.checkBox(\n            self._plot_box, self,\n            value=\"graph.orthonormal_regression\",\n            label=\"Treat variables as independent\",\n            callback=self.graph.update_regression_line,\n            tooltip=\n            \"If checked, fit line to group (minimize distance from points);\\n\"\n            \"otherwise fit y as a function of x (minimize vertical distances)\",\n            disabledBy=self.cb_reg_line)\n\n    def _add_controls_axis(self):\n        common_options = dict(\n            labelWidth=50, orientation=Qt.Horizontal, sendSelectedValue=True,\n            contentsLength=12, searchable=True\n        )\n        self.attr_box = gui.vBox(self.controlArea, 'Axes',\n                                 spacing=2 if gui.is_macstyle() else 8)\n        dmod = DomainModel\n        self.xy_model = DomainModel(dmod.MIXED, valid_types=dmod.PRIMITIVE)\n        self.cb_attr_x = gui.comboBox(\n            self.attr_box, self, \"attr_x\", label=\"Axis x:\",\n            callback=self.set_attr_from_combo,\n            model=self.xy_model, **common_options,\n        )\n        self.cb_attr_y = gui.comboBox(\n            self.attr_box, self, \"attr_y\", label=\"Axis y:\",\n            callback=self.set_attr_from_combo,\n            model=self.xy_model, **common_options,\n        )\n        vizrank_box = gui.hBox(self.attr_box)\n        self.vizrank, self.vizrank_button = ScatterPlotVizRank.add_vizrank(\n            vizrank_box, self, \"Find Informative Projections\", self.set_attr)\n\n    def _add_controls_sampling(self):\n        self.sampling = gui.auto_commit(\n            self.controlArea, self, \"auto_sample\", \"Sample\", box=\"Sampling\",\n            callback=self.switch_sampling, commit=lambda: self.add_data(1))\n        self.sampling.setVisible(False)\n\n    @property\n    def effective_variables(self):\n        return [self.attr_x, self.attr_y] if self.attr_x and self.attr_y else []\n\n    @property\n    def effective_data(self):\n        eff_var = self.effective_variables\n        if eff_var and self.attr_x.name == self.attr_y.name:\n            eff_var = [self.attr_x]\n        return self.data.transform(Domain(eff_var))\n\n    def _vizrank_color_change(self):\n        self.vizrank.initialize()\n        err_msg = \"\"\n        if self.data is None:\n            err_msg = \"No data on input\"\n        elif self.data.is_sparse():\n            err_msg = \"Data is sparse\"\n        elif len(self.xy_model) < 3:\n            err_msg = \"Not enough features for ranking\"\n        elif self.attr_color is None:\n            err_msg = \"Color variable is not selected\"\n        elif np.isnan(self.data.get_column_view(\n                self.attr_color)[0].astype(float)).all():\n            err_msg = \"Color variable has no values\"\n        self.vizrank_button.setEnabled(not err_msg)\n        self.vizrank_button.setToolTip(err_msg)\n\n    @OWDataProjectionWidget.Inputs.data\n    def set_data(self, data):\n        super().set_data(data)\n        self._vizrank_color_change()\n\n        def findvar(name, iterable):\n            \"\"\"Find a Orange.data.Variable in `iterable` by name\"\"\"\n            for el in iterable:\n                if isinstance(el, Variable) and el.name == name:\n                    return el\n            return None\n\n        # handle restored settings from  < 3.3.9 when attr_* were stored\n        # by name\n        if isinstance(self.attr_x, str):\n            self.attr_x = findvar(self.attr_x, self.xy_model)\n        if isinstance(self.attr_y, str):\n            self.attr_y = findvar(self.attr_y, self.xy_model)\n        if isinstance(self.attr_label, str):\n            self.attr_label = findvar(self.attr_label, self.gui.label_model)\n        if isinstance(self.attr_color, str):\n            self.attr_color = findvar(self.attr_color, self.gui.color_model)\n        if isinstance(self.attr_shape, str):\n            self.attr_shape = findvar(self.attr_shape, self.gui.shape_model)\n        if isinstance(self.attr_size, str):\n            self.attr_size = findvar(self.attr_size, self.gui.size_model)\n\n    def check_data(self):\n        super().check_data()\n        self.__timer.stop()\n        self.sampling.setVisible(False)\n        self.sql_data = None\n        if isinstance(self.data, SqlTable):\n            if self.data.approx_len() < 4000:\n                self.data = Table(self.data)\n            else:\n                self.Information.sampled_sql()\n                self.sql_data = self.data\n                data_sample = self.data.sample_time(0.8, no_cache=True)\n                data_sample.download_data(2000, partial=True)\n                self.data = Table(data_sample)\n                self.sampling.setVisible(True)\n                if self.auto_sample:\n                    self.__timer.start()\n\n        if self.data is not None and (len(self.data) == 0 or\n                                      len(self.data.domain.variables) == 0):\n            self.data = None\n\n    def get_embedding(self):\n        self.valid_data = None\n        if self.data is None:\n            return None\n\n        x_data = self.get_column(self.attr_x, filter_valid=False)\n        y_data = self.get_column(self.attr_y, filter_valid=False)\n        if x_data is None or y_data is None:\n            return None\n\n        self.Warning.missing_coords.clear()\n        self.Information.missing_coords.clear()\n        self.valid_data = np.isfinite(x_data) & np.isfinite(y_data)\n        if self.valid_data is not None and not np.all(self.valid_data):\n            msg = self.Information if np.any(self.valid_data) else self.Warning\n            msg.missing_coords(self.attr_x.name, self.attr_y.name)\n        return np.vstack((x_data, y_data)).T\n\n    # Tooltip\n    def _point_tooltip(self, point_id, skip_attrs=()):\n        point_data = self.data[point_id]\n        xy_attrs = (self.attr_x, self.attr_y)\n        text = \"<br/>\".join(\n            escape('{} = {}'.format(var.name, point_data[var]))\n            for var in xy_attrs)\n        if self.tooltip_shows_all:\n            others = super()._point_tooltip(point_id, skip_attrs=xy_attrs)\n            if others:\n                text = \"<b>{}</b><br/><br/>{}\".format(text, others)\n        return text\n\n    def can_draw_regresssion_line(self):\n        return self.data is not None and \\\n               self.data.domain is not None and \\\n               self.attr_x.is_continuous and \\\n               self.attr_y.is_continuous\n\n    def add_data(self, time=0.4):\n        if self.data and len(self.data) > 2000:\n            self.__timer.stop()\n            return\n        data_sample = self.sql_data.sample_time(time, no_cache=True)\n        if data_sample:\n            data_sample.download_data(2000, partial=True)\n            data = Table(data_sample)\n            self.data = Table.concatenate((self.data, data), axis=0)\n            self.handleNewSignals()\n\n    def init_attr_values(self):\n        super().init_attr_values()\n        data = self.data\n        domain = data.domain if data and len(data) else None\n        self.xy_model.set_domain(domain)\n        self.attr_x = self.xy_model[0] if self.xy_model else None\n        self.attr_y = self.xy_model[1] if len(self.xy_model) >= 2 \\\n            else self.attr_x\n\n    def switch_sampling(self):\n        self.__timer.stop()\n        if self.auto_sample and self.sql_data:\n            self.add_data()\n            self.__timer.start()\n\n    @OWDataProjectionWidget.Inputs.data_subset\n    def set_subset_data(self, subset_data):\n        self.warning()\n        if isinstance(subset_data, SqlTable):\n            if subset_data.approx_len() < AUTO_DL_LIMIT:\n                subset_data = Table(subset_data)\n            else:\n                self.warning(\"Data subset does not support large Sql tables\")\n                subset_data = None\n        super().set_subset_data(subset_data)\n\n    # called when all signals are received, so the graph is updated only once\n    def handleNewSignals(self):\n        self.attr_box.setEnabled(True)\n        self.vizrank.setEnabled(True)\n        if self.attribute_selection_list and self.data is not None and \\\n                self.data.domain is not None and \\\n                all(attr in self.data.domain for attr\n                        in self.attribute_selection_list):\n            self.attr_x, self.attr_y = self.attribute_selection_list[:2]\n            self.attr_box.setEnabled(False)\n            self.vizrank.setEnabled(False)\n        self._invalidated = self._invalidated or self._xy_invalidated\n        self._xy_invalidated = False\n        super().handleNewSignals()\n        if self._domain_invalidated:\n            self.graph.update_axes()\n            self._domain_invalidated = False\n        self.cb_reg_line.setEnabled(self.can_draw_regresssion_line())\n\n    @Inputs.features\n    def set_shown_attributes(self, attributes):\n        if attributes and len(attributes) >= 2:\n            self.attribute_selection_list = attributes[:2]\n            self._xy_invalidated = self._xy_invalidated \\\n                or self.attr_x != attributes[0] \\\n                or self.attr_y != attributes[1]\n        else:\n            self.attribute_selection_list = None\n\n    def set_attr(self, attr_x, attr_y):\n        if attr_x != self.attr_x or attr_y != self.attr_y:\n            self.attr_x, self.attr_y = attr_x, attr_y\n            self.attr_changed()\n\n    def set_attr_from_combo(self):\n        self.attr_changed()\n        self.xy_changed_manually.emit(self.attr_x, self.attr_y)\n\n    def attr_changed(self):\n        self.cb_reg_line.setEnabled(self.can_draw_regresssion_line())\n        self.setup_plot()\n        self.commit.deferred()\n\n    def get_axes(self):\n        return {\"bottom\": self.attr_x, \"left\": self.attr_y}\n\n    def colors_changed(self):\n        super().colors_changed()\n        self._vizrank_color_change()\n\n    @gui.deferred\n    def commit(self):\n        super().commit()\n        self.send_features()\n\n    def send_features(self):\n        features = [attr for attr in [self.attr_x, self.attr_y] if attr]\n        self.Outputs.features.send(AttributeList(features) or None)\n\n    def get_widget_name_extension(self):\n        if self.data is not None:\n            return \"{} vs {}\".format(self.attr_x.name, self.attr_y.name)\n        return None\n\n    def _get_send_report_caption(self):\n        return report.render_items_vert((\n            (\"Color\", self._get_caption_var_name(self.attr_color)),\n            (\"Label\", self._get_caption_var_name(self.attr_label)),\n            (\"Shape\", self._get_caption_var_name(self.attr_shape)),\n            (\"Size\", self._get_caption_var_name(self.attr_size)),\n            (\"Jittering\", (self.attr_x.is_discrete or\n                           self.attr_y.is_discrete or\n                           self.graph.jitter_continuous) and\n             self.graph.jitter_size)))\n\n    @classmethod\n    def migrate_settings(cls, settings, version):\n        if version < 2 and \"selection\" in settings and settings[\"selection\"]:\n            settings[\"selection_group\"] = [(a, 1) for a in settings[\"selection\"]]\n        if version < 3:\n            if \"auto_send_selection\" in settings:\n                settings[\"auto_commit\"] = settings[\"auto_send_selection\"]\n            if \"selection_group\" in settings:\n                settings[\"selection\"] = settings[\"selection_group\"]\n        if version < 5:\n            if \"graph\" in settings and \\\n                    \"jitter_continuous\" not in settings[\"graph\"]:\n                settings[\"graph\"][\"jitter_continuous\"] = True\n\n    @classmethod\n    def migrate_context(cls, context, version):\n        values = context.values\n        if version < 3:\n            values[\"attr_color\"] = values[\"graph\"][\"attr_color\"]\n            values[\"attr_size\"] = values[\"graph\"][\"attr_size\"]\n            values[\"attr_shape\"] = values[\"graph\"][\"attr_shape\"]\n            values[\"attr_label\"] = values[\"graph\"][\"attr_label\"]\n        if version < 4:\n            if values[\"attr_x\"][1] % 100 == 1 or values[\"attr_y\"][1] % 100 == 1:\n                raise IncompatibleContext()\n\n\nif __name__ == \"__main__\":  # pragma: no cover\n    table = Table(\"iris\")\n    WidgetPreview(OWScatterPlot).run(set_data=table,\n                                     set_subset_data=table[:30])\n",
            "header": {
                "file": {
                    "path": "Orange/widgets/visualize/owscatterplot.py"
                },
                "misc_line": null
            },
            "index_line": {
                "old_commit_hash": "d877d0c72",
                "new_commit_hash": "8a12ea558",
                "mode": "100644"
            },
            "is_binary_file": false,
            "binary_line": null,
            "minus_file": {
                "path": "a/Orange/widgets/visualize/owscatterplot.py"
            },
            "plus_file": {
                "path": "b/Orange/widgets/visualize/owscatterplot.py"
            },
            "hunks": [
                {
                    "descriptor": {
                        "old_range": {
                            "start": 358,
                            "length": 6
                        },
                        "new_range": {
                            "start": 358,
                            "length": 7
                        },
                        "section": "class OWScatterPlot(OWDataProjectionWidget):"
                    },
                    "line_group": {
                        "all_lines": [
                            {
                                "content": "        self.vizrank: ScatterPlotVizRank = None",
                                "type": "context"
                            },
                            {
                                "content": "        self.vizrank_button: QPushButton = None",
                                "type": "context"
                            },
                            {
                                "content": "        self.sampling: QGroupBox = None",
                                "type": "context"
                            },
                            {
                                "content": "        self._xy_invalidated: bool = True",
                                "type": "added"
                            },
                            {
                                "content": "",
                                "type": "context"
                            },
                            {
                                "content": "        self.sql_data = None  # Orange.data.sql.table.SqlTable",
                                "type": "context"
                            },
                            {
                                "content": "        self.attribute_selection_list = None  # list of Orange.data.Variable",
                                "type": "context"
                            }
                        ]
                    },
                    "modified_entities": [
                        {
                            "file_name": "Orange/widgets/visualize/owscatterplot.py",
                            "type": "method",
                            "name": "OWScatterPlot.__init__",
                            "content": "class OWScatterPlot(OWDataProjectionWidget):\n    \"\"\"Scatterplot visualization with explorative analysis and intelligent\n    data visualization enhancements.\"\"\"\n\n    name = 'Scatter Plot'\n    description = \"Interactive scatter plot visualization with \" \\\n                  \"intelligent data visualization enhancements.\"\n    icon = \"icons/ScatterPlot.svg\"\n    priority = 140\n    keywords = []\n\n    class Inputs(OWDataProjectionWidget.Inputs):\n        features = Input(\"Features\", AttributeList)\n\n    class Outputs(OWDataProjectionWidget.Outputs):\n        features = Output(\"Features\", AttributeList, dynamic=False)\n\n    settings_version = 5\n    auto_sample = Setting(True)\n    attr_x = ContextSetting(None)\n    attr_y = ContextSetting(None)\n    tooltip_shows_all = Setting(True)\n\n    GRAPH_CLASS = OWScatterPlotGraph\n    graph = SettingProvider(OWScatterPlotGraph)\n    embedding_variables_names = None\n\n    xy_changed_manually = Signal(Variable, Variable)\n\n    class Warning(OWDataProjectionWidget.Warning):\n        missing_coords = Msg(\n            \"Plot cannot be displayed because '{}' or '{}' \"\n            \"is missing for all data points.\")\n\n    class Information(OWDataProjectionWidget.Information):\n        sampled_sql = Msg(\"Large SQL table; showing a sample.\")\n        missing_coords = Msg(\n            \"Points with missing '{}' or '{}' are not displayed\")\n\n    def __init__(self):\n        self.attr_box: QGroupBox = None\n        self.xy_model: DomainModel = None\n        self.cb_attr_x: ComboBoxSearch = None\n        self.cb_attr_y: ComboBoxSearch = None\n        self.vizrank: ScatterPlotVizRank = None\n        self.vizrank_button: QPushButton = None\n        self.sampling: QGroupBox = None\n        self._xy_invalidated: bool = True\n\n        self.sql_data = None  # Orange.data.sql.table.SqlTable\n        self.attribute_selection_list = None  # list of Orange.data.Variable\n        self.__timer = QTimer(self, interval=1200)\n        self.__timer.timeout.connect(self.add_data)\n        super().__init__()\n\n        # manually register Matplotlib file writers\n        self.graph_writers = self.graph_writers.copy()\n        for w in [MatplotlibFormat, MatplotlibPDFFormat]:\n            self.graph_writers.append(w)\n\n    def _add_controls(self):\n        self._add_controls_axis()\n        self._add_controls_sampling()\n        super()._add_controls()\n        self.gui.add_widget(self.gui.JitterNumericValues, self._effects_box)\n        self.gui.add_widgets(\n            [self.gui.ShowGridLines,\n             self.gui.ToolTipShowsAll,\n             self.gui.RegressionLine],\n            self._plot_box)\n        gui.checkBox(\n            self._plot_box, self,\n            value=\"graph.orthonormal_regression\",\n            label=\"Treat variables as independent\",\n            callback=self.graph.update_regression_line,\n            tooltip=\n            \"If checked, fit line to group (minimize distance from points);\\n\"\n            \"otherwise fit y as a function of x (minimize vertical distances)\",\n            disabledBy=self.cb_reg_line)\n\n    def _add_controls_axis(self):\n        common_options = dict(\n            labelWidth=50, orientation=Qt.Horizontal, sendSelectedValue=True,\n            contentsLength=12, searchable=True\n        )\n        self.attr_box = gui.vBox(self.controlArea, 'Axes',\n                                 spacing=2 if gui.is_macstyle() else 8)\n        dmod = DomainModel\n        self.xy_model = DomainModel(dmod.MIXED, valid_types=dmod.PRIMITIVE)\n        self.cb_attr_x = gui.comboBox(\n            self.attr_box, self, \"attr_x\", label=\"Axis x:\",\n            callback=self.set_attr_from_combo,\n            model=self.xy_model, **common_options,\n        )\n        self.cb_attr_y = gui.comboBox(\n            self.attr_box, self, \"attr_y\", label=\"Axis y:\",\n            callback=self.set_attr_from_combo,\n            model=self.xy_model, **common_options,\n        )\n        vizrank_box = gui.hBox(self.attr_box)\n        self.vizrank, self.vizrank_button = ScatterPlotVizRank.add_vizrank(\n            vizrank_box, self, \"Find Informative Projections\", self.set_attr)\n\n    def _add_controls_sampling(self):\n        self.sampling = gui.auto_commit(\n            self.controlArea, self, \"auto_sample\", \"Sample\", box=\"Sampling\",\n            callback=self.switch_sampling, commit=lambda: self.add_data(1))\n        self.sampling.setVisible(False)\n\n    @property\n    def effective_variables(self):\n        return [self.attr_x, self.attr_y] if self.attr_x and self.attr_y else []\n\n    @property\n    def effective_data(self):\n        eff_var = self.effective_variables\n        if eff_var and self.attr_x.name == self.attr_y.name:\n            eff_var = [self.attr_x]\n        return self.data.transform(Domain(eff_var))\n\n    def _vizrank_color_change(self):\n        self.vizrank.initialize()\n        err_msg = \"\"\n        if self.data is None:\n            err_msg = \"No data on input\"\n        elif self.data.is_sparse():\n            err_msg = \"Data is sparse\"\n        elif len(self.xy_model) < 3:\n            err_msg = \"Not enough features for ranking\"\n        elif self.attr_color is None:\n            err_msg = \"Color variable is not selected\"\n        elif np.isnan(self.data.get_column_view(\n                self.attr_color)[0].astype(float)).all():\n            err_msg = \"Color variable has no values\"\n        self.vizrank_button.setEnabled(not err_msg)\n        self.vizrank_button.setToolTip(err_msg)\n\n    @OWDataProjectionWidget.Inputs.data\n    def set_data(self, data):\n        super().set_data(data)\n        self._vizrank_color_change()\n\n        def findvar(name, iterable):\n            \"\"\"Find a Orange.data.Variable in `iterable` by name\"\"\"\n            for el in iterable:\n                if isinstance(el, Variable) and el.name == name:\n                    return el\n            return None\n\n        # handle restored settings from  < 3.3.9 when attr_* were stored\n        # by name\n        if isinstance(self.attr_x, str):\n            self.attr_x = findvar(self.attr_x, self.xy_model)\n        if isinstance(self.attr_y, str):\n            self.attr_y = findvar(self.attr_y, self.xy_model)\n        if isinstance(self.attr_label, str):\n            self.attr_label = findvar(self.attr_label, self.gui.label_model)\n        if isinstance(self.attr_color, str):\n            self.attr_color = findvar(self.attr_color, self.gui.color_model)\n        if isinstance(self.attr_shape, str):\n            self.attr_shape = findvar(self.attr_shape, self.gui.shape_model)\n        if isinstance(self.attr_size, str):\n            self.attr_size = findvar(self.attr_size, self.gui.size_model)\n\n    def check_data(self):\n        super().check_data()\n        self.__timer.stop()\n        self.sampling.setVisible(False)\n        self.sql_data = None\n        if isinstance(self.data, SqlTable):\n            if self.data.approx_len() < 4000:\n                self.data = Table(self.data)\n            else:\n                self.Information.sampled_sql()\n                self.sql_data = self.data\n                data_sample = self.data.sample_time(0.8, no_cache=True)\n                data_sample.download_data(2000, partial=True)\n                self.data = Table(data_sample)\n                self.sampling.setVisible(True)\n                if self.auto_sample:\n                    self.__timer.start()\n\n        if self.data is not None and (len(self.data) == 0 or\n                                      len(self.data.domain.variables) == 0):\n            self.data = None\n\n    def get_embedding(self):\n        self.valid_data = None\n        if self.data is None:\n            return None\n\n        x_data = self.get_column(self.attr_x, filter_valid=False)\n        y_data = self.get_column(self.attr_y, filter_valid=False)\n        if x_data is None or y_data is None:\n            return None\n\n        self.Warning.missing_coords.clear()\n        self.Information.missing_coords.clear()\n        self.valid_data = np.isfinite(x_data) & np.isfinite(y_data)\n        if self.valid_data is not None and not np.all(self.valid_data):\n            msg = self.Information if np.any(self.valid_data) else self.Warning\n            msg.missing_coords(self.attr_x.name, self.attr_y.name)\n        return np.vstack((x_data, y_data)).T\n\n    # Tooltip\n    def _point_tooltip(self, point_id, skip_attrs=()):\n        point_data = self.data[point_id]\n        xy_attrs = (self.attr_x, self.attr_y)\n        text = \"<br/>\".join(\n            escape('{} = {}'.format(var.name, point_data[var]))\n            for var in xy_attrs)\n        if self.tooltip_shows_all:\n            others = super()._point_tooltip(point_id, skip_attrs=xy_attrs)\n            if others:\n                text = \"<b>{}</b><br/><br/>{}\".format(text, others)\n        return text\n\n    def can_draw_regresssion_line(self):\n        return self.data is not None and \\\n               self.data.domain is not None and \\\n               self.attr_x.is_continuous and \\\n               self.attr_y.is_continuous\n\n    def add_data(self, time=0.4):\n        if self.data and len(self.data) > 2000:\n            self.__timer.stop()\n            return\n        data_sample = self.sql_data.sample_time(time, no_cache=True)\n        if data_sample:\n            data_sample.download_data(2000, partial=True)\n            data = Table(data_sample)\n            self.data = Table.concatenate((self.data, data), axis=0)\n            self.handleNewSignals()\n\n    def init_attr_values(self):\n        super().init_attr_values()\n        data = self.data\n        domain = data.domain if data and len(data) else None\n        self.xy_model.set_domain(domain)\n        self.attr_x = self.xy_model[0] if self.xy_model else None\n        self.attr_y = self.xy_model[1] if len(self.xy_model) >= 2 \\\n            else self.attr_x\n\n    def switch_sampling(self):\n        self.__timer.stop()\n        if self.auto_sample and self.sql_data:\n            self.add_data()\n            self.__timer.start()\n\n    @OWDataProjectionWidget.Inputs.data_subset\n    def set_subset_data(self, subset_data):\n        self.warning()\n        if isinstance(subset_data, SqlTable):\n            if subset_data.approx_len() < AUTO_DL_LIMIT:\n                subset_data = Table(subset_data)\n            else:\n                self.warning(\"Data subset does not support large Sql tables\")\n                subset_data = None\n        super().set_subset_data(subset_data)\n\n    # called when all signals are received, so the graph is updated only once\n    def handleNewSignals(self):\n        self.attr_box.setEnabled(True)\n        self.vizrank.setEnabled(True)\n        if self.attribute_selection_list and self.data is not None and \\\n                self.data.domain is not None and \\\n                all(attr in self.data.domain for attr\n                        in self.attribute_selection_list):\n            self.attr_x, self.attr_y = self.attribute_selection_list[:2]\n            self.attr_box.setEnabled(False)\n            self.vizrank.setEnabled(False)\n        self._invalidated = self._invalidated or self._xy_invalidated\n        self._xy_invalidated = False\n        super().handleNewSignals()\n        if self._domain_invalidated:\n            self.graph.update_axes()\n            self._domain_invalidated = False\n        self.cb_reg_line.setEnabled(self.can_draw_regresssion_line())\n\n    @Inputs.features\n    def set_shown_attributes(self, attributes):\n        if attributes and len(attributes) >= 2:\n            self.attribute_selection_list = attributes[:2]\n            self._xy_invalidated = self._xy_invalidated \\\n                or self.attr_x != attributes[0] \\\n                or self.attr_y != attributes[1]\n        else:\n            self.attribute_selection_list = None\n\n    def set_attr(self, attr_x, attr_y):\n        if attr_x != self.attr_x or attr_y != self.attr_y:\n            self.attr_x, self.attr_y = attr_x, attr_y\n            self.attr_changed()\n\n    def set_attr_from_combo(self):\n        self.attr_changed()\n        self.xy_changed_manually.emit(self.attr_x, self.attr_y)\n\n    def attr_changed(self):\n        self.cb_reg_line.setEnabled(self.can_draw_regresssion_line())\n        self.setup_plot()\n        self.commit.deferred()\n\n    def get_axes(self):\n        return {\"bottom\": self.attr_x, \"left\": self.attr_y}\n\n    def colors_changed(self):\n        super().colors_changed()\n        self._vizrank_color_change()\n\n    @gui.deferred\n    def commit(self):\n        super().commit()\n        self.send_features()\n\n    def send_features(self):\n        features = [attr for attr in [self.attr_x, self.attr_y] if attr]\n        self.Outputs.features.send(AttributeList(features) or None)\n\n    def get_widget_name_extension(self):\n        if self.data is not None:\n            return \"{} vs {}\".format(self.attr_x.name, self.attr_y.name)\n        return None\n\n    def _get_send_report_caption(self):\n        return report.render_items_vert((\n            (\"Color\", self._get_caption_var_name(self.attr_color)),\n            (\"Label\", self._get_caption_var_name(self.attr_label)),\n            (\"Shape\", self._get_caption_var_name(self.attr_shape)),\n            (\"Size\", self._get_caption_var_name(self.attr_size)),\n            (\"Jittering\", (self.attr_x.is_discrete or\n                           self.attr_y.is_discrete or\n                           self.graph.jitter_continuous) and\n             self.graph.jitter_size)))\n\n    @classmethod\n    def migrate_settings(cls, settings, version):\n        if version < 2 and \"selection\" in settings and settings[\"selection\"]:\n            settings[\"selection_group\"] = [(a, 1) for a in settings[\"selection\"]]\n        if version < 3:\n            if \"auto_send_selection\" in settings:\n                settings[\"auto_commit\"] = settings[\"auto_send_selection\"]\n            if \"selection_group\" in settings:\n                settings[\"selection\"] = settings[\"selection_group\"]\n        if version < 5:\n            if \"graph\" in settings and \\\n                    \"jitter_continuous\" not in settings[\"graph\"]:\n                settings[\"graph\"][\"jitter_continuous\"] = True\n\n    @classmethod\n    def migrate_context(cls, context, version):\n        values = context.values\n        if version < 3:\n            values[\"attr_color\"] = values[\"graph\"][\"attr_color\"]\n            values[\"attr_size\"] = values[\"graph\"][\"attr_size\"]\n            values[\"attr_shape\"] = values[\"graph\"][\"attr_shape\"]\n            values[\"attr_label\"] = values[\"graph\"][\"attr_label\"]\n        if version < 4:\n            if values[\"attr_x\"][1] % 100 == 1 or values[\"attr_y\"][1] % 100 == 1:\n                raise IncompatibleContext()",
                            "ast_type_str": "ClassDef",
                            "start_lineno": 353,
                            "end_lineno": 372,
                            "parent": {
                                "file_name": "Orange/widgets/visualize/owscatterplot.py",
                                "type": "class",
                                "name": "OWScatterPlot",
                                "content": "class OWScatterPlot(OWDataProjectionWidget):\n    \"\"\"Scatterplot visualization with explorative analysis and intelligent\n    data visualization enhancements.\"\"\"\n\n    name = 'Scatter Plot'\n    description = \"Interactive scatter plot visualization with \" \\\n                  \"intelligent data visualization enhancements.\"\n    icon = \"icons/ScatterPlot.svg\"\n    priority = 140\n    keywords = []\n\n    class Inputs(OWDataProjectionWidget.Inputs):\n        features = Input(\"Features\", AttributeList)\n\n    class Outputs(OWDataProjectionWidget.Outputs):\n        features = Output(\"Features\", AttributeList, dynamic=False)\n\n    settings_version = 5\n    auto_sample = Setting(True)\n    attr_x = ContextSetting(None)\n    attr_y = ContextSetting(None)\n    tooltip_shows_all = Setting(True)\n\n    GRAPH_CLASS = OWScatterPlotGraph\n    graph = SettingProvider(OWScatterPlotGraph)\n    embedding_variables_names = None\n\n    xy_changed_manually = Signal(Variable, Variable)\n\n    class Warning(OWDataProjectionWidget.Warning):\n        missing_coords = Msg(\n            \"Plot cannot be displayed because '{}' or '{}' \"\n            \"is missing for all data points.\")\n\n    class Information(OWDataProjectionWidget.Information):\n        sampled_sql = Msg(\"Large SQL table; showing a sample.\")\n        missing_coords = Msg(\n            \"Points with missing '{}' or '{}' are not displayed\")\n\n    def __init__(self):\n        self.attr_box: QGroupBox = None\n        self.xy_model: DomainModel = None\n        self.cb_attr_x: ComboBoxSearch = None\n        self.cb_attr_y: ComboBoxSearch = None\n        self.vizrank: ScatterPlotVizRank = None\n        self.vizrank_button: QPushButton = None\n        self.sampling: QGroupBox = None\n        self._xy_invalidated: bool = True\n\n        self.sql_data = None  # Orange.data.sql.table.SqlTable\n        self.attribute_selection_list = None  # list of Orange.data.Variable\n        self.__timer = QTimer(self, interval=1200)\n        self.__timer.timeout.connect(self.add_data)\n        super().__init__()\n\n        # manually register Matplotlib file writers\n        self.graph_writers = self.graph_writers.copy()\n        for w in [MatplotlibFormat, MatplotlibPDFFormat]:\n            self.graph_writers.append(w)\n\n    def _add_controls(self):\n        self._add_controls_axis()\n        self._add_controls_sampling()\n        super()._add_controls()\n        self.gui.add_widget(self.gui.JitterNumericValues, self._effects_box)\n        self.gui.add_widgets(\n            [self.gui.ShowGridLines,\n             self.gui.ToolTipShowsAll,\n             self.gui.RegressionLine],\n            self._plot_box)\n        gui.checkBox(\n            self._plot_box, self,\n            value=\"graph.orthonormal_regression\",\n            label=\"Treat variables as independent\",\n            callback=self.graph.update_regression_line,\n            tooltip=\n            \"If checked, fit line to group (minimize distance from points);\\n\"\n            \"otherwise fit y as a function of x (minimize vertical distances)\",\n            disabledBy=self.cb_reg_line)\n\n    def _add_controls_axis(self):\n        common_options = dict(\n            labelWidth=50, orientation=Qt.Horizontal, sendSelectedValue=True,\n            contentsLength=12, searchable=True\n        )\n        self.attr_box = gui.vBox(self.controlArea, 'Axes',\n                                 spacing=2 if gui.is_macstyle() else 8)\n        dmod = DomainModel\n        self.xy_model = DomainModel(dmod.MIXED, valid_types=dmod.PRIMITIVE)\n        self.cb_attr_x = gui.comboBox(\n            self.attr_box, self, \"attr_x\", label=\"Axis x:\",\n            callback=self.set_attr_from_combo,\n            model=self.xy_model, **common_options,\n        )\n        self.cb_attr_y = gui.comboBox(\n            self.attr_box, self, \"attr_y\", label=\"Axis y:\",\n            callback=self.set_attr_from_combo,\n            model=self.xy_model, **common_options,\n        )\n        vizrank_box = gui.hBox(self.attr_box)\n        self.vizrank, self.vizrank_button = ScatterPlotVizRank.add_vizrank(\n            vizrank_box, self, \"Find Informative Projections\", self.set_attr)\n\n    def _add_controls_sampling(self):\n        self.sampling = gui.auto_commit(\n            self.controlArea, self, \"auto_sample\", \"Sample\", box=\"Sampling\",\n            callback=self.switch_sampling, commit=lambda: self.add_data(1))\n        self.sampling.setVisible(False)\n\n    @property\n    def effective_variables(self):\n        return [self.attr_x, self.attr_y] if self.attr_x and self.attr_y else []\n\n    @property\n    def effective_data(self):\n        eff_var = self.effective_variables\n        if eff_var and self.attr_x.name == self.attr_y.name:\n            eff_var = [self.attr_x]\n        return self.data.transform(Domain(eff_var))\n\n    def _vizrank_color_change(self):\n        self.vizrank.initialize()\n        err_msg = \"\"\n        if self.data is None:\n            err_msg = \"No data on input\"\n        elif self.data.is_sparse():\n            err_msg = \"Data is sparse\"\n        elif len(self.xy_model) < 3:\n            err_msg = \"Not enough features for ranking\"\n        elif self.attr_color is None:\n            err_msg = \"Color variable is not selected\"\n        elif np.isnan(self.data.get_column_view(\n                self.attr_color)[0].astype(float)).all():\n            err_msg = \"Color variable has no values\"\n        self.vizrank_button.setEnabled(not err_msg)\n        self.vizrank_button.setToolTip(err_msg)\n\n    @OWDataProjectionWidget.Inputs.data\n    def set_data(self, data):\n        super().set_data(data)\n        self._vizrank_color_change()\n\n        def findvar(name, iterable):\n            \"\"\"Find a Orange.data.Variable in `iterable` by name\"\"\"\n            for el in iterable:\n                if isinstance(el, Variable) and el.name == name:\n                    return el\n            return None\n\n        # handle restored settings from  < 3.3.9 when attr_* were stored\n        # by name\n        if isinstance(self.attr_x, str):\n            self.attr_x = findvar(self.attr_x, self.xy_model)\n        if isinstance(self.attr_y, str):\n            self.attr_y = findvar(self.attr_y, self.xy_model)\n        if isinstance(self.attr_label, str):\n            self.attr_label = findvar(self.attr_label, self.gui.label_model)\n        if isinstance(self.attr_color, str):\n            self.attr_color = findvar(self.attr_color, self.gui.color_model)\n        if isinstance(self.attr_shape, str):\n            self.attr_shape = findvar(self.attr_shape, self.gui.shape_model)\n        if isinstance(self.attr_size, str):\n            self.attr_size = findvar(self.attr_size, self.gui.size_model)\n\n    def check_data(self):\n        super().check_data()\n        self.__timer.stop()\n        self.sampling.setVisible(False)\n        self.sql_data = None\n        if isinstance(self.data, SqlTable):\n            if self.data.approx_len() < 4000:\n                self.data = Table(self.data)\n            else:\n                self.Information.sampled_sql()\n                self.sql_data = self.data\n                data_sample = self.data.sample_time(0.8, no_cache=True)\n                data_sample.download_data(2000, partial=True)\n                self.data = Table(data_sample)\n                self.sampling.setVisible(True)\n                if self.auto_sample:\n                    self.__timer.start()\n\n        if self.data is not None and (len(self.data) == 0 or\n                                      len(self.data.domain.variables) == 0):\n            self.data = None\n\n    def get_embedding(self):\n        self.valid_data = None\n        if self.data is None:\n            return None\n\n        x_data = self.get_column(self.attr_x, filter_valid=False)\n        y_data = self.get_column(self.attr_y, filter_valid=False)\n        if x_data is None or y_data is None:\n            return None\n\n        self.Warning.missing_coords.clear()\n        self.Information.missing_coords.clear()\n        self.valid_data = np.isfinite(x_data) & np.isfinite(y_data)\n        if self.valid_data is not None and not np.all(self.valid_data):\n            msg = self.Information if np.any(self.valid_data) else self.Warning\n            msg.missing_coords(self.attr_x.name, self.attr_y.name)\n        return np.vstack((x_data, y_data)).T\n\n    # Tooltip\n    def _point_tooltip(self, point_id, skip_attrs=()):\n        point_data = self.data[point_id]\n        xy_attrs = (self.attr_x, self.attr_y)\n        text = \"<br/>\".join(\n            escape('{} = {}'.format(var.name, point_data[var]))\n            for var in xy_attrs)\n        if self.tooltip_shows_all:\n            others = super()._point_tooltip(point_id, skip_attrs=xy_attrs)\n            if others:\n                text = \"<b>{}</b><br/><br/>{}\".format(text, others)\n        return text\n\n    def can_draw_regresssion_line(self):\n        return self.data is not None and \\\n               self.data.domain is not None and \\\n               self.attr_x.is_continuous and \\\n               self.attr_y.is_continuous\n\n    def add_data(self, time=0.4):\n        if self.data and len(self.data) > 2000:\n            self.__timer.stop()\n            return\n        data_sample = self.sql_data.sample_time(time, no_cache=True)\n        if data_sample:\n            data_sample.download_data(2000, partial=True)\n            data = Table(data_sample)\n            self.data = Table.concatenate((self.data, data), axis=0)\n            self.handleNewSignals()\n\n    def init_attr_values(self):\n        super().init_attr_values()\n        data = self.data\n        domain = data.domain if data and len(data) else None\n        self.xy_model.set_domain(domain)\n        self.attr_x = self.xy_model[0] if self.xy_model else None\n        self.attr_y = self.xy_model[1] if len(self.xy_model) >= 2 \\\n            else self.attr_x\n\n    def switch_sampling(self):\n        self.__timer.stop()\n        if self.auto_sample and self.sql_data:\n            self.add_data()\n            self.__timer.start()\n\n    @OWDataProjectionWidget.Inputs.data_subset\n    def set_subset_data(self, subset_data):\n        self.warning()\n        if isinstance(subset_data, SqlTable):\n            if subset_data.approx_len() < AUTO_DL_LIMIT:\n                subset_data = Table(subset_data)\n            else:\n                self.warning(\"Data subset does not support large Sql tables\")\n                subset_data = None\n        super().set_subset_data(subset_data)\n\n    # called when all signals are received, so the graph is updated only once\n    def handleNewSignals(self):\n        self.attr_box.setEnabled(True)\n        self.vizrank.setEnabled(True)\n        if self.attribute_selection_list and self.data is not None and \\\n                self.data.domain is not None and \\\n                all(attr in self.data.domain for attr\n                        in self.attribute_selection_list):\n            self.attr_x, self.attr_y = self.attribute_selection_list[:2]\n            self.attr_box.setEnabled(False)\n            self.vizrank.setEnabled(False)\n        self._invalidated = self._invalidated or self._xy_invalidated\n        self._xy_invalidated = False\n        super().handleNewSignals()\n        if self._domain_invalidated:\n            self.graph.update_axes()\n            self._domain_invalidated = False\n        self.cb_reg_line.setEnabled(self.can_draw_regresssion_line())\n\n    @Inputs.features\n    def set_shown_attributes(self, attributes):\n        if attributes and len(attributes) >= 2:\n            self.attribute_selection_list = attributes[:2]\n            self._xy_invalidated = self._xy_invalidated \\\n                or self.attr_x != attributes[0] \\\n                or self.attr_y != attributes[1]\n        else:\n            self.attribute_selection_list = None\n\n    def set_attr(self, attr_x, attr_y):\n        if attr_x != self.attr_x or attr_y != self.attr_y:\n            self.attr_x, self.attr_y = attr_x, attr_y\n            self.attr_changed()\n\n    def set_attr_from_combo(self):\n        self.attr_changed()\n        self.xy_changed_manually.emit(self.attr_x, self.attr_y)\n\n    def attr_changed(self):\n        self.cb_reg_line.setEnabled(self.can_draw_regresssion_line())\n        self.setup_plot()\n        self.commit.deferred()\n\n    def get_axes(self):\n        return {\"bottom\": self.attr_x, \"left\": self.attr_y}\n\n    def colors_changed(self):\n        super().colors_changed()\n        self._vizrank_color_change()\n\n    @gui.deferred\n    def commit(self):\n        super().commit()\n        self.send_features()\n\n    def send_features(self):\n        features = [attr for attr in [self.attr_x, self.attr_y] if attr]\n        self.Outputs.features.send(AttributeList(features) or None)\n\n    def get_widget_name_extension(self):\n        if self.data is not None:\n            return \"{} vs {}\".format(self.attr_x.name, self.attr_y.name)\n        return None\n\n    def _get_send_report_caption(self):\n        return report.render_items_vert((\n            (\"Color\", self._get_caption_var_name(self.attr_color)),\n            (\"Label\", self._get_caption_var_name(self.attr_label)),\n            (\"Shape\", self._get_caption_var_name(self.attr_shape)),\n            (\"Size\", self._get_caption_var_name(self.attr_size)),\n            (\"Jittering\", (self.attr_x.is_discrete or\n                           self.attr_y.is_discrete or\n                           self.graph.jitter_continuous) and\n             self.graph.jitter_size)))\n\n    @classmethod\n    def migrate_settings(cls, settings, version):\n        if version < 2 and \"selection\" in settings and settings[\"selection\"]:\n            settings[\"selection_group\"] = [(a, 1) for a in settings[\"selection\"]]\n        if version < 3:\n            if \"auto_send_selection\" in settings:\n                settings[\"auto_commit\"] = settings[\"auto_send_selection\"]\n            if \"selection_group\" in settings:\n                settings[\"selection\"] = settings[\"selection_group\"]\n        if version < 5:\n            if \"graph\" in settings and \\\n                    \"jitter_continuous\" not in settings[\"graph\"]:\n                settings[\"graph\"][\"jitter_continuous\"] = True\n\n    @classmethod\n    def migrate_context(cls, context, version):\n        values = context.values\n        if version < 3:\n            values[\"attr_color\"] = values[\"graph\"][\"attr_color\"]\n            values[\"attr_size\"] = values[\"graph\"][\"attr_size\"]\n            values[\"attr_shape\"] = values[\"graph\"][\"attr_shape\"]\n            values[\"attr_label\"] = values[\"graph\"][\"attr_label\"]\n        if version < 4:\n            if values[\"attr_x\"][1] % 100 == 1 or values[\"attr_y\"][1] % 100 == 1:\n                raise IncompatibleContext()",
                                "ast_type_str": "ClassDef",
                                "start_lineno": 314,
                                "end_lineno": 673,
                                "parent": null
                            }
                        },
                        {
                            "file_name": "Orange/widgets/visualize/owscatterplot.py",
                            "type": "class",
                            "name": "OWScatterPlot",
                            "content": "class OWScatterPlot(OWDataProjectionWidget):\n    \"\"\"Scatterplot visualization with explorative analysis and intelligent\n    data visualization enhancements.\"\"\"\n\n    name = 'Scatter Plot'\n    description = \"Interactive scatter plot visualization with \" \\\n                  \"intelligent data visualization enhancements.\"\n    icon = \"icons/ScatterPlot.svg\"\n    priority = 140\n    keywords = []\n\n    class Inputs(OWDataProjectionWidget.Inputs):\n        features = Input(\"Features\", AttributeList)\n\n    class Outputs(OWDataProjectionWidget.Outputs):\n        features = Output(\"Features\", AttributeList, dynamic=False)\n\n    settings_version = 5\n    auto_sample = Setting(True)\n    attr_x = ContextSetting(None)\n    attr_y = ContextSetting(None)\n    tooltip_shows_all = Setting(True)\n\n    GRAPH_CLASS = OWScatterPlotGraph\n    graph = SettingProvider(OWScatterPlotGraph)\n    embedding_variables_names = None\n\n    xy_changed_manually = Signal(Variable, Variable)\n\n    class Warning(OWDataProjectionWidget.Warning):\n        missing_coords = Msg(\n            \"Plot cannot be displayed because '{}' or '{}' \"\n            \"is missing for all data points.\")\n\n    class Information(OWDataProjectionWidget.Information):\n        sampled_sql = Msg(\"Large SQL table; showing a sample.\")\n        missing_coords = Msg(\n            \"Points with missing '{}' or '{}' are not displayed\")\n\n    def __init__(self):\n        self.attr_box: QGroupBox = None\n        self.xy_model: DomainModel = None\n        self.cb_attr_x: ComboBoxSearch = None\n        self.cb_attr_y: ComboBoxSearch = None\n        self.vizrank: ScatterPlotVizRank = None\n        self.vizrank_button: QPushButton = None\n        self.sampling: QGroupBox = None\n        self._xy_invalidated: bool = True\n\n        self.sql_data = None  # Orange.data.sql.table.SqlTable\n        self.attribute_selection_list = None  # list of Orange.data.Variable\n        self.__timer = QTimer(self, interval=1200)\n        self.__timer.timeout.connect(self.add_data)\n        super().__init__()\n\n        # manually register Matplotlib file writers\n        self.graph_writers = self.graph_writers.copy()\n        for w in [MatplotlibFormat, MatplotlibPDFFormat]:\n            self.graph_writers.append(w)\n\n    def _add_controls(self):\n        self._add_controls_axis()\n        self._add_controls_sampling()\n        super()._add_controls()\n        self.gui.add_widget(self.gui.JitterNumericValues, self._effects_box)\n        self.gui.add_widgets(\n            [self.gui.ShowGridLines,\n             self.gui.ToolTipShowsAll,\n             self.gui.RegressionLine],\n            self._plot_box)\n        gui.checkBox(\n            self._plot_box, self,\n            value=\"graph.orthonormal_regression\",\n            label=\"Treat variables as independent\",\n            callback=self.graph.update_regression_line,\n            tooltip=\n            \"If checked, fit line to group (minimize distance from points);\\n\"\n            \"otherwise fit y as a function of x (minimize vertical distances)\",\n            disabledBy=self.cb_reg_line)\n\n    def _add_controls_axis(self):\n        common_options = dict(\n            labelWidth=50, orientation=Qt.Horizontal, sendSelectedValue=True,\n            contentsLength=12, searchable=True\n        )\n        self.attr_box = gui.vBox(self.controlArea, 'Axes',\n                                 spacing=2 if gui.is_macstyle() else 8)\n        dmod = DomainModel\n        self.xy_model = DomainModel(dmod.MIXED, valid_types=dmod.PRIMITIVE)\n        self.cb_attr_x = gui.comboBox(\n            self.attr_box, self, \"attr_x\", label=\"Axis x:\",\n            callback=self.set_attr_from_combo,\n            model=self.xy_model, **common_options,\n        )\n        self.cb_attr_y = gui.comboBox(\n            self.attr_box, self, \"attr_y\", label=\"Axis y:\",\n            callback=self.set_attr_from_combo,\n            model=self.xy_model, **common_options,\n        )\n        vizrank_box = gui.hBox(self.attr_box)\n        self.vizrank, self.vizrank_button = ScatterPlotVizRank.add_vizrank(\n            vizrank_box, self, \"Find Informative Projections\", self.set_attr)\n\n    def _add_controls_sampling(self):\n        self.sampling = gui.auto_commit(\n            self.controlArea, self, \"auto_sample\", \"Sample\", box=\"Sampling\",\n            callback=self.switch_sampling, commit=lambda: self.add_data(1))\n        self.sampling.setVisible(False)\n\n    @property\n    def effective_variables(self):\n        return [self.attr_x, self.attr_y] if self.attr_x and self.attr_y else []\n\n    @property\n    def effective_data(self):\n        eff_var = self.effective_variables\n        if eff_var and self.attr_x.name == self.attr_y.name:\n            eff_var = [self.attr_x]\n        return self.data.transform(Domain(eff_var))\n\n    def _vizrank_color_change(self):\n        self.vizrank.initialize()\n        err_msg = \"\"\n        if self.data is None:\n            err_msg = \"No data on input\"\n        elif self.data.is_sparse():\n            err_msg = \"Data is sparse\"\n        elif len(self.xy_model) < 3:\n            err_msg = \"Not enough features for ranking\"\n        elif self.attr_color is None:\n            err_msg = \"Color variable is not selected\"\n        elif np.isnan(self.data.get_column_view(\n                self.attr_color)[0].astype(float)).all():\n            err_msg = \"Color variable has no values\"\n        self.vizrank_button.setEnabled(not err_msg)\n        self.vizrank_button.setToolTip(err_msg)\n\n    @OWDataProjectionWidget.Inputs.data\n    def set_data(self, data):\n        super().set_data(data)\n        self._vizrank_color_change()\n\n        def findvar(name, iterable):\n            \"\"\"Find a Orange.data.Variable in `iterable` by name\"\"\"\n            for el in iterable:\n                if isinstance(el, Variable) and el.name == name:\n                    return el\n            return None\n\n        # handle restored settings from  < 3.3.9 when attr_* were stored\n        # by name\n        if isinstance(self.attr_x, str):\n            self.attr_x = findvar(self.attr_x, self.xy_model)\n        if isinstance(self.attr_y, str):\n            self.attr_y = findvar(self.attr_y, self.xy_model)\n        if isinstance(self.attr_label, str):\n            self.attr_label = findvar(self.attr_label, self.gui.label_model)\n        if isinstance(self.attr_color, str):\n            self.attr_color = findvar(self.attr_color, self.gui.color_model)\n        if isinstance(self.attr_shape, str):\n            self.attr_shape = findvar(self.attr_shape, self.gui.shape_model)\n        if isinstance(self.attr_size, str):\n            self.attr_size = findvar(self.attr_size, self.gui.size_model)\n\n    def check_data(self):\n        super().check_data()\n        self.__timer.stop()\n        self.sampling.setVisible(False)\n        self.sql_data = None\n        if isinstance(self.data, SqlTable):\n            if self.data.approx_len() < 4000:\n                self.data = Table(self.data)\n            else:\n                self.Information.sampled_sql()\n                self.sql_data = self.data\n                data_sample = self.data.sample_time(0.8, no_cache=True)\n                data_sample.download_data(2000, partial=True)\n                self.data = Table(data_sample)\n                self.sampling.setVisible(True)\n                if self.auto_sample:\n                    self.__timer.start()\n\n        if self.data is not None and (len(self.data) == 0 or\n                                      len(self.data.domain.variables) == 0):\n            self.data = None\n\n    def get_embedding(self):\n        self.valid_data = None\n        if self.data is None:\n            return None\n\n        x_data = self.get_column(self.attr_x, filter_valid=False)\n        y_data = self.get_column(self.attr_y, filter_valid=False)\n        if x_data is None or y_data is None:\n            return None\n\n        self.Warning.missing_coords.clear()\n        self.Information.missing_coords.clear()\n        self.valid_data = np.isfinite(x_data) & np.isfinite(y_data)\n        if self.valid_data is not None and not np.all(self.valid_data):\n            msg = self.Information if np.any(self.valid_data) else self.Warning\n            msg.missing_coords(self.attr_x.name, self.attr_y.name)\n        return np.vstack((x_data, y_data)).T\n\n    # Tooltip\n    def _point_tooltip(self, point_id, skip_attrs=()):\n        point_data = self.data[point_id]\n        xy_attrs = (self.attr_x, self.attr_y)\n        text = \"<br/>\".join(\n            escape('{} = {}'.format(var.name, point_data[var]))\n            for var in xy_attrs)\n        if self.tooltip_shows_all:\n            others = super()._point_tooltip(point_id, skip_attrs=xy_attrs)\n            if others:\n                text = \"<b>{}</b><br/><br/>{}\".format(text, others)\n        return text\n\n    def can_draw_regresssion_line(self):\n        return self.data is not None and \\\n               self.data.domain is not None and \\\n               self.attr_x.is_continuous and \\\n               self.attr_y.is_continuous\n\n    def add_data(self, time=0.4):\n        if self.data and len(self.data) > 2000:\n            self.__timer.stop()\n            return\n        data_sample = self.sql_data.sample_time(time, no_cache=True)\n        if data_sample:\n            data_sample.download_data(2000, partial=True)\n            data = Table(data_sample)\n            self.data = Table.concatenate((self.data, data), axis=0)\n            self.handleNewSignals()\n\n    def init_attr_values(self):\n        super().init_attr_values()\n        data = self.data\n        domain = data.domain if data and len(data) else None\n        self.xy_model.set_domain(domain)\n        self.attr_x = self.xy_model[0] if self.xy_model else None\n        self.attr_y = self.xy_model[1] if len(self.xy_model) >= 2 \\\n            else self.attr_x\n\n    def switch_sampling(self):\n        self.__timer.stop()\n        if self.auto_sample and self.sql_data:\n            self.add_data()\n            self.__timer.start()\n\n    @OWDataProjectionWidget.Inputs.data_subset\n    def set_subset_data(self, subset_data):\n        self.warning()\n        if isinstance(subset_data, SqlTable):\n            if subset_data.approx_len() < AUTO_DL_LIMIT:\n                subset_data = Table(subset_data)\n            else:\n                self.warning(\"Data subset does not support large Sql tables\")\n                subset_data = None\n        super().set_subset_data(subset_data)\n\n    # called when all signals are received, so the graph is updated only once\n    def handleNewSignals(self):\n        self.attr_box.setEnabled(True)\n        self.vizrank.setEnabled(True)\n        if self.attribute_selection_list and self.data is not None and \\\n                self.data.domain is not None and \\\n                all(attr in self.data.domain for attr\n                        in self.attribute_selection_list):\n            self.attr_x, self.attr_y = self.attribute_selection_list[:2]\n            self.attr_box.setEnabled(False)\n            self.vizrank.setEnabled(False)\n        self._invalidated = self._invalidated or self._xy_invalidated\n        self._xy_invalidated = False\n        super().handleNewSignals()\n        if self._domain_invalidated:\n            self.graph.update_axes()\n            self._domain_invalidated = False\n        self.cb_reg_line.setEnabled(self.can_draw_regresssion_line())\n\n    @Inputs.features\n    def set_shown_attributes(self, attributes):\n        if attributes and len(attributes) >= 2:\n            self.attribute_selection_list = attributes[:2]\n            self._xy_invalidated = self._xy_invalidated \\\n                or self.attr_x != attributes[0] \\\n                or self.attr_y != attributes[1]\n        else:\n            self.attribute_selection_list = None\n\n    def set_attr(self, attr_x, attr_y):\n        if attr_x != self.attr_x or attr_y != self.attr_y:\n            self.attr_x, self.attr_y = attr_x, attr_y\n            self.attr_changed()\n\n    def set_attr_from_combo(self):\n        self.attr_changed()\n        self.xy_changed_manually.emit(self.attr_x, self.attr_y)\n\n    def attr_changed(self):\n        self.cb_reg_line.setEnabled(self.can_draw_regresssion_line())\n        self.setup_plot()\n        self.commit.deferred()\n\n    def get_axes(self):\n        return {\"bottom\": self.attr_x, \"left\": self.attr_y}\n\n    def colors_changed(self):\n        super().colors_changed()\n        self._vizrank_color_change()\n\n    @gui.deferred\n    def commit(self):\n        super().commit()\n        self.send_features()\n\n    def send_features(self):\n        features = [attr for attr in [self.attr_x, self.attr_y] if attr]\n        self.Outputs.features.send(AttributeList(features) or None)\n\n    def get_widget_name_extension(self):\n        if self.data is not None:\n            return \"{} vs {}\".format(self.attr_x.name, self.attr_y.name)\n        return None\n\n    def _get_send_report_caption(self):\n        return report.render_items_vert((\n            (\"Color\", self._get_caption_var_name(self.attr_color)),\n            (\"Label\", self._get_caption_var_name(self.attr_label)),\n            (\"Shape\", self._get_caption_var_name(self.attr_shape)),\n            (\"Size\", self._get_caption_var_name(self.attr_size)),\n            (\"Jittering\", (self.attr_x.is_discrete or\n                           self.attr_y.is_discrete or\n                           self.graph.jitter_continuous) and\n             self.graph.jitter_size)))\n\n    @classmethod\n    def migrate_settings(cls, settings, version):\n        if version < 2 and \"selection\" in settings and settings[\"selection\"]:\n            settings[\"selection_group\"] = [(a, 1) for a in settings[\"selection\"]]\n        if version < 3:\n            if \"auto_send_selection\" in settings:\n                settings[\"auto_commit\"] = settings[\"auto_send_selection\"]\n            if \"selection_group\" in settings:\n                settings[\"selection\"] = settings[\"selection_group\"]\n        if version < 5:\n            if \"graph\" in settings and \\\n                    \"jitter_continuous\" not in settings[\"graph\"]:\n                settings[\"graph\"][\"jitter_continuous\"] = True\n\n    @classmethod\n    def migrate_context(cls, context, version):\n        values = context.values\n        if version < 3:\n            values[\"attr_color\"] = values[\"graph\"][\"attr_color\"]\n            values[\"attr_size\"] = values[\"graph\"][\"attr_size\"]\n            values[\"attr_shape\"] = values[\"graph\"][\"attr_shape\"]\n            values[\"attr_label\"] = values[\"graph\"][\"attr_label\"]\n        if version < 4:\n            if values[\"attr_x\"][1] % 100 == 1 or values[\"attr_y\"][1] % 100 == 1:\n                raise IncompatibleContext()",
                            "ast_type_str": "ClassDef",
                            "start_lineno": 314,
                            "end_lineno": 673,
                            "parent": null
                        }
                    ],
                    "added_entities": [],
                    "deleted_entities": []
                },
                {
                    "descriptor": {
                        "old_range": {
                            "start": 581,
                            "length": 6
                        },
                        "new_range": {
                            "start": 582,
                            "length": 8
                        },
                        "section": "class OWScatterPlot(OWDataProjectionWidget):"
                    },
                    "line_group": {
                        "all_lines": [
                            {
                                "content": "            self.attr_x, self.attr_y = self.attribute_selection_list[:2]",
                                "type": "context"
                            },
                            {
                                "content": "            self.attr_box.setEnabled(False)",
                                "type": "context"
                            },
                            {
                                "content": "            self.vizrank.setEnabled(False)",
                                "type": "context"
                            },
                            {
                                "content": "        self._invalidated = self._invalidated or self._xy_invalidated",
                                "type": "added"
                            },
                            {
                                "content": "        self._xy_invalidated = False",
                                "type": "added"
                            },
                            {
                                "content": "        super().handleNewSignals()",
                                "type": "context"
                            },
                            {
                                "content": "        if self._domain_invalidated:",
                                "type": "context"
                            },
                            {
                                "content": "            self.graph.update_axes()",
                                "type": "context"
                            }
                        ]
                    },
                    "modified_entities": [
                        {
                            "file_name": "Orange/widgets/visualize/owscatterplot.py",
                            "type": "method",
                            "name": "OWScatterPlot.handleNewSignals",
                            "content": "class OWScatterPlot(OWDataProjectionWidget):\n    \"\"\"Scatterplot visualization with explorative analysis and intelligent\n    data visualization enhancements.\"\"\"\n\n    name = 'Scatter Plot'\n    description = \"Interactive scatter plot visualization with \" \\\n                  \"intelligent data visualization enhancements.\"\n    icon = \"icons/ScatterPlot.svg\"\n    priority = 140\n    keywords = []\n\n    class Inputs(OWDataProjectionWidget.Inputs):\n        features = Input(\"Features\", AttributeList)\n\n    class Outputs(OWDataProjectionWidget.Outputs):\n        features = Output(\"Features\", AttributeList, dynamic=False)\n\n    settings_version = 5\n    auto_sample = Setting(True)\n    attr_x = ContextSetting(None)\n    attr_y = ContextSetting(None)\n    tooltip_shows_all = Setting(True)\n\n    GRAPH_CLASS = OWScatterPlotGraph\n    graph = SettingProvider(OWScatterPlotGraph)\n    embedding_variables_names = None\n\n    xy_changed_manually = Signal(Variable, Variable)\n\n    class Warning(OWDataProjectionWidget.Warning):\n        missing_coords = Msg(\n            \"Plot cannot be displayed because '{}' or '{}' \"\n            \"is missing for all data points.\")\n\n    class Information(OWDataProjectionWidget.Information):\n        sampled_sql = Msg(\"Large SQL table; showing a sample.\")\n        missing_coords = Msg(\n            \"Points with missing '{}' or '{}' are not displayed\")\n\n    def __init__(self):\n        self.attr_box: QGroupBox = None\n        self.xy_model: DomainModel = None\n        self.cb_attr_x: ComboBoxSearch = None\n        self.cb_attr_y: ComboBoxSearch = None\n        self.vizrank: ScatterPlotVizRank = None\n        self.vizrank_button: QPushButton = None\n        self.sampling: QGroupBox = None\n        self._xy_invalidated: bool = True\n\n        self.sql_data = None  # Orange.data.sql.table.SqlTable\n        self.attribute_selection_list = None  # list of Orange.data.Variable\n        self.__timer = QTimer(self, interval=1200)\n        self.__timer.timeout.connect(self.add_data)\n        super().__init__()\n\n        # manually register Matplotlib file writers\n        self.graph_writers = self.graph_writers.copy()\n        for w in [MatplotlibFormat, MatplotlibPDFFormat]:\n            self.graph_writers.append(w)\n\n    def _add_controls(self):\n        self._add_controls_axis()\n        self._add_controls_sampling()\n        super()._add_controls()\n        self.gui.add_widget(self.gui.JitterNumericValues, self._effects_box)\n        self.gui.add_widgets(\n            [self.gui.ShowGridLines,\n             self.gui.ToolTipShowsAll,\n             self.gui.RegressionLine],\n            self._plot_box)\n        gui.checkBox(\n            self._plot_box, self,\n            value=\"graph.orthonormal_regression\",\n            label=\"Treat variables as independent\",\n            callback=self.graph.update_regression_line,\n            tooltip=\n            \"If checked, fit line to group (minimize distance from points);\\n\"\n            \"otherwise fit y as a function of x (minimize vertical distances)\",\n            disabledBy=self.cb_reg_line)\n\n    def _add_controls_axis(self):\n        common_options = dict(\n            labelWidth=50, orientation=Qt.Horizontal, sendSelectedValue=True,\n            contentsLength=12, searchable=True\n        )\n        self.attr_box = gui.vBox(self.controlArea, 'Axes',\n                                 spacing=2 if gui.is_macstyle() else 8)\n        dmod = DomainModel\n        self.xy_model = DomainModel(dmod.MIXED, valid_types=dmod.PRIMITIVE)\n        self.cb_attr_x = gui.comboBox(\n            self.attr_box, self, \"attr_x\", label=\"Axis x:\",\n            callback=self.set_attr_from_combo,\n            model=self.xy_model, **common_options,\n        )\n        self.cb_attr_y = gui.comboBox(\n            self.attr_box, self, \"attr_y\", label=\"Axis y:\",\n            callback=self.set_attr_from_combo,\n            model=self.xy_model, **common_options,\n        )\n        vizrank_box = gui.hBox(self.attr_box)\n        self.vizrank, self.vizrank_button = ScatterPlotVizRank.add_vizrank(\n            vizrank_box, self, \"Find Informative Projections\", self.set_attr)\n\n    def _add_controls_sampling(self):\n        self.sampling = gui.auto_commit(\n            self.controlArea, self, \"auto_sample\", \"Sample\", box=\"Sampling\",\n            callback=self.switch_sampling, commit=lambda: self.add_data(1))\n        self.sampling.setVisible(False)\n\n    @property\n    def effective_variables(self):\n        return [self.attr_x, self.attr_y] if self.attr_x and self.attr_y else []\n\n    @property\n    def effective_data(self):\n        eff_var = self.effective_variables\n        if eff_var and self.attr_x.name == self.attr_y.name:\n            eff_var = [self.attr_x]\n        return self.data.transform(Domain(eff_var))\n\n    def _vizrank_color_change(self):\n        self.vizrank.initialize()\n        err_msg = \"\"\n        if self.data is None:\n            err_msg = \"No data on input\"\n        elif self.data.is_sparse():\n            err_msg = \"Data is sparse\"\n        elif len(self.xy_model) < 3:\n            err_msg = \"Not enough features for ranking\"\n        elif self.attr_color is None:\n            err_msg = \"Color variable is not selected\"\n        elif np.isnan(self.data.get_column_view(\n                self.attr_color)[0].astype(float)).all():\n            err_msg = \"Color variable has no values\"\n        self.vizrank_button.setEnabled(not err_msg)\n        self.vizrank_button.setToolTip(err_msg)\n\n    @OWDataProjectionWidget.Inputs.data\n    def set_data(self, data):\n        super().set_data(data)\n        self._vizrank_color_change()\n\n        def findvar(name, iterable):\n            \"\"\"Find a Orange.data.Variable in `iterable` by name\"\"\"\n            for el in iterable:\n                if isinstance(el, Variable) and el.name == name:\n                    return el\n            return None\n\n        # handle restored settings from  < 3.3.9 when attr_* were stored\n        # by name\n        if isinstance(self.attr_x, str):\n            self.attr_x = findvar(self.attr_x, self.xy_model)\n        if isinstance(self.attr_y, str):\n            self.attr_y = findvar(self.attr_y, self.xy_model)\n        if isinstance(self.attr_label, str):\n            self.attr_label = findvar(self.attr_label, self.gui.label_model)\n        if isinstance(self.attr_color, str):\n            self.attr_color = findvar(self.attr_color, self.gui.color_model)\n        if isinstance(self.attr_shape, str):\n            self.attr_shape = findvar(self.attr_shape, self.gui.shape_model)\n        if isinstance(self.attr_size, str):\n            self.attr_size = findvar(self.attr_size, self.gui.size_model)\n\n    def check_data(self):\n        super().check_data()\n        self.__timer.stop()\n        self.sampling.setVisible(False)\n        self.sql_data = None\n        if isinstance(self.data, SqlTable):\n            if self.data.approx_len() < 4000:\n                self.data = Table(self.data)\n            else:\n                self.Information.sampled_sql()\n                self.sql_data = self.data\n                data_sample = self.data.sample_time(0.8, no_cache=True)\n                data_sample.download_data(2000, partial=True)\n                self.data = Table(data_sample)\n                self.sampling.setVisible(True)\n                if self.auto_sample:\n                    self.__timer.start()\n\n        if self.data is not None and (len(self.data) == 0 or\n                                      len(self.data.domain.variables) == 0):\n            self.data = None\n\n    def get_embedding(self):\n        self.valid_data = None\n        if self.data is None:\n            return None\n\n        x_data = self.get_column(self.attr_x, filter_valid=False)\n        y_data = self.get_column(self.attr_y, filter_valid=False)\n        if x_data is None or y_data is None:\n            return None\n\n        self.Warning.missing_coords.clear()\n        self.Information.missing_coords.clear()\n        self.valid_data = np.isfinite(x_data) & np.isfinite(y_data)\n        if self.valid_data is not None and not np.all(self.valid_data):\n            msg = self.Information if np.any(self.valid_data) else self.Warning\n            msg.missing_coords(self.attr_x.name, self.attr_y.name)\n        return np.vstack((x_data, y_data)).T\n\n    # Tooltip\n    def _point_tooltip(self, point_id, skip_attrs=()):\n        point_data = self.data[point_id]\n        xy_attrs = (self.attr_x, self.attr_y)\n        text = \"<br/>\".join(\n            escape('{} = {}'.format(var.name, point_data[var]))\n            for var in xy_attrs)\n        if self.tooltip_shows_all:\n            others = super()._point_tooltip(point_id, skip_attrs=xy_attrs)\n            if others:\n                text = \"<b>{}</b><br/><br/>{}\".format(text, others)\n        return text\n\n    def can_draw_regresssion_line(self):\n        return self.data is not None and \\\n               self.data.domain is not None and \\\n               self.attr_x.is_continuous and \\\n               self.attr_y.is_continuous\n\n    def add_data(self, time=0.4):\n        if self.data and len(self.data) > 2000:\n            self.__timer.stop()\n            return\n        data_sample = self.sql_data.sample_time(time, no_cache=True)\n        if data_sample:\n            data_sample.download_data(2000, partial=True)\n            data = Table(data_sample)\n            self.data = Table.concatenate((self.data, data), axis=0)\n            self.handleNewSignals()\n\n    def init_attr_values(self):\n        super().init_attr_values()\n        data = self.data\n        domain = data.domain if data and len(data) else None\n        self.xy_model.set_domain(domain)\n        self.attr_x = self.xy_model[0] if self.xy_model else None\n        self.attr_y = self.xy_model[1] if len(self.xy_model) >= 2 \\\n            else self.attr_x\n\n    def switch_sampling(self):\n        self.__timer.stop()\n        if self.auto_sample and self.sql_data:\n            self.add_data()\n            self.__timer.start()\n\n    @OWDataProjectionWidget.Inputs.data_subset\n    def set_subset_data(self, subset_data):\n        self.warning()\n        if isinstance(subset_data, SqlTable):\n            if subset_data.approx_len() < AUTO_DL_LIMIT:\n                subset_data = Table(subset_data)\n            else:\n                self.warning(\"Data subset does not support large Sql tables\")\n                subset_data = None\n        super().set_subset_data(subset_data)\n\n    # called when all signals are received, so the graph is updated only once\n    def handleNewSignals(self):\n        self.attr_box.setEnabled(True)\n        self.vizrank.setEnabled(True)\n        if self.attribute_selection_list and self.data is not None and \\\n                self.data.domain is not None and \\\n                all(attr in self.data.domain for attr\n                        in self.attribute_selection_list):\n            self.attr_x, self.attr_y = self.attribute_selection_list[:2]\n            self.attr_box.setEnabled(False)\n            self.vizrank.setEnabled(False)\n        self._invalidated = self._invalidated or self._xy_invalidated\n        self._xy_invalidated = False\n        super().handleNewSignals()\n        if self._domain_invalidated:\n            self.graph.update_axes()\n            self._domain_invalidated = False\n        self.cb_reg_line.setEnabled(self.can_draw_regresssion_line())\n\n    @Inputs.features\n    def set_shown_attributes(self, attributes):\n        if attributes and len(attributes) >= 2:\n            self.attribute_selection_list = attributes[:2]\n            self._xy_invalidated = self._xy_invalidated \\\n                or self.attr_x != attributes[0] \\\n                or self.attr_y != attributes[1]\n        else:\n            self.attribute_selection_list = None\n\n    def set_attr(self, attr_x, attr_y):\n        if attr_x != self.attr_x or attr_y != self.attr_y:\n            self.attr_x, self.attr_y = attr_x, attr_y\n            self.attr_changed()\n\n    def set_attr_from_combo(self):\n        self.attr_changed()\n        self.xy_changed_manually.emit(self.attr_x, self.attr_y)\n\n    def attr_changed(self):\n        self.cb_reg_line.setEnabled(self.can_draw_regresssion_line())\n        self.setup_plot()\n        self.commit.deferred()\n\n    def get_axes(self):\n        return {\"bottom\": self.attr_x, \"left\": self.attr_y}\n\n    def colors_changed(self):\n        super().colors_changed()\n        self._vizrank_color_change()\n\n    @gui.deferred\n    def commit(self):\n        super().commit()\n        self.send_features()\n\n    def send_features(self):\n        features = [attr for attr in [self.attr_x, self.attr_y] if attr]\n        self.Outputs.features.send(AttributeList(features) or None)\n\n    def get_widget_name_extension(self):\n        if self.data is not None:\n            return \"{} vs {}\".format(self.attr_x.name, self.attr_y.name)\n        return None\n\n    def _get_send_report_caption(self):\n        return report.render_items_vert((\n            (\"Color\", self._get_caption_var_name(self.attr_color)),\n            (\"Label\", self._get_caption_var_name(self.attr_label)),\n            (\"Shape\", self._get_caption_var_name(self.attr_shape)),\n            (\"Size\", self._get_caption_var_name(self.attr_size)),\n            (\"Jittering\", (self.attr_x.is_discrete or\n                           self.attr_y.is_discrete or\n                           self.graph.jitter_continuous) and\n             self.graph.jitter_size)))\n\n    @classmethod\n    def migrate_settings(cls, settings, version):\n        if version < 2 and \"selection\" in settings and settings[\"selection\"]:\n            settings[\"selection_group\"] = [(a, 1) for a in settings[\"selection\"]]\n        if version < 3:\n            if \"auto_send_selection\" in settings:\n                settings[\"auto_commit\"] = settings[\"auto_send_selection\"]\n            if \"selection_group\" in settings:\n                settings[\"selection\"] = settings[\"selection_group\"]\n        if version < 5:\n            if \"graph\" in settings and \\\n                    \"jitter_continuous\" not in settings[\"graph\"]:\n                settings[\"graph\"][\"jitter_continuous\"] = True\n\n    @classmethod\n    def migrate_context(cls, context, version):\n        values = context.values\n        if version < 3:\n            values[\"attr_color\"] = values[\"graph\"][\"attr_color\"]\n            values[\"attr_size\"] = values[\"graph\"][\"attr_size\"]\n            values[\"attr_shape\"] = values[\"graph\"][\"attr_shape\"]\n            values[\"attr_label\"] = values[\"graph\"][\"attr_label\"]\n        if version < 4:\n            if values[\"attr_x\"][1] % 100 == 1 or values[\"attr_y\"][1] % 100 == 1:\n                raise IncompatibleContext()",
                            "ast_type_str": "ClassDef",
                            "start_lineno": 575,
                            "end_lineno": 591,
                            "parent": {
                                "file_name": "Orange/widgets/visualize/owscatterplot.py",
                                "type": "class",
                                "name": "OWScatterPlot",
                                "content": "class OWScatterPlot(OWDataProjectionWidget):\n    \"\"\"Scatterplot visualization with explorative analysis and intelligent\n    data visualization enhancements.\"\"\"\n\n    name = 'Scatter Plot'\n    description = \"Interactive scatter plot visualization with \" \\\n                  \"intelligent data visualization enhancements.\"\n    icon = \"icons/ScatterPlot.svg\"\n    priority = 140\n    keywords = []\n\n    class Inputs(OWDataProjectionWidget.Inputs):\n        features = Input(\"Features\", AttributeList)\n\n    class Outputs(OWDataProjectionWidget.Outputs):\n        features = Output(\"Features\", AttributeList, dynamic=False)\n\n    settings_version = 5\n    auto_sample = Setting(True)\n    attr_x = ContextSetting(None)\n    attr_y = ContextSetting(None)\n    tooltip_shows_all = Setting(True)\n\n    GRAPH_CLASS = OWScatterPlotGraph\n    graph = SettingProvider(OWScatterPlotGraph)\n    embedding_variables_names = None\n\n    xy_changed_manually = Signal(Variable, Variable)\n\n    class Warning(OWDataProjectionWidget.Warning):\n        missing_coords = Msg(\n            \"Plot cannot be displayed because '{}' or '{}' \"\n            \"is missing for all data points.\")\n\n    class Information(OWDataProjectionWidget.Information):\n        sampled_sql = Msg(\"Large SQL table; showing a sample.\")\n        missing_coords = Msg(\n            \"Points with missing '{}' or '{}' are not displayed\")\n\n    def __init__(self):\n        self.attr_box: QGroupBox = None\n        self.xy_model: DomainModel = None\n        self.cb_attr_x: ComboBoxSearch = None\n        self.cb_attr_y: ComboBoxSearch = None\n        self.vizrank: ScatterPlotVizRank = None\n        self.vizrank_button: QPushButton = None\n        self.sampling: QGroupBox = None\n        self._xy_invalidated: bool = True\n\n        self.sql_data = None  # Orange.data.sql.table.SqlTable\n        self.attribute_selection_list = None  # list of Orange.data.Variable\n        self.__timer = QTimer(self, interval=1200)\n        self.__timer.timeout.connect(self.add_data)\n        super().__init__()\n\n        # manually register Matplotlib file writers\n        self.graph_writers = self.graph_writers.copy()\n        for w in [MatplotlibFormat, MatplotlibPDFFormat]:\n            self.graph_writers.append(w)\n\n    def _add_controls(self):\n        self._add_controls_axis()\n        self._add_controls_sampling()\n        super()._add_controls()\n        self.gui.add_widget(self.gui.JitterNumericValues, self._effects_box)\n        self.gui.add_widgets(\n            [self.gui.ShowGridLines,\n             self.gui.ToolTipShowsAll,\n             self.gui.RegressionLine],\n            self._plot_box)\n        gui.checkBox(\n            self._plot_box, self,\n            value=\"graph.orthonormal_regression\",\n            label=\"Treat variables as independent\",\n            callback=self.graph.update_regression_line,\n            tooltip=\n            \"If checked, fit line to group (minimize distance from points);\\n\"\n            \"otherwise fit y as a function of x (minimize vertical distances)\",\n            disabledBy=self.cb_reg_line)\n\n    def _add_controls_axis(self):\n        common_options = dict(\n            labelWidth=50, orientation=Qt.Horizontal, sendSelectedValue=True,\n            contentsLength=12, searchable=True\n        )\n        self.attr_box = gui.vBox(self.controlArea, 'Axes',\n                                 spacing=2 if gui.is_macstyle() else 8)\n        dmod = DomainModel\n        self.xy_model = DomainModel(dmod.MIXED, valid_types=dmod.PRIMITIVE)\n        self.cb_attr_x = gui.comboBox(\n            self.attr_box, self, \"attr_x\", label=\"Axis x:\",\n            callback=self.set_attr_from_combo,\n            model=self.xy_model, **common_options,\n        )\n        self.cb_attr_y = gui.comboBox(\n            self.attr_box, self, \"attr_y\", label=\"Axis y:\",\n            callback=self.set_attr_from_combo,\n            model=self.xy_model, **common_options,\n        )\n        vizrank_box = gui.hBox(self.attr_box)\n        self.vizrank, self.vizrank_button = ScatterPlotVizRank.add_vizrank(\n            vizrank_box, self, \"Find Informative Projections\", self.set_attr)\n\n    def _add_controls_sampling(self):\n        self.sampling = gui.auto_commit(\n            self.controlArea, self, \"auto_sample\", \"Sample\", box=\"Sampling\",\n            callback=self.switch_sampling, commit=lambda: self.add_data(1))\n        self.sampling.setVisible(False)\n\n    @property\n    def effective_variables(self):\n        return [self.attr_x, self.attr_y] if self.attr_x and self.attr_y else []\n\n    @property\n    def effective_data(self):\n        eff_var = self.effective_variables\n        if eff_var and self.attr_x.name == self.attr_y.name:\n            eff_var = [self.attr_x]\n        return self.data.transform(Domain(eff_var))\n\n    def _vizrank_color_change(self):\n        self.vizrank.initialize()\n        err_msg = \"\"\n        if self.data is None:\n            err_msg = \"No data on input\"\n        elif self.data.is_sparse():\n            err_msg = \"Data is sparse\"\n        elif len(self.xy_model) < 3:\n            err_msg = \"Not enough features for ranking\"\n        elif self.attr_color is None:\n            err_msg = \"Color variable is not selected\"\n        elif np.isnan(self.data.get_column_view(\n                self.attr_color)[0].astype(float)).all():\n            err_msg = \"Color variable has no values\"\n        self.vizrank_button.setEnabled(not err_msg)\n        self.vizrank_button.setToolTip(err_msg)\n\n    @OWDataProjectionWidget.Inputs.data\n    def set_data(self, data):\n        super().set_data(data)\n        self._vizrank_color_change()\n\n        def findvar(name, iterable):\n            \"\"\"Find a Orange.data.Variable in `iterable` by name\"\"\"\n            for el in iterable:\n                if isinstance(el, Variable) and el.name == name:\n                    return el\n            return None\n\n        # handle restored settings from  < 3.3.9 when attr_* were stored\n        # by name\n        if isinstance(self.attr_x, str):\n            self.attr_x = findvar(self.attr_x, self.xy_model)\n        if isinstance(self.attr_y, str):\n            self.attr_y = findvar(self.attr_y, self.xy_model)\n        if isinstance(self.attr_label, str):\n            self.attr_label = findvar(self.attr_label, self.gui.label_model)\n        if isinstance(self.attr_color, str):\n            self.attr_color = findvar(self.attr_color, self.gui.color_model)\n        if isinstance(self.attr_shape, str):\n            self.attr_shape = findvar(self.attr_shape, self.gui.shape_model)\n        if isinstance(self.attr_size, str):\n            self.attr_size = findvar(self.attr_size, self.gui.size_model)\n\n    def check_data(self):\n        super().check_data()\n        self.__timer.stop()\n        self.sampling.setVisible(False)\n        self.sql_data = None\n        if isinstance(self.data, SqlTable):\n            if self.data.approx_len() < 4000:\n                self.data = Table(self.data)\n            else:\n                self.Information.sampled_sql()\n                self.sql_data = self.data\n                data_sample = self.data.sample_time(0.8, no_cache=True)\n                data_sample.download_data(2000, partial=True)\n                self.data = Table(data_sample)\n                self.sampling.setVisible(True)\n                if self.auto_sample:\n                    self.__timer.start()\n\n        if self.data is not None and (len(self.data) == 0 or\n                                      len(self.data.domain.variables) == 0):\n            self.data = None\n\n    def get_embedding(self):\n        self.valid_data = None\n        if self.data is None:\n            return None\n\n        x_data = self.get_column(self.attr_x, filter_valid=False)\n        y_data = self.get_column(self.attr_y, filter_valid=False)\n        if x_data is None or y_data is None:\n            return None\n\n        self.Warning.missing_coords.clear()\n        self.Information.missing_coords.clear()\n        self.valid_data = np.isfinite(x_data) & np.isfinite(y_data)\n        if self.valid_data is not None and not np.all(self.valid_data):\n            msg = self.Information if np.any(self.valid_data) else self.Warning\n            msg.missing_coords(self.attr_x.name, self.attr_y.name)\n        return np.vstack((x_data, y_data)).T\n\n    # Tooltip\n    def _point_tooltip(self, point_id, skip_attrs=()):\n        point_data = self.data[point_id]\n        xy_attrs = (self.attr_x, self.attr_y)\n        text = \"<br/>\".join(\n            escape('{} = {}'.format(var.name, point_data[var]))\n            for var in xy_attrs)\n        if self.tooltip_shows_all:\n            others = super()._point_tooltip(point_id, skip_attrs=xy_attrs)\n            if others:\n                text = \"<b>{}</b><br/><br/>{}\".format(text, others)\n        return text\n\n    def can_draw_regresssion_line(self):\n        return self.data is not None and \\\n               self.data.domain is not None and \\\n               self.attr_x.is_continuous and \\\n               self.attr_y.is_continuous\n\n    def add_data(self, time=0.4):\n        if self.data and len(self.data) > 2000:\n            self.__timer.stop()\n            return\n        data_sample = self.sql_data.sample_time(time, no_cache=True)\n        if data_sample:\n            data_sample.download_data(2000, partial=True)\n            data = Table(data_sample)\n            self.data = Table.concatenate((self.data, data), axis=0)\n            self.handleNewSignals()\n\n    def init_attr_values(self):\n        super().init_attr_values()\n        data = self.data\n        domain = data.domain if data and len(data) else None\n        self.xy_model.set_domain(domain)\n        self.attr_x = self.xy_model[0] if self.xy_model else None\n        self.attr_y = self.xy_model[1] if len(self.xy_model) >= 2 \\\n            else self.attr_x\n\n    def switch_sampling(self):\n        self.__timer.stop()\n        if self.auto_sample and self.sql_data:\n            self.add_data()\n            self.__timer.start()\n\n    @OWDataProjectionWidget.Inputs.data_subset\n    def set_subset_data(self, subset_data):\n        self.warning()\n        if isinstance(subset_data, SqlTable):\n            if subset_data.approx_len() < AUTO_DL_LIMIT:\n                subset_data = Table(subset_data)\n            else:\n                self.warning(\"Data subset does not support large Sql tables\")\n                subset_data = None\n        super().set_subset_data(subset_data)\n\n    # called when all signals are received, so the graph is updated only once\n    def handleNewSignals(self):\n        self.attr_box.setEnabled(True)\n        self.vizrank.setEnabled(True)\n        if self.attribute_selection_list and self.data is not None and \\\n                self.data.domain is not None and \\\n                all(attr in self.data.domain for attr\n                        in self.attribute_selection_list):\n            self.attr_x, self.attr_y = self.attribute_selection_list[:2]\n            self.attr_box.setEnabled(False)\n            self.vizrank.setEnabled(False)\n        self._invalidated = self._invalidated or self._xy_invalidated\n        self._xy_invalidated = False\n        super().handleNewSignals()\n        if self._domain_invalidated:\n            self.graph.update_axes()\n            self._domain_invalidated = False\n        self.cb_reg_line.setEnabled(self.can_draw_regresssion_line())\n\n    @Inputs.features\n    def set_shown_attributes(self, attributes):\n        if attributes and len(attributes) >= 2:\n            self.attribute_selection_list = attributes[:2]\n            self._xy_invalidated = self._xy_invalidated \\\n                or self.attr_x != attributes[0] \\\n                or self.attr_y != attributes[1]\n        else:\n            self.attribute_selection_list = None\n\n    def set_attr(self, attr_x, attr_y):\n        if attr_x != self.attr_x or attr_y != self.attr_y:\n            self.attr_x, self.attr_y = attr_x, attr_y\n            self.attr_changed()\n\n    def set_attr_from_combo(self):\n        self.attr_changed()\n        self.xy_changed_manually.emit(self.attr_x, self.attr_y)\n\n    def attr_changed(self):\n        self.cb_reg_line.setEnabled(self.can_draw_regresssion_line())\n        self.setup_plot()\n        self.commit.deferred()\n\n    def get_axes(self):\n        return {\"bottom\": self.attr_x, \"left\": self.attr_y}\n\n    def colors_changed(self):\n        super().colors_changed()\n        self._vizrank_color_change()\n\n    @gui.deferred\n    def commit(self):\n        super().commit()\n        self.send_features()\n\n    def send_features(self):\n        features = [attr for attr in [self.attr_x, self.attr_y] if attr]\n        self.Outputs.features.send(AttributeList(features) or None)\n\n    def get_widget_name_extension(self):\n        if self.data is not None:\n            return \"{} vs {}\".format(self.attr_x.name, self.attr_y.name)\n        return None\n\n    def _get_send_report_caption(self):\n        return report.render_items_vert((\n            (\"Color\", self._get_caption_var_name(self.attr_color)),\n            (\"Label\", self._get_caption_var_name(self.attr_label)),\n            (\"Shape\", self._get_caption_var_name(self.attr_shape)),\n            (\"Size\", self._get_caption_var_name(self.attr_size)),\n            (\"Jittering\", (self.attr_x.is_discrete or\n                           self.attr_y.is_discrete or\n                           self.graph.jitter_continuous) and\n             self.graph.jitter_size)))\n\n    @classmethod\n    def migrate_settings(cls, settings, version):\n        if version < 2 and \"selection\" in settings and settings[\"selection\"]:\n            settings[\"selection_group\"] = [(a, 1) for a in settings[\"selection\"]]\n        if version < 3:\n            if \"auto_send_selection\" in settings:\n                settings[\"auto_commit\"] = settings[\"auto_send_selection\"]\n            if \"selection_group\" in settings:\n                settings[\"selection\"] = settings[\"selection_group\"]\n        if version < 5:\n            if \"graph\" in settings and \\\n                    \"jitter_continuous\" not in settings[\"graph\"]:\n                settings[\"graph\"][\"jitter_continuous\"] = True\n\n    @classmethod\n    def migrate_context(cls, context, version):\n        values = context.values\n        if version < 3:\n            values[\"attr_color\"] = values[\"graph\"][\"attr_color\"]\n            values[\"attr_size\"] = values[\"graph\"][\"attr_size\"]\n            values[\"attr_shape\"] = values[\"graph\"][\"attr_shape\"]\n            values[\"attr_label\"] = values[\"graph\"][\"attr_label\"]\n        if version < 4:\n            if values[\"attr_x\"][1] % 100 == 1 or values[\"attr_y\"][1] % 100 == 1:\n                raise IncompatibleContext()",
                                "ast_type_str": "ClassDef",
                                "start_lineno": 314,
                                "end_lineno": 673,
                                "parent": null
                            }
                        },
                        {
                            "file_name": "Orange/widgets/visualize/owscatterplot.py",
                            "type": "class",
                            "name": "OWScatterPlot",
                            "content": "class OWScatterPlot(OWDataProjectionWidget):\n    \"\"\"Scatterplot visualization with explorative analysis and intelligent\n    data visualization enhancements.\"\"\"\n\n    name = 'Scatter Plot'\n    description = \"Interactive scatter plot visualization with \" \\\n                  \"intelligent data visualization enhancements.\"\n    icon = \"icons/ScatterPlot.svg\"\n    priority = 140\n    keywords = []\n\n    class Inputs(OWDataProjectionWidget.Inputs):\n        features = Input(\"Features\", AttributeList)\n\n    class Outputs(OWDataProjectionWidget.Outputs):\n        features = Output(\"Features\", AttributeList, dynamic=False)\n\n    settings_version = 5\n    auto_sample = Setting(True)\n    attr_x = ContextSetting(None)\n    attr_y = ContextSetting(None)\n    tooltip_shows_all = Setting(True)\n\n    GRAPH_CLASS = OWScatterPlotGraph\n    graph = SettingProvider(OWScatterPlotGraph)\n    embedding_variables_names = None\n\n    xy_changed_manually = Signal(Variable, Variable)\n\n    class Warning(OWDataProjectionWidget.Warning):\n        missing_coords = Msg(\n            \"Plot cannot be displayed because '{}' or '{}' \"\n            \"is missing for all data points.\")\n\n    class Information(OWDataProjectionWidget.Information):\n        sampled_sql = Msg(\"Large SQL table; showing a sample.\")\n        missing_coords = Msg(\n            \"Points with missing '{}' or '{}' are not displayed\")\n\n    def __init__(self):\n        self.attr_box: QGroupBox = None\n        self.xy_model: DomainModel = None\n        self.cb_attr_x: ComboBoxSearch = None\n        self.cb_attr_y: ComboBoxSearch = None\n        self.vizrank: ScatterPlotVizRank = None\n        self.vizrank_button: QPushButton = None\n        self.sampling: QGroupBox = None\n        self._xy_invalidated: bool = True\n\n        self.sql_data = None  # Orange.data.sql.table.SqlTable\n        self.attribute_selection_list = None  # list of Orange.data.Variable\n        self.__timer = QTimer(self, interval=1200)\n        self.__timer.timeout.connect(self.add_data)\n        super().__init__()\n\n        # manually register Matplotlib file writers\n        self.graph_writers = self.graph_writers.copy()\n        for w in [MatplotlibFormat, MatplotlibPDFFormat]:\n            self.graph_writers.append(w)\n\n    def _add_controls(self):\n        self._add_controls_axis()\n        self._add_controls_sampling()\n        super()._add_controls()\n        self.gui.add_widget(self.gui.JitterNumericValues, self._effects_box)\n        self.gui.add_widgets(\n            [self.gui.ShowGridLines,\n             self.gui.ToolTipShowsAll,\n             self.gui.RegressionLine],\n            self._plot_box)\n        gui.checkBox(\n            self._plot_box, self,\n            value=\"graph.orthonormal_regression\",\n            label=\"Treat variables as independent\",\n            callback=self.graph.update_regression_line,\n            tooltip=\n            \"If checked, fit line to group (minimize distance from points);\\n\"\n            \"otherwise fit y as a function of x (minimize vertical distances)\",\n            disabledBy=self.cb_reg_line)\n\n    def _add_controls_axis(self):\n        common_options = dict(\n            labelWidth=50, orientation=Qt.Horizontal, sendSelectedValue=True,\n            contentsLength=12, searchable=True\n        )\n        self.attr_box = gui.vBox(self.controlArea, 'Axes',\n                                 spacing=2 if gui.is_macstyle() else 8)\n        dmod = DomainModel\n        self.xy_model = DomainModel(dmod.MIXED, valid_types=dmod.PRIMITIVE)\n        self.cb_attr_x = gui.comboBox(\n            self.attr_box, self, \"attr_x\", label=\"Axis x:\",\n            callback=self.set_attr_from_combo,\n            model=self.xy_model, **common_options,\n        )\n        self.cb_attr_y = gui.comboBox(\n            self.attr_box, self, \"attr_y\", label=\"Axis y:\",\n            callback=self.set_attr_from_combo,\n            model=self.xy_model, **common_options,\n        )\n        vizrank_box = gui.hBox(self.attr_box)\n        self.vizrank, self.vizrank_button = ScatterPlotVizRank.add_vizrank(\n            vizrank_box, self, \"Find Informative Projections\", self.set_attr)\n\n    def _add_controls_sampling(self):\n        self.sampling = gui.auto_commit(\n            self.controlArea, self, \"auto_sample\", \"Sample\", box=\"Sampling\",\n            callback=self.switch_sampling, commit=lambda: self.add_data(1))\n        self.sampling.setVisible(False)\n\n    @property\n    def effective_variables(self):\n        return [self.attr_x, self.attr_y] if self.attr_x and self.attr_y else []\n\n    @property\n    def effective_data(self):\n        eff_var = self.effective_variables\n        if eff_var and self.attr_x.name == self.attr_y.name:\n            eff_var = [self.attr_x]\n        return self.data.transform(Domain(eff_var))\n\n    def _vizrank_color_change(self):\n        self.vizrank.initialize()\n        err_msg = \"\"\n        if self.data is None:\n            err_msg = \"No data on input\"\n        elif self.data.is_sparse():\n            err_msg = \"Data is sparse\"\n        elif len(self.xy_model) < 3:\n            err_msg = \"Not enough features for ranking\"\n        elif self.attr_color is None:\n            err_msg = \"Color variable is not selected\"\n        elif np.isnan(self.data.get_column_view(\n                self.attr_color)[0].astype(float)).all():\n            err_msg = \"Color variable has no values\"\n        self.vizrank_button.setEnabled(not err_msg)\n        self.vizrank_button.setToolTip(err_msg)\n\n    @OWDataProjectionWidget.Inputs.data\n    def set_data(self, data):\n        super().set_data(data)\n        self._vizrank_color_change()\n\n        def findvar(name, iterable):\n            \"\"\"Find a Orange.data.Variable in `iterable` by name\"\"\"\n            for el in iterable:\n                if isinstance(el, Variable) and el.name == name:\n                    return el\n            return None\n\n        # handle restored settings from  < 3.3.9 when attr_* were stored\n        # by name\n        if isinstance(self.attr_x, str):\n            self.attr_x = findvar(self.attr_x, self.xy_model)\n        if isinstance(self.attr_y, str):\n            self.attr_y = findvar(self.attr_y, self.xy_model)\n        if isinstance(self.attr_label, str):\n            self.attr_label = findvar(self.attr_label, self.gui.label_model)\n        if isinstance(self.attr_color, str):\n            self.attr_color = findvar(self.attr_color, self.gui.color_model)\n        if isinstance(self.attr_shape, str):\n            self.attr_shape = findvar(self.attr_shape, self.gui.shape_model)\n        if isinstance(self.attr_size, str):\n            self.attr_size = findvar(self.attr_size, self.gui.size_model)\n\n    def check_data(self):\n        super().check_data()\n        self.__timer.stop()\n        self.sampling.setVisible(False)\n        self.sql_data = None\n        if isinstance(self.data, SqlTable):\n            if self.data.approx_len() < 4000:\n                self.data = Table(self.data)\n            else:\n                self.Information.sampled_sql()\n                self.sql_data = self.data\n                data_sample = self.data.sample_time(0.8, no_cache=True)\n                data_sample.download_data(2000, partial=True)\n                self.data = Table(data_sample)\n                self.sampling.setVisible(True)\n                if self.auto_sample:\n                    self.__timer.start()\n\n        if self.data is not None and (len(self.data) == 0 or\n                                      len(self.data.domain.variables) == 0):\n            self.data = None\n\n    def get_embedding(self):\n        self.valid_data = None\n        if self.data is None:\n            return None\n\n        x_data = self.get_column(self.attr_x, filter_valid=False)\n        y_data = self.get_column(self.attr_y, filter_valid=False)\n        if x_data is None or y_data is None:\n            return None\n\n        self.Warning.missing_coords.clear()\n        self.Information.missing_coords.clear()\n        self.valid_data = np.isfinite(x_data) & np.isfinite(y_data)\n        if self.valid_data is not None and not np.all(self.valid_data):\n            msg = self.Information if np.any(self.valid_data) else self.Warning\n            msg.missing_coords(self.attr_x.name, self.attr_y.name)\n        return np.vstack((x_data, y_data)).T\n\n    # Tooltip\n    def _point_tooltip(self, point_id, skip_attrs=()):\n        point_data = self.data[point_id]\n        xy_attrs = (self.attr_x, self.attr_y)\n        text = \"<br/>\".join(\n            escape('{} = {}'.format(var.name, point_data[var]))\n            for var in xy_attrs)\n        if self.tooltip_shows_all:\n            others = super()._point_tooltip(point_id, skip_attrs=xy_attrs)\n            if others:\n                text = \"<b>{}</b><br/><br/>{}\".format(text, others)\n        return text\n\n    def can_draw_regresssion_line(self):\n        return self.data is not None and \\\n               self.data.domain is not None and \\\n               self.attr_x.is_continuous and \\\n               self.attr_y.is_continuous\n\n    def add_data(self, time=0.4):\n        if self.data and len(self.data) > 2000:\n            self.__timer.stop()\n            return\n        data_sample = self.sql_data.sample_time(time, no_cache=True)\n        if data_sample:\n            data_sample.download_data(2000, partial=True)\n            data = Table(data_sample)\n            self.data = Table.concatenate((self.data, data), axis=0)\n            self.handleNewSignals()\n\n    def init_attr_values(self):\n        super().init_attr_values()\n        data = self.data\n        domain = data.domain if data and len(data) else None\n        self.xy_model.set_domain(domain)\n        self.attr_x = self.xy_model[0] if self.xy_model else None\n        self.attr_y = self.xy_model[1] if len(self.xy_model) >= 2 \\\n            else self.attr_x\n\n    def switch_sampling(self):\n        self.__timer.stop()\n        if self.auto_sample and self.sql_data:\n            self.add_data()\n            self.__timer.start()\n\n    @OWDataProjectionWidget.Inputs.data_subset\n    def set_subset_data(self, subset_data):\n        self.warning()\n        if isinstance(subset_data, SqlTable):\n            if subset_data.approx_len() < AUTO_DL_LIMIT:\n                subset_data = Table(subset_data)\n            else:\n                self.warning(\"Data subset does not support large Sql tables\")\n                subset_data = None\n        super().set_subset_data(subset_data)\n\n    # called when all signals are received, so the graph is updated only once\n    def handleNewSignals(self):\n        self.attr_box.setEnabled(True)\n        self.vizrank.setEnabled(True)\n        if self.attribute_selection_list and self.data is not None and \\\n                self.data.domain is not None and \\\n                all(attr in self.data.domain for attr\n                        in self.attribute_selection_list):\n            self.attr_x, self.attr_y = self.attribute_selection_list[:2]\n            self.attr_box.setEnabled(False)\n            self.vizrank.setEnabled(False)\n        self._invalidated = self._invalidated or self._xy_invalidated\n        self._xy_invalidated = False\n        super().handleNewSignals()\n        if self._domain_invalidated:\n            self.graph.update_axes()\n            self._domain_invalidated = False\n        self.cb_reg_line.setEnabled(self.can_draw_regresssion_line())\n\n    @Inputs.features\n    def set_shown_attributes(self, attributes):\n        if attributes and len(attributes) >= 2:\n            self.attribute_selection_list = attributes[:2]\n            self._xy_invalidated = self._xy_invalidated \\\n                or self.attr_x != attributes[0] \\\n                or self.attr_y != attributes[1]\n        else:\n            self.attribute_selection_list = None\n\n    def set_attr(self, attr_x, attr_y):\n        if attr_x != self.attr_x or attr_y != self.attr_y:\n            self.attr_x, self.attr_y = attr_x, attr_y\n            self.attr_changed()\n\n    def set_attr_from_combo(self):\n        self.attr_changed()\n        self.xy_changed_manually.emit(self.attr_x, self.attr_y)\n\n    def attr_changed(self):\n        self.cb_reg_line.setEnabled(self.can_draw_regresssion_line())\n        self.setup_plot()\n        self.commit.deferred()\n\n    def get_axes(self):\n        return {\"bottom\": self.attr_x, \"left\": self.attr_y}\n\n    def colors_changed(self):\n        super().colors_changed()\n        self._vizrank_color_change()\n\n    @gui.deferred\n    def commit(self):\n        super().commit()\n        self.send_features()\n\n    def send_features(self):\n        features = [attr for attr in [self.attr_x, self.attr_y] if attr]\n        self.Outputs.features.send(AttributeList(features) or None)\n\n    def get_widget_name_extension(self):\n        if self.data is not None:\n            return \"{} vs {}\".format(self.attr_x.name, self.attr_y.name)\n        return None\n\n    def _get_send_report_caption(self):\n        return report.render_items_vert((\n            (\"Color\", self._get_caption_var_name(self.attr_color)),\n            (\"Label\", self._get_caption_var_name(self.attr_label)),\n            (\"Shape\", self._get_caption_var_name(self.attr_shape)),\n            (\"Size\", self._get_caption_var_name(self.attr_size)),\n            (\"Jittering\", (self.attr_x.is_discrete or\n                           self.attr_y.is_discrete or\n                           self.graph.jitter_continuous) and\n             self.graph.jitter_size)))\n\n    @classmethod\n    def migrate_settings(cls, settings, version):\n        if version < 2 and \"selection\" in settings and settings[\"selection\"]:\n            settings[\"selection_group\"] = [(a, 1) for a in settings[\"selection\"]]\n        if version < 3:\n            if \"auto_send_selection\" in settings:\n                settings[\"auto_commit\"] = settings[\"auto_send_selection\"]\n            if \"selection_group\" in settings:\n                settings[\"selection\"] = settings[\"selection_group\"]\n        if version < 5:\n            if \"graph\" in settings and \\\n                    \"jitter_continuous\" not in settings[\"graph\"]:\n                settings[\"graph\"][\"jitter_continuous\"] = True\n\n    @classmethod\n    def migrate_context(cls, context, version):\n        values = context.values\n        if version < 3:\n            values[\"attr_color\"] = values[\"graph\"][\"attr_color\"]\n            values[\"attr_size\"] = values[\"graph\"][\"attr_size\"]\n            values[\"attr_shape\"] = values[\"graph\"][\"attr_shape\"]\n            values[\"attr_label\"] = values[\"graph\"][\"attr_label\"]\n        if version < 4:\n            if values[\"attr_x\"][1] % 100 == 1 or values[\"attr_y\"][1] % 100 == 1:\n                raise IncompatibleContext()",
                            "ast_type_str": "ClassDef",
                            "start_lineno": 314,
                            "end_lineno": 673,
                            "parent": null
                        }
                    ],
                    "added_entities": [],
                    "deleted_entities": []
                },
                {
                    "descriptor": {
                        "old_range": {
                            "start": 591,
                            "length": 7
                        },
                        "new_range": {
                            "start": 594,
                            "length": 7
                        },
                        "section": "class OWScatterPlot(OWDataProjectionWidget):"
                    },
                    "line_group": {
                        "all_lines": [
                            {
                                "content": "    def set_shown_attributes(self, attributes):",
                                "type": "context"
                            },
                            {
                                "content": "        if attributes and len(attributes) >= 2:",
                                "type": "context"
                            },
                            {
                                "content": "            self.attribute_selection_list = attributes[:2]",
                                "type": "context"
                            },
                            {
                                "content": "            self._invalidated = self._invalidated \\",
                                "type": "deleted"
                            },
                            {
                                "content": "            self._xy_invalidated = self._xy_invalidated \\",
                                "type": "added"
                            },
                            {
                                "content": "                or self.attr_x != attributes[0] \\",
                                "type": "context"
                            },
                            {
                                "content": "                or self.attr_y != attributes[1]",
                                "type": "context"
                            },
                            {
                                "content": "        else:",
                                "type": "context"
                            }
                        ]
                    },
                    "modified_entities": [
                        {
                            "file_name": "Orange/widgets/visualize/owscatterplot.py",
                            "type": "method",
                            "name": "OWScatterPlot.set_shown_attributes",
                            "content": "class OWScatterPlot(OWDataProjectionWidget):\n    \"\"\"Scatterplot visualization with explorative analysis and intelligent\n    data visualization enhancements.\"\"\"\n\n    name = 'Scatter Plot'\n    description = \"Interactive scatter plot visualization with \" \\\n                  \"intelligent data visualization enhancements.\"\n    icon = \"icons/ScatterPlot.svg\"\n    priority = 140\n    keywords = []\n\n    class Inputs(OWDataProjectionWidget.Inputs):\n        features = Input(\"Features\", AttributeList)\n\n    class Outputs(OWDataProjectionWidget.Outputs):\n        features = Output(\"Features\", AttributeList, dynamic=False)\n\n    settings_version = 5\n    auto_sample = Setting(True)\n    attr_x = ContextSetting(None)\n    attr_y = ContextSetting(None)\n    tooltip_shows_all = Setting(True)\n\n    GRAPH_CLASS = OWScatterPlotGraph\n    graph = SettingProvider(OWScatterPlotGraph)\n    embedding_variables_names = None\n\n    xy_changed_manually = Signal(Variable, Variable)\n\n    class Warning(OWDataProjectionWidget.Warning):\n        missing_coords = Msg(\n            \"Plot cannot be displayed because '{}' or '{}' \"\n            \"is missing for all data points.\")\n\n    class Information(OWDataProjectionWidget.Information):\n        sampled_sql = Msg(\"Large SQL table; showing a sample.\")\n        missing_coords = Msg(\n            \"Points with missing '{}' or '{}' are not displayed\")\n\n    def __init__(self):\n        self.attr_box: QGroupBox = None\n        self.xy_model: DomainModel = None\n        self.cb_attr_x: ComboBoxSearch = None\n        self.cb_attr_y: ComboBoxSearch = None\n        self.vizrank: ScatterPlotVizRank = None\n        self.vizrank_button: QPushButton = None\n        self.sampling: QGroupBox = None\n\n        self.sql_data = None  # Orange.data.sql.table.SqlTable\n        self.attribute_selection_list = None  # list of Orange.data.Variable\n        self.__timer = QTimer(self, interval=1200)\n        self.__timer.timeout.connect(self.add_data)\n        super().__init__()\n\n        # manually register Matplotlib file writers\n        self.graph_writers = self.graph_writers.copy()\n        for w in [MatplotlibFormat, MatplotlibPDFFormat]:\n            self.graph_writers.append(w)\n\n    def _add_controls(self):\n        self._add_controls_axis()\n        self._add_controls_sampling()\n        super()._add_controls()\n        self.gui.add_widget(self.gui.JitterNumericValues, self._effects_box)\n        self.gui.add_widgets(\n            [self.gui.ShowGridLines,\n             self.gui.ToolTipShowsAll,\n             self.gui.RegressionLine],\n            self._plot_box)\n        gui.checkBox(\n            self._plot_box, self,\n            value=\"graph.orthonormal_regression\",\n            label=\"Treat variables as independent\",\n            callback=self.graph.update_regression_line,\n            tooltip=\n            \"If checked, fit line to group (minimize distance from points);\\n\"\n            \"otherwise fit y as a function of x (minimize vertical distances)\",\n            disabledBy=self.cb_reg_line)\n\n    def _add_controls_axis(self):\n        common_options = dict(\n            labelWidth=50, orientation=Qt.Horizontal, sendSelectedValue=True,\n            contentsLength=12, searchable=True\n        )\n        self.attr_box = gui.vBox(self.controlArea, 'Axes',\n                                 spacing=2 if gui.is_macstyle() else 8)\n        dmod = DomainModel\n        self.xy_model = DomainModel(dmod.MIXED, valid_types=dmod.PRIMITIVE)\n        self.cb_attr_x = gui.comboBox(\n            self.attr_box, self, \"attr_x\", label=\"Axis x:\",\n            callback=self.set_attr_from_combo,\n            model=self.xy_model, **common_options,\n        )\n        self.cb_attr_y = gui.comboBox(\n            self.attr_box, self, \"attr_y\", label=\"Axis y:\",\n            callback=self.set_attr_from_combo,\n            model=self.xy_model, **common_options,\n        )\n        vizrank_box = gui.hBox(self.attr_box)\n        self.vizrank, self.vizrank_button = ScatterPlotVizRank.add_vizrank(\n            vizrank_box, self, \"Find Informative Projections\", self.set_attr)\n\n    def _add_controls_sampling(self):\n        self.sampling = gui.auto_commit(\n            self.controlArea, self, \"auto_sample\", \"Sample\", box=\"Sampling\",\n            callback=self.switch_sampling, commit=lambda: self.add_data(1))\n        self.sampling.setVisible(False)\n\n    @property\n    def effective_variables(self):\n        return [self.attr_x, self.attr_y] if self.attr_x and self.attr_y else []\n\n    @property\n    def effective_data(self):\n        eff_var = self.effective_variables\n        if eff_var and self.attr_x.name == self.attr_y.name:\n            eff_var = [self.attr_x]\n        return self.data.transform(Domain(eff_var))\n\n    def _vizrank_color_change(self):\n        self.vizrank.initialize()\n        err_msg = \"\"\n        if self.data is None:\n            err_msg = \"No data on input\"\n        elif self.data.is_sparse():\n            err_msg = \"Data is sparse\"\n        elif len(self.xy_model) < 3:\n            err_msg = \"Not enough features for ranking\"\n        elif self.attr_color is None:\n            err_msg = \"Color variable is not selected\"\n        elif np.isnan(self.data.get_column_view(\n                self.attr_color)[0].astype(float)).all():\n            err_msg = \"Color variable has no values\"\n        self.vizrank_button.setEnabled(not err_msg)\n        self.vizrank_button.setToolTip(err_msg)\n\n    @OWDataProjectionWidget.Inputs.data\n    def set_data(self, data):\n        super().set_data(data)\n        self._vizrank_color_change()\n\n        def findvar(name, iterable):\n            \"\"\"Find a Orange.data.Variable in `iterable` by name\"\"\"\n            for el in iterable:\n                if isinstance(el, Variable) and el.name == name:\n                    return el\n            return None\n\n        # handle restored settings from  < 3.3.9 when attr_* were stored\n        # by name\n        if isinstance(self.attr_x, str):\n            self.attr_x = findvar(self.attr_x, self.xy_model)\n        if isinstance(self.attr_y, str):\n            self.attr_y = findvar(self.attr_y, self.xy_model)\n        if isinstance(self.attr_label, str):\n            self.attr_label = findvar(self.attr_label, self.gui.label_model)\n        if isinstance(self.attr_color, str):\n            self.attr_color = findvar(self.attr_color, self.gui.color_model)\n        if isinstance(self.attr_shape, str):\n            self.attr_shape = findvar(self.attr_shape, self.gui.shape_model)\n        if isinstance(self.attr_size, str):\n            self.attr_size = findvar(self.attr_size, self.gui.size_model)\n\n    def check_data(self):\n        super().check_data()\n        self.__timer.stop()\n        self.sampling.setVisible(False)\n        self.sql_data = None\n        if isinstance(self.data, SqlTable):\n            if self.data.approx_len() < 4000:\n                self.data = Table(self.data)\n            else:\n                self.Information.sampled_sql()\n                self.sql_data = self.data\n                data_sample = self.data.sample_time(0.8, no_cache=True)\n                data_sample.download_data(2000, partial=True)\n                self.data = Table(data_sample)\n                self.sampling.setVisible(True)\n                if self.auto_sample:\n                    self.__timer.start()\n\n        if self.data is not None and (len(self.data) == 0 or\n                                      len(self.data.domain.variables) == 0):\n            self.data = None\n\n    def get_embedding(self):\n        self.valid_data = None\n        if self.data is None:\n            return None\n\n        x_data = self.get_column(self.attr_x, filter_valid=False)\n        y_data = self.get_column(self.attr_y, filter_valid=False)\n        if x_data is None or y_data is None:\n            return None\n\n        self.Warning.missing_coords.clear()\n        self.Information.missing_coords.clear()\n        self.valid_data = np.isfinite(x_data) & np.isfinite(y_data)\n        if self.valid_data is not None and not np.all(self.valid_data):\n            msg = self.Information if np.any(self.valid_data) else self.Warning\n            msg.missing_coords(self.attr_x.name, self.attr_y.name)\n        return np.vstack((x_data, y_data)).T\n\n    # Tooltip\n    def _point_tooltip(self, point_id, skip_attrs=()):\n        point_data = self.data[point_id]\n        xy_attrs = (self.attr_x, self.attr_y)\n        text = \"<br/>\".join(\n            escape('{} = {}'.format(var.name, point_data[var]))\n            for var in xy_attrs)\n        if self.tooltip_shows_all:\n            others = super()._point_tooltip(point_id, skip_attrs=xy_attrs)\n            if others:\n                text = \"<b>{}</b><br/><br/>{}\".format(text, others)\n        return text\n\n    def can_draw_regresssion_line(self):\n        return self.data is not None and \\\n               self.data.domain is not None and \\\n               self.attr_x.is_continuous and \\\n               self.attr_y.is_continuous\n\n    def add_data(self, time=0.4):\n        if self.data and len(self.data) > 2000:\n            self.__timer.stop()\n            return\n        data_sample = self.sql_data.sample_time(time, no_cache=True)\n        if data_sample:\n            data_sample.download_data(2000, partial=True)\n            data = Table(data_sample)\n            self.data = Table.concatenate((self.data, data), axis=0)\n            self.handleNewSignals()\n\n    def init_attr_values(self):\n        super().init_attr_values()\n        data = self.data\n        domain = data.domain if data and len(data) else None\n        self.xy_model.set_domain(domain)\n        self.attr_x = self.xy_model[0] if self.xy_model else None\n        self.attr_y = self.xy_model[1] if len(self.xy_model) >= 2 \\\n            else self.attr_x\n\n    def switch_sampling(self):\n        self.__timer.stop()\n        if self.auto_sample and self.sql_data:\n            self.add_data()\n            self.__timer.start()\n\n    @OWDataProjectionWidget.Inputs.data_subset\n    def set_subset_data(self, subset_data):\n        self.warning()\n        if isinstance(subset_data, SqlTable):\n            if subset_data.approx_len() < AUTO_DL_LIMIT:\n                subset_data = Table(subset_data)\n            else:\n                self.warning(\"Data subset does not support large Sql tables\")\n                subset_data = None\n        super().set_subset_data(subset_data)\n\n    # called when all signals are received, so the graph is updated only once\n    def handleNewSignals(self):\n        self.attr_box.setEnabled(True)\n        self.vizrank.setEnabled(True)\n        if self.attribute_selection_list and self.data is not None and \\\n                self.data.domain is not None and \\\n                all(attr in self.data.domain for attr\n                        in self.attribute_selection_list):\n            self.attr_x, self.attr_y = self.attribute_selection_list[:2]\n            self.attr_box.setEnabled(False)\n            self.vizrank.setEnabled(False)\n        super().handleNewSignals()\n        if self._domain_invalidated:\n            self.graph.update_axes()\n            self._domain_invalidated = False\n        self.cb_reg_line.setEnabled(self.can_draw_regresssion_line())\n\n    @Inputs.features\n    def set_shown_attributes(self, attributes):\n        if attributes and len(attributes) >= 2:\n            self.attribute_selection_list = attributes[:2]\n            self._invalidated = self._invalidated \\\n                or self.attr_x != attributes[0] \\\n                or self.attr_y != attributes[1]\n        else:\n            self.attribute_selection_list = None\n\n    def set_attr(self, attr_x, attr_y):\n        if attr_x != self.attr_x or attr_y != self.attr_y:\n            self.attr_x, self.attr_y = attr_x, attr_y\n            self.attr_changed()\n\n    def set_attr_from_combo(self):\n        self.attr_changed()\n        self.xy_changed_manually.emit(self.attr_x, self.attr_y)\n\n    def attr_changed(self):\n        self.cb_reg_line.setEnabled(self.can_draw_regresssion_line())\n        self.setup_plot()\n        self.commit.deferred()\n\n    def get_axes(self):\n        return {\"bottom\": self.attr_x, \"left\": self.attr_y}\n\n    def colors_changed(self):\n        super().colors_changed()\n        self._vizrank_color_change()\n\n    @gui.deferred\n    def commit(self):\n        super().commit()\n        self.send_features()\n\n    def send_features(self):\n        features = [attr for attr in [self.attr_x, self.attr_y] if attr]\n        self.Outputs.features.send(AttributeList(features) or None)\n\n    def get_widget_name_extension(self):\n        if self.data is not None:\n            return \"{} vs {}\".format(self.attr_x.name, self.attr_y.name)\n        return None\n\n    def _get_send_report_caption(self):\n        return report.render_items_vert((\n            (\"Color\", self._get_caption_var_name(self.attr_color)),\n            (\"Label\", self._get_caption_var_name(self.attr_label)),\n            (\"Shape\", self._get_caption_var_name(self.attr_shape)),\n            (\"Size\", self._get_caption_var_name(self.attr_size)),\n            (\"Jittering\", (self.attr_x.is_discrete or\n                           self.attr_y.is_discrete or\n                           self.graph.jitter_continuous) and\n             self.graph.jitter_size)))\n\n    @classmethod\n    def migrate_settings(cls, settings, version):\n        if version < 2 and \"selection\" in settings and settings[\"selection\"]:\n            settings[\"selection_group\"] = [(a, 1) for a in settings[\"selection\"]]\n        if version < 3:\n            if \"auto_send_selection\" in settings:\n                settings[\"auto_commit\"] = settings[\"auto_send_selection\"]\n            if \"selection_group\" in settings:\n                settings[\"selection\"] = settings[\"selection_group\"]\n        if version < 5:\n            if \"graph\" in settings and \\\n                    \"jitter_continuous\" not in settings[\"graph\"]:\n                settings[\"graph\"][\"jitter_continuous\"] = True\n\n    @classmethod\n    def migrate_context(cls, context, version):\n        values = context.values\n        if version < 3:\n            values[\"attr_color\"] = values[\"graph\"][\"attr_color\"]\n            values[\"attr_size\"] = values[\"graph\"][\"attr_size\"]\n            values[\"attr_shape\"] = values[\"graph\"][\"attr_shape\"]\n            values[\"attr_label\"] = values[\"graph\"][\"attr_label\"]\n        if version < 4:\n            if values[\"attr_x\"][1] % 100 == 1 or values[\"attr_y\"][1] % 100 == 1:\n                raise IncompatibleContext()",
                            "ast_type_str": "ClassDef",
                            "start_lineno": 591,
                            "end_lineno": 598,
                            "parent": {
                                "file_name": "Orange/widgets/visualize/owscatterplot.py",
                                "type": "class",
                                "name": "OWScatterPlot",
                                "content": "class OWScatterPlot(OWDataProjectionWidget):\n    \"\"\"Scatterplot visualization with explorative analysis and intelligent\n    data visualization enhancements.\"\"\"\n\n    name = 'Scatter Plot'\n    description = \"Interactive scatter plot visualization with \" \\\n                  \"intelligent data visualization enhancements.\"\n    icon = \"icons/ScatterPlot.svg\"\n    priority = 140\n    keywords = []\n\n    class Inputs(OWDataProjectionWidget.Inputs):\n        features = Input(\"Features\", AttributeList)\n\n    class Outputs(OWDataProjectionWidget.Outputs):\n        features = Output(\"Features\", AttributeList, dynamic=False)\n\n    settings_version = 5\n    auto_sample = Setting(True)\n    attr_x = ContextSetting(None)\n    attr_y = ContextSetting(None)\n    tooltip_shows_all = Setting(True)\n\n    GRAPH_CLASS = OWScatterPlotGraph\n    graph = SettingProvider(OWScatterPlotGraph)\n    embedding_variables_names = None\n\n    xy_changed_manually = Signal(Variable, Variable)\n\n    class Warning(OWDataProjectionWidget.Warning):\n        missing_coords = Msg(\n            \"Plot cannot be displayed because '{}' or '{}' \"\n            \"is missing for all data points.\")\n\n    class Information(OWDataProjectionWidget.Information):\n        sampled_sql = Msg(\"Large SQL table; showing a sample.\")\n        missing_coords = Msg(\n            \"Points with missing '{}' or '{}' are not displayed\")\n\n    def __init__(self):\n        self.attr_box: QGroupBox = None\n        self.xy_model: DomainModel = None\n        self.cb_attr_x: ComboBoxSearch = None\n        self.cb_attr_y: ComboBoxSearch = None\n        self.vizrank: ScatterPlotVizRank = None\n        self.vizrank_button: QPushButton = None\n        self.sampling: QGroupBox = None\n\n        self.sql_data = None  # Orange.data.sql.table.SqlTable\n        self.attribute_selection_list = None  # list of Orange.data.Variable\n        self.__timer = QTimer(self, interval=1200)\n        self.__timer.timeout.connect(self.add_data)\n        super().__init__()\n\n        # manually register Matplotlib file writers\n        self.graph_writers = self.graph_writers.copy()\n        for w in [MatplotlibFormat, MatplotlibPDFFormat]:\n            self.graph_writers.append(w)\n\n    def _add_controls(self):\n        self._add_controls_axis()\n        self._add_controls_sampling()\n        super()._add_controls()\n        self.gui.add_widget(self.gui.JitterNumericValues, self._effects_box)\n        self.gui.add_widgets(\n            [self.gui.ShowGridLines,\n             self.gui.ToolTipShowsAll,\n             self.gui.RegressionLine],\n            self._plot_box)\n        gui.checkBox(\n            self._plot_box, self,\n            value=\"graph.orthonormal_regression\",\n            label=\"Treat variables as independent\",\n            callback=self.graph.update_regression_line,\n            tooltip=\n            \"If checked, fit line to group (minimize distance from points);\\n\"\n            \"otherwise fit y as a function of x (minimize vertical distances)\",\n            disabledBy=self.cb_reg_line)\n\n    def _add_controls_axis(self):\n        common_options = dict(\n            labelWidth=50, orientation=Qt.Horizontal, sendSelectedValue=True,\n            contentsLength=12, searchable=True\n        )\n        self.attr_box = gui.vBox(self.controlArea, 'Axes',\n                                 spacing=2 if gui.is_macstyle() else 8)\n        dmod = DomainModel\n        self.xy_model = DomainModel(dmod.MIXED, valid_types=dmod.PRIMITIVE)\n        self.cb_attr_x = gui.comboBox(\n            self.attr_box, self, \"attr_x\", label=\"Axis x:\",\n            callback=self.set_attr_from_combo,\n            model=self.xy_model, **common_options,\n        )\n        self.cb_attr_y = gui.comboBox(\n            self.attr_box, self, \"attr_y\", label=\"Axis y:\",\n            callback=self.set_attr_from_combo,\n            model=self.xy_model, **common_options,\n        )\n        vizrank_box = gui.hBox(self.attr_box)\n        self.vizrank, self.vizrank_button = ScatterPlotVizRank.add_vizrank(\n            vizrank_box, self, \"Find Informative Projections\", self.set_attr)\n\n    def _add_controls_sampling(self):\n        self.sampling = gui.auto_commit(\n            self.controlArea, self, \"auto_sample\", \"Sample\", box=\"Sampling\",\n            callback=self.switch_sampling, commit=lambda: self.add_data(1))\n        self.sampling.setVisible(False)\n\n    @property\n    def effective_variables(self):\n        return [self.attr_x, self.attr_y] if self.attr_x and self.attr_y else []\n\n    @property\n    def effective_data(self):\n        eff_var = self.effective_variables\n        if eff_var and self.attr_x.name == self.attr_y.name:\n            eff_var = [self.attr_x]\n        return self.data.transform(Domain(eff_var))\n\n    def _vizrank_color_change(self):\n        self.vizrank.initialize()\n        err_msg = \"\"\n        if self.data is None:\n            err_msg = \"No data on input\"\n        elif self.data.is_sparse():\n            err_msg = \"Data is sparse\"\n        elif len(self.xy_model) < 3:\n            err_msg = \"Not enough features for ranking\"\n        elif self.attr_color is None:\n            err_msg = \"Color variable is not selected\"\n        elif np.isnan(self.data.get_column_view(\n                self.attr_color)[0].astype(float)).all():\n            err_msg = \"Color variable has no values\"\n        self.vizrank_button.setEnabled(not err_msg)\n        self.vizrank_button.setToolTip(err_msg)\n\n    @OWDataProjectionWidget.Inputs.data\n    def set_data(self, data):\n        super().set_data(data)\n        self._vizrank_color_change()\n\n        def findvar(name, iterable):\n            \"\"\"Find a Orange.data.Variable in `iterable` by name\"\"\"\n            for el in iterable:\n                if isinstance(el, Variable) and el.name == name:\n                    return el\n            return None\n\n        # handle restored settings from  < 3.3.9 when attr_* were stored\n        # by name\n        if isinstance(self.attr_x, str):\n            self.attr_x = findvar(self.attr_x, self.xy_model)\n        if isinstance(self.attr_y, str):\n            self.attr_y = findvar(self.attr_y, self.xy_model)\n        if isinstance(self.attr_label, str):\n            self.attr_label = findvar(self.attr_label, self.gui.label_model)\n        if isinstance(self.attr_color, str):\n            self.attr_color = findvar(self.attr_color, self.gui.color_model)\n        if isinstance(self.attr_shape, str):\n            self.attr_shape = findvar(self.attr_shape, self.gui.shape_model)\n        if isinstance(self.attr_size, str):\n            self.attr_size = findvar(self.attr_size, self.gui.size_model)\n\n    def check_data(self):\n        super().check_data()\n        self.__timer.stop()\n        self.sampling.setVisible(False)\n        self.sql_data = None\n        if isinstance(self.data, SqlTable):\n            if self.data.approx_len() < 4000:\n                self.data = Table(self.data)\n            else:\n                self.Information.sampled_sql()\n                self.sql_data = self.data\n                data_sample = self.data.sample_time(0.8, no_cache=True)\n                data_sample.download_data(2000, partial=True)\n                self.data = Table(data_sample)\n                self.sampling.setVisible(True)\n                if self.auto_sample:\n                    self.__timer.start()\n\n        if self.data is not None and (len(self.data) == 0 or\n                                      len(self.data.domain.variables) == 0):\n            self.data = None\n\n    def get_embedding(self):\n        self.valid_data = None\n        if self.data is None:\n            return None\n\n        x_data = self.get_column(self.attr_x, filter_valid=False)\n        y_data = self.get_column(self.attr_y, filter_valid=False)\n        if x_data is None or y_data is None:\n            return None\n\n        self.Warning.missing_coords.clear()\n        self.Information.missing_coords.clear()\n        self.valid_data = np.isfinite(x_data) & np.isfinite(y_data)\n        if self.valid_data is not None and not np.all(self.valid_data):\n            msg = self.Information if np.any(self.valid_data) else self.Warning\n            msg.missing_coords(self.attr_x.name, self.attr_y.name)\n        return np.vstack((x_data, y_data)).T\n\n    # Tooltip\n    def _point_tooltip(self, point_id, skip_attrs=()):\n        point_data = self.data[point_id]\n        xy_attrs = (self.attr_x, self.attr_y)\n        text = \"<br/>\".join(\n            escape('{} = {}'.format(var.name, point_data[var]))\n            for var in xy_attrs)\n        if self.tooltip_shows_all:\n            others = super()._point_tooltip(point_id, skip_attrs=xy_attrs)\n            if others:\n                text = \"<b>{}</b><br/><br/>{}\".format(text, others)\n        return text\n\n    def can_draw_regresssion_line(self):\n        return self.data is not None and \\\n               self.data.domain is not None and \\\n               self.attr_x.is_continuous and \\\n               self.attr_y.is_continuous\n\n    def add_data(self, time=0.4):\n        if self.data and len(self.data) > 2000:\n            self.__timer.stop()\n            return\n        data_sample = self.sql_data.sample_time(time, no_cache=True)\n        if data_sample:\n            data_sample.download_data(2000, partial=True)\n            data = Table(data_sample)\n            self.data = Table.concatenate((self.data, data), axis=0)\n            self.handleNewSignals()\n\n    def init_attr_values(self):\n        super().init_attr_values()\n        data = self.data\n        domain = data.domain if data and len(data) else None\n        self.xy_model.set_domain(domain)\n        self.attr_x = self.xy_model[0] if self.xy_model else None\n        self.attr_y = self.xy_model[1] if len(self.xy_model) >= 2 \\\n            else self.attr_x\n\n    def switch_sampling(self):\n        self.__timer.stop()\n        if self.auto_sample and self.sql_data:\n            self.add_data()\n            self.__timer.start()\n\n    @OWDataProjectionWidget.Inputs.data_subset\n    def set_subset_data(self, subset_data):\n        self.warning()\n        if isinstance(subset_data, SqlTable):\n            if subset_data.approx_len() < AUTO_DL_LIMIT:\n                subset_data = Table(subset_data)\n            else:\n                self.warning(\"Data subset does not support large Sql tables\")\n                subset_data = None\n        super().set_subset_data(subset_data)\n\n    # called when all signals are received, so the graph is updated only once\n    def handleNewSignals(self):\n        self.attr_box.setEnabled(True)\n        self.vizrank.setEnabled(True)\n        if self.attribute_selection_list and self.data is not None and \\\n                self.data.domain is not None and \\\n                all(attr in self.data.domain for attr\n                        in self.attribute_selection_list):\n            self.attr_x, self.attr_y = self.attribute_selection_list[:2]\n            self.attr_box.setEnabled(False)\n            self.vizrank.setEnabled(False)\n        super().handleNewSignals()\n        if self._domain_invalidated:\n            self.graph.update_axes()\n            self._domain_invalidated = False\n        self.cb_reg_line.setEnabled(self.can_draw_regresssion_line())\n\n    @Inputs.features\n    def set_shown_attributes(self, attributes):\n        if attributes and len(attributes) >= 2:\n            self.attribute_selection_list = attributes[:2]\n            self._invalidated = self._invalidated \\\n                or self.attr_x != attributes[0] \\\n                or self.attr_y != attributes[1]\n        else:\n            self.attribute_selection_list = None\n\n    def set_attr(self, attr_x, attr_y):\n        if attr_x != self.attr_x or attr_y != self.attr_y:\n            self.attr_x, self.attr_y = attr_x, attr_y\n            self.attr_changed()\n\n    def set_attr_from_combo(self):\n        self.attr_changed()\n        self.xy_changed_manually.emit(self.attr_x, self.attr_y)\n\n    def attr_changed(self):\n        self.cb_reg_line.setEnabled(self.can_draw_regresssion_line())\n        self.setup_plot()\n        self.commit.deferred()\n\n    def get_axes(self):\n        return {\"bottom\": self.attr_x, \"left\": self.attr_y}\n\n    def colors_changed(self):\n        super().colors_changed()\n        self._vizrank_color_change()\n\n    @gui.deferred\n    def commit(self):\n        super().commit()\n        self.send_features()\n\n    def send_features(self):\n        features = [attr for attr in [self.attr_x, self.attr_y] if attr]\n        self.Outputs.features.send(AttributeList(features) or None)\n\n    def get_widget_name_extension(self):\n        if self.data is not None:\n            return \"{} vs {}\".format(self.attr_x.name, self.attr_y.name)\n        return None\n\n    def _get_send_report_caption(self):\n        return report.render_items_vert((\n            (\"Color\", self._get_caption_var_name(self.attr_color)),\n            (\"Label\", self._get_caption_var_name(self.attr_label)),\n            (\"Shape\", self._get_caption_var_name(self.attr_shape)),\n            (\"Size\", self._get_caption_var_name(self.attr_size)),\n            (\"Jittering\", (self.attr_x.is_discrete or\n                           self.attr_y.is_discrete or\n                           self.graph.jitter_continuous) and\n             self.graph.jitter_size)))\n\n    @classmethod\n    def migrate_settings(cls, settings, version):\n        if version < 2 and \"selection\" in settings and settings[\"selection\"]:\n            settings[\"selection_group\"] = [(a, 1) for a in settings[\"selection\"]]\n        if version < 3:\n            if \"auto_send_selection\" in settings:\n                settings[\"auto_commit\"] = settings[\"auto_send_selection\"]\n            if \"selection_group\" in settings:\n                settings[\"selection\"] = settings[\"selection_group\"]\n        if version < 5:\n            if \"graph\" in settings and \\\n                    \"jitter_continuous\" not in settings[\"graph\"]:\n                settings[\"graph\"][\"jitter_continuous\"] = True\n\n    @classmethod\n    def migrate_context(cls, context, version):\n        values = context.values\n        if version < 3:\n            values[\"attr_color\"] = values[\"graph\"][\"attr_color\"]\n            values[\"attr_size\"] = values[\"graph\"][\"attr_size\"]\n            values[\"attr_shape\"] = values[\"graph\"][\"attr_shape\"]\n            values[\"attr_label\"] = values[\"graph\"][\"attr_label\"]\n        if version < 4:\n            if values[\"attr_x\"][1] % 100 == 1 or values[\"attr_y\"][1] % 100 == 1:\n                raise IncompatibleContext()",
                                "ast_type_str": "ClassDef",
                                "start_lineno": 314,
                                "end_lineno": 670,
                                "parent": null
                            }
                        },
                        {
                            "file_name": "Orange/widgets/visualize/owscatterplot.py",
                            "type": "class",
                            "name": "OWScatterPlot",
                            "content": "class OWScatterPlot(OWDataProjectionWidget):\n    \"\"\"Scatterplot visualization with explorative analysis and intelligent\n    data visualization enhancements.\"\"\"\n\n    name = 'Scatter Plot'\n    description = \"Interactive scatter plot visualization with \" \\\n                  \"intelligent data visualization enhancements.\"\n    icon = \"icons/ScatterPlot.svg\"\n    priority = 140\n    keywords = []\n\n    class Inputs(OWDataProjectionWidget.Inputs):\n        features = Input(\"Features\", AttributeList)\n\n    class Outputs(OWDataProjectionWidget.Outputs):\n        features = Output(\"Features\", AttributeList, dynamic=False)\n\n    settings_version = 5\n    auto_sample = Setting(True)\n    attr_x = ContextSetting(None)\n    attr_y = ContextSetting(None)\n    tooltip_shows_all = Setting(True)\n\n    GRAPH_CLASS = OWScatterPlotGraph\n    graph = SettingProvider(OWScatterPlotGraph)\n    embedding_variables_names = None\n\n    xy_changed_manually = Signal(Variable, Variable)\n\n    class Warning(OWDataProjectionWidget.Warning):\n        missing_coords = Msg(\n            \"Plot cannot be displayed because '{}' or '{}' \"\n            \"is missing for all data points.\")\n\n    class Information(OWDataProjectionWidget.Information):\n        sampled_sql = Msg(\"Large SQL table; showing a sample.\")\n        missing_coords = Msg(\n            \"Points with missing '{}' or '{}' are not displayed\")\n\n    def __init__(self):\n        self.attr_box: QGroupBox = None\n        self.xy_model: DomainModel = None\n        self.cb_attr_x: ComboBoxSearch = None\n        self.cb_attr_y: ComboBoxSearch = None\n        self.vizrank: ScatterPlotVizRank = None\n        self.vizrank_button: QPushButton = None\n        self.sampling: QGroupBox = None\n\n        self.sql_data = None  # Orange.data.sql.table.SqlTable\n        self.attribute_selection_list = None  # list of Orange.data.Variable\n        self.__timer = QTimer(self, interval=1200)\n        self.__timer.timeout.connect(self.add_data)\n        super().__init__()\n\n        # manually register Matplotlib file writers\n        self.graph_writers = self.graph_writers.copy()\n        for w in [MatplotlibFormat, MatplotlibPDFFormat]:\n            self.graph_writers.append(w)\n\n    def _add_controls(self):\n        self._add_controls_axis()\n        self._add_controls_sampling()\n        super()._add_controls()\n        self.gui.add_widget(self.gui.JitterNumericValues, self._effects_box)\n        self.gui.add_widgets(\n            [self.gui.ShowGridLines,\n             self.gui.ToolTipShowsAll,\n             self.gui.RegressionLine],\n            self._plot_box)\n        gui.checkBox(\n            self._plot_box, self,\n            value=\"graph.orthonormal_regression\",\n            label=\"Treat variables as independent\",\n            callback=self.graph.update_regression_line,\n            tooltip=\n            \"If checked, fit line to group (minimize distance from points);\\n\"\n            \"otherwise fit y as a function of x (minimize vertical distances)\",\n            disabledBy=self.cb_reg_line)\n\n    def _add_controls_axis(self):\n        common_options = dict(\n            labelWidth=50, orientation=Qt.Horizontal, sendSelectedValue=True,\n            contentsLength=12, searchable=True\n        )\n        self.attr_box = gui.vBox(self.controlArea, 'Axes',\n                                 spacing=2 if gui.is_macstyle() else 8)\n        dmod = DomainModel\n        self.xy_model = DomainModel(dmod.MIXED, valid_types=dmod.PRIMITIVE)\n        self.cb_attr_x = gui.comboBox(\n            self.attr_box, self, \"attr_x\", label=\"Axis x:\",\n            callback=self.set_attr_from_combo,\n            model=self.xy_model, **common_options,\n        )\n        self.cb_attr_y = gui.comboBox(\n            self.attr_box, self, \"attr_y\", label=\"Axis y:\",\n            callback=self.set_attr_from_combo,\n            model=self.xy_model, **common_options,\n        )\n        vizrank_box = gui.hBox(self.attr_box)\n        self.vizrank, self.vizrank_button = ScatterPlotVizRank.add_vizrank(\n            vizrank_box, self, \"Find Informative Projections\", self.set_attr)\n\n    def _add_controls_sampling(self):\n        self.sampling = gui.auto_commit(\n            self.controlArea, self, \"auto_sample\", \"Sample\", box=\"Sampling\",\n            callback=self.switch_sampling, commit=lambda: self.add_data(1))\n        self.sampling.setVisible(False)\n\n    @property\n    def effective_variables(self):\n        return [self.attr_x, self.attr_y] if self.attr_x and self.attr_y else []\n\n    @property\n    def effective_data(self):\n        eff_var = self.effective_variables\n        if eff_var and self.attr_x.name == self.attr_y.name:\n            eff_var = [self.attr_x]\n        return self.data.transform(Domain(eff_var))\n\n    def _vizrank_color_change(self):\n        self.vizrank.initialize()\n        err_msg = \"\"\n        if self.data is None:\n            err_msg = \"No data on input\"\n        elif self.data.is_sparse():\n            err_msg = \"Data is sparse\"\n        elif len(self.xy_model) < 3:\n            err_msg = \"Not enough features for ranking\"\n        elif self.attr_color is None:\n            err_msg = \"Color variable is not selected\"\n        elif np.isnan(self.data.get_column_view(\n                self.attr_color)[0].astype(float)).all():\n            err_msg = \"Color variable has no values\"\n        self.vizrank_button.setEnabled(not err_msg)\n        self.vizrank_button.setToolTip(err_msg)\n\n    @OWDataProjectionWidget.Inputs.data\n    def set_data(self, data):\n        super().set_data(data)\n        self._vizrank_color_change()\n\n        def findvar(name, iterable):\n            \"\"\"Find a Orange.data.Variable in `iterable` by name\"\"\"\n            for el in iterable:\n                if isinstance(el, Variable) and el.name == name:\n                    return el\n            return None\n\n        # handle restored settings from  < 3.3.9 when attr_* were stored\n        # by name\n        if isinstance(self.attr_x, str):\n            self.attr_x = findvar(self.attr_x, self.xy_model)\n        if isinstance(self.attr_y, str):\n            self.attr_y = findvar(self.attr_y, self.xy_model)\n        if isinstance(self.attr_label, str):\n            self.attr_label = findvar(self.attr_label, self.gui.label_model)\n        if isinstance(self.attr_color, str):\n            self.attr_color = findvar(self.attr_color, self.gui.color_model)\n        if isinstance(self.attr_shape, str):\n            self.attr_shape = findvar(self.attr_shape, self.gui.shape_model)\n        if isinstance(self.attr_size, str):\n            self.attr_size = findvar(self.attr_size, self.gui.size_model)\n\n    def check_data(self):\n        super().check_data()\n        self.__timer.stop()\n        self.sampling.setVisible(False)\n        self.sql_data = None\n        if isinstance(self.data, SqlTable):\n            if self.data.approx_len() < 4000:\n                self.data = Table(self.data)\n            else:\n                self.Information.sampled_sql()\n                self.sql_data = self.data\n                data_sample = self.data.sample_time(0.8, no_cache=True)\n                data_sample.download_data(2000, partial=True)\n                self.data = Table(data_sample)\n                self.sampling.setVisible(True)\n                if self.auto_sample:\n                    self.__timer.start()\n\n        if self.data is not None and (len(self.data) == 0 or\n                                      len(self.data.domain.variables) == 0):\n            self.data = None\n\n    def get_embedding(self):\n        self.valid_data = None\n        if self.data is None:\n            return None\n\n        x_data = self.get_column(self.attr_x, filter_valid=False)\n        y_data = self.get_column(self.attr_y, filter_valid=False)\n        if x_data is None or y_data is None:\n            return None\n\n        self.Warning.missing_coords.clear()\n        self.Information.missing_coords.clear()\n        self.valid_data = np.isfinite(x_data) & np.isfinite(y_data)\n        if self.valid_data is not None and not np.all(self.valid_data):\n            msg = self.Information if np.any(self.valid_data) else self.Warning\n            msg.missing_coords(self.attr_x.name, self.attr_y.name)\n        return np.vstack((x_data, y_data)).T\n\n    # Tooltip\n    def _point_tooltip(self, point_id, skip_attrs=()):\n        point_data = self.data[point_id]\n        xy_attrs = (self.attr_x, self.attr_y)\n        text = \"<br/>\".join(\n            escape('{} = {}'.format(var.name, point_data[var]))\n            for var in xy_attrs)\n        if self.tooltip_shows_all:\n            others = super()._point_tooltip(point_id, skip_attrs=xy_attrs)\n            if others:\n                text = \"<b>{}</b><br/><br/>{}\".format(text, others)\n        return text\n\n    def can_draw_regresssion_line(self):\n        return self.data is not None and \\\n               self.data.domain is not None and \\\n               self.attr_x.is_continuous and \\\n               self.attr_y.is_continuous\n\n    def add_data(self, time=0.4):\n        if self.data and len(self.data) > 2000:\n            self.__timer.stop()\n            return\n        data_sample = self.sql_data.sample_time(time, no_cache=True)\n        if data_sample:\n            data_sample.download_data(2000, partial=True)\n            data = Table(data_sample)\n            self.data = Table.concatenate((self.data, data), axis=0)\n            self.handleNewSignals()\n\n    def init_attr_values(self):\n        super().init_attr_values()\n        data = self.data\n        domain = data.domain if data and len(data) else None\n        self.xy_model.set_domain(domain)\n        self.attr_x = self.xy_model[0] if self.xy_model else None\n        self.attr_y = self.xy_model[1] if len(self.xy_model) >= 2 \\\n            else self.attr_x\n\n    def switch_sampling(self):\n        self.__timer.stop()\n        if self.auto_sample and self.sql_data:\n            self.add_data()\n            self.__timer.start()\n\n    @OWDataProjectionWidget.Inputs.data_subset\n    def set_subset_data(self, subset_data):\n        self.warning()\n        if isinstance(subset_data, SqlTable):\n            if subset_data.approx_len() < AUTO_DL_LIMIT:\n                subset_data = Table(subset_data)\n            else:\n                self.warning(\"Data subset does not support large Sql tables\")\n                subset_data = None\n        super().set_subset_data(subset_data)\n\n    # called when all signals are received, so the graph is updated only once\n    def handleNewSignals(self):\n        self.attr_box.setEnabled(True)\n        self.vizrank.setEnabled(True)\n        if self.attribute_selection_list and self.data is not None and \\\n                self.data.domain is not None and \\\n                all(attr in self.data.domain for attr\n                        in self.attribute_selection_list):\n            self.attr_x, self.attr_y = self.attribute_selection_list[:2]\n            self.attr_box.setEnabled(False)\n            self.vizrank.setEnabled(False)\n        super().handleNewSignals()\n        if self._domain_invalidated:\n            self.graph.update_axes()\n            self._domain_invalidated = False\n        self.cb_reg_line.setEnabled(self.can_draw_regresssion_line())\n\n    @Inputs.features\n    def set_shown_attributes(self, attributes):\n        if attributes and len(attributes) >= 2:\n            self.attribute_selection_list = attributes[:2]\n            self._invalidated = self._invalidated \\\n                or self.attr_x != attributes[0] \\\n                or self.attr_y != attributes[1]\n        else:\n            self.attribute_selection_list = None\n\n    def set_attr(self, attr_x, attr_y):\n        if attr_x != self.attr_x or attr_y != self.attr_y:\n            self.attr_x, self.attr_y = attr_x, attr_y\n            self.attr_changed()\n\n    def set_attr_from_combo(self):\n        self.attr_changed()\n        self.xy_changed_manually.emit(self.attr_x, self.attr_y)\n\n    def attr_changed(self):\n        self.cb_reg_line.setEnabled(self.can_draw_regresssion_line())\n        self.setup_plot()\n        self.commit.deferred()\n\n    def get_axes(self):\n        return {\"bottom\": self.attr_x, \"left\": self.attr_y}\n\n    def colors_changed(self):\n        super().colors_changed()\n        self._vizrank_color_change()\n\n    @gui.deferred\n    def commit(self):\n        super().commit()\n        self.send_features()\n\n    def send_features(self):\n        features = [attr for attr in [self.attr_x, self.attr_y] if attr]\n        self.Outputs.features.send(AttributeList(features) or None)\n\n    def get_widget_name_extension(self):\n        if self.data is not None:\n            return \"{} vs {}\".format(self.attr_x.name, self.attr_y.name)\n        return None\n\n    def _get_send_report_caption(self):\n        return report.render_items_vert((\n            (\"Color\", self._get_caption_var_name(self.attr_color)),\n            (\"Label\", self._get_caption_var_name(self.attr_label)),\n            (\"Shape\", self._get_caption_var_name(self.attr_shape)),\n            (\"Size\", self._get_caption_var_name(self.attr_size)),\n            (\"Jittering\", (self.attr_x.is_discrete or\n                           self.attr_y.is_discrete or\n                           self.graph.jitter_continuous) and\n             self.graph.jitter_size)))\n\n    @classmethod\n    def migrate_settings(cls, settings, version):\n        if version < 2 and \"selection\" in settings and settings[\"selection\"]:\n            settings[\"selection_group\"] = [(a, 1) for a in settings[\"selection\"]]\n        if version < 3:\n            if \"auto_send_selection\" in settings:\n                settings[\"auto_commit\"] = settings[\"auto_send_selection\"]\n            if \"selection_group\" in settings:\n                settings[\"selection\"] = settings[\"selection_group\"]\n        if version < 5:\n            if \"graph\" in settings and \\\n                    \"jitter_continuous\" not in settings[\"graph\"]:\n                settings[\"graph\"][\"jitter_continuous\"] = True\n\n    @classmethod\n    def migrate_context(cls, context, version):\n        values = context.values\n        if version < 3:\n            values[\"attr_color\"] = values[\"graph\"][\"attr_color\"]\n            values[\"attr_size\"] = values[\"graph\"][\"attr_size\"]\n            values[\"attr_shape\"] = values[\"graph\"][\"attr_shape\"]\n            values[\"attr_label\"] = values[\"graph\"][\"attr_label\"]\n        if version < 4:\n            if values[\"attr_x\"][1] % 100 == 1 or values[\"attr_y\"][1] % 100 == 1:\n                raise IncompatibleContext()",
                            "ast_type_str": "ClassDef",
                            "start_lineno": 314,
                            "end_lineno": 670,
                            "parent": null
                        }
                    ],
                    "added_entities": [],
                    "deleted_entities": []
                }
            ]
        },
        {
            "old_file_content": "# Test methods with long descriptive names can omit docstrings\n# pylint: disable=missing-docstring,too-many-public-methods,protected-access\n# pylint: disable=too-many-lines\nfrom unittest.mock import MagicMock, patch, Mock\nimport numpy as np\n\nfrom AnyQt.QtCore import QRectF, Qt\nfrom AnyQt.QtWidgets import QToolTip\nfrom AnyQt.QtGui import QColor, QFont\n\nfrom Orange.data import (\n    Table, Domain, ContinuousVariable, DiscreteVariable, TimeVariable\n)\nfrom Orange.widgets.tests.base import (\n    WidgetTest, WidgetOutputsTestMixin, datasets, ProjectionWidgetTestMixin\n)\nfrom Orange.widgets.tests.utils import simulate\nfrom Orange.widgets.utils.colorpalettes import DefaultRGBColors\nfrom Orange.widgets.visualize.owscatterplot import (\n    OWScatterPlot, ScatterPlotVizRank, OWScatterPlotGraph)\nfrom Orange.widgets.visualize.utils.widget import MAX_COLORS\nfrom Orange.widgets.widget import AttributeList\n\n\nclass TestOWScatterPlot(WidgetTest, ProjectionWidgetTestMixin,\n                        WidgetOutputsTestMixin):\n    @classmethod\n    def setUpClass(cls):\n        super().setUpClass()\n        WidgetOutputsTestMixin.init(cls)\n        cls.same_input_output_domain = False\n\n        cls.signal_name = \"Data\"\n        cls.signal_data = cls.data\n\n    def setUp(self):\n        self.widget = self.create_widget(OWScatterPlot)\n\n    def test_set_data(self):\n        # Connect iris to scatter plot\n        self.send_signal(self.widget.Inputs.data, self.data)\n\n        # First two attribute should be selected as x an y\n        self.assertEqual(self.widget.attr_x, self.data.domain[0])\n        self.assertEqual(self.widget.attr_y, self.data.domain[1])\n\n        # Class var should be selected as color\n        self.assertIs(self.widget.attr_color, self.data.domain.class_var)\n\n        # Change which attributes are displayed\n        self.widget.attr_x = self.data.domain[2]\n        self.widget.attr_y = self.data.domain[3]\n\n        # Disconnect the data\n        self.send_signal(self.widget.Inputs.data, None)\n\n        # removing data should have cleared attributes\n        self.assertIsNone(self.widget.attr_x)\n        self.assertIsNone(self.widget.attr_y)\n        self.assertIsNone(self.widget.attr_color)\n\n        # and remove the legend\n        self.assertEqual(len(self.widget.graph.color_legend.items), 0)\n\n        # Connect iris again\n        # same attributes that were used last time should be selected\n        self.send_signal(self.widget.Inputs.data, self.data)\n\n        self.assertIs(self.widget.attr_x, self.data.domain[2])\n        self.assertIs(self.widget.attr_y, self.data.domain[3])\n\n    def test_score_heuristics(self):\n        domain = Domain([ContinuousVariable(c) for c in \"abcd\"],\n                        DiscreteVariable(\"e\", values=\"ab\"))\n        a = np.arange(10).reshape((10, 1))\n        data = Table(domain, np.hstack([a, a, a, a]), a >= 5)\n        self.send_signal(self.widget.Inputs.data, data)\n        vizrank = ScatterPlotVizRank(self.widget)\n        self.assertEqual([x.name for x in vizrank.score_heuristic()],\n                         list(\"abcd\"))\n\n    def test_optional_combos(self):\n        domain = self.data.domain\n        d1 = Domain(domain.attributes[:2], domain.class_var,\n                    [domain.attributes[2]])\n        t1 = self.data.transform(d1)\n        self.send_signal(self.widget.Inputs.data, t1)\n        self.widget.graph.attr_size = domain.attributes[2]\n\n        d2 = Domain(domain.attributes[:2], domain.class_var,\n                    [domain.attributes[3]])\n        t2 = self.data.transform(d2)\n        self.send_signal(self.widget.Inputs.data, t2)\n\n    def test_error_message(self):\n        \"\"\"Check if error message appears and then disappears when\n        data is removed from input\"\"\"\n        data = self.data.copy()\n        with data.unlocked():\n            data.X[:, 0] = np.nan\n        self.send_signal(self.widget.Inputs.data, data)\n        self.assertTrue(self.widget.Warning.missing_coords.is_shown())\n        self.send_signal(self.widget.Inputs.data, None)\n        self.assertFalse(self.widget.Warning.missing_coords.is_shown())\n\n    def test_report_on_empty(self):\n        self.widget.report_plot = MagicMock()\n        self.widget.report_caption = MagicMock()\n        self.widget.report_items = MagicMock()\n        self.widget.send_report()  # Essentially, don't crash\n        self.widget.report_plot.assert_not_called()\n        self.widget.report_caption.assert_not_called()\n        self.widget.report_items.assert_not_called()\n\n    def test_data_column_nans(self):\n        \"\"\"\n        ValueError cannot convert float NaN to integer.\n        In case when all column values are NaN then it throws that error.\n        GH-2061\n        \"\"\"\n        table = datasets.data_one_column_nans()\n        self.send_signal(self.widget.Inputs.data, table)\n        cb_attr_color = self.widget.controls.attr_color\n        simulate.combobox_activate_item(cb_attr_color, \"b\")\n        simulate.combobox_activate_item(self.widget.cb_attr_x, \"a\")\n        simulate.combobox_activate_item(self.widget.cb_attr_y, \"a\")\n\n        #self.widget.update_graph()\n        self.widget.graph.reset_graph()\n\n    def test_data_column_infs(self):\n        \"\"\"\n        Scatter Plot should not crash on data with infinity values\n        GH-2707\n        GH-2684\n        \"\"\"\n        table = datasets.data_one_column_infs()\n        self.send_signal(self.widget.Inputs.data, table)\n        attr_x = self.widget.controls.attr_x\n        simulate.combobox_activate_item(attr_x, \"b\")\n\n    def test_regression_line(self):\n        \"\"\"It is possible to draw the line only for pair of continuous attrs\"\"\"\n        self.send_signal(self.widget.Inputs.data, self.data)\n        self.assertTrue(self.widget.cb_reg_line.isEnabled())\n        self.assertIsNone(self.widget.graph.reg_line_item)\n        self.widget.cb_reg_line.setChecked(True)\n        self.assertIsNotNone(self.widget.graph.reg_line_item)\n        self.widget.cb_attr_y.activated.emit(4)\n        self.widget.cb_attr_y.setCurrentIndex(4)\n        self.assertFalse(self.widget.cb_reg_line.isEnabled())\n        self.assertIsNone(self.widget.graph.reg_line_item)\n\n    def test_points_combo_boxes(self):\n        \"\"\"Check Point box combo models and values\"\"\"\n        self.send_signal(self.widget.Inputs.data, self.data)\n        controls = self.widget.controls\n\n        # color and label should contain all variables\n        # size should contain only continuous variables\n        # shape should contain only discrete variables\n        for var in self.data.domain.variables + self.data.domain.metas:\n            self.assertIn(var, controls.attr_color.model())\n            self.assertIn(var, controls.attr_label.model())\n            if var.is_continuous:\n                self.assertIn(var, controls.attr_size.model())\n                self.assertNotIn(var, controls.attr_shape.model())\n            if var.is_discrete:\n                self.assertNotIn(var, controls.attr_size.model())\n                self.assertIn(var, controls.attr_shape.model())\n\n        widget = self.create_widget(OWScatterPlot)\n        self.send_signal(self.widget.Inputs.data, self.data, widget=widget)\n        self.assertEqual(controls.attr_color.currentText(),\n                         self.data.domain.class_var.name)\n\n    def test_group_selections(self):\n        self.send_signal(self.widget.Inputs.data, self.data)\n        graph = self.widget.graph\n        points = graph.scatterplot_item.points()\n        sel_column = np.zeros((len(self.data), 1))\n\n        x = self.data.X\n\n        def selectedx():\n            return self.get_output(self.widget.Outputs.selected_data).X\n\n        def selected_groups():\n            return self.get_output(self.widget.Outputs.selected_data).metas[:, 0]\n\n        def annotated():\n            return self.get_output(self.widget.Outputs.annotated_data).metas\n\n        def annotations():\n            return self.get_output(self.widget.Outputs.annotated_data).domain.metas[0].values\n\n        # Select 0:5\n        graph.select(points[:5])\n        np.testing.assert_equal(selectedx(), x[:5])\n        np.testing.assert_equal(selected_groups(), np.zeros(5))\n        sel_column[:5] = 1\n        np.testing.assert_equal(annotated(), sel_column)\n        self.assertEqual(annotations(), (\"No\", \"Yes\", ))\n\n        # Shift-select 5:10; now we have groups 0:5 and 5:10\n        with self.modifiers(Qt.ShiftModifier):\n            graph.select(points[5:10])\n        np.testing.assert_equal(selectedx(), x[:10])\n        np.testing.assert_equal(selected_groups(), np.array([0] * 5 + [1] * 5))\n        sel_column[:5] = 0\n        sel_column[5:10] = 1\n        sel_column[10:] = 2\n        np.testing.assert_equal(annotated(), sel_column)\n        self.assertEqual(len(annotations()), 3)\n\n        # Select: 15:20; we have 15:20\n        graph.select(points[15:20])\n        sel_column = np.zeros((len(self.data), 1))\n        sel_column[15:20] = 1\n        np.testing.assert_equal(selectedx(), x[15:20])\n        np.testing.assert_equal(selected_groups(), np.zeros(5))\n        self.assertEqual(annotations(), (\"No\", \"Yes\"))\n\n        # Alt-select (remove) 10:17; we have 17:20\n        with self.modifiers(Qt.AltModifier):\n            graph.select(points[10:17])\n        np.testing.assert_equal(selectedx(), x[17:20])\n        np.testing.assert_equal(selected_groups(), np.zeros(3))\n        sel_column[15:17] = 0\n        np.testing.assert_equal(annotated(), sel_column)\n        self.assertEqual(annotations(), (\"No\", \"Yes\"))\n\n        # Ctrl-Shift-select (add-to-last) 10:17; we have 17:25\n        with self.modifiers(Qt.ShiftModifier | Qt.ControlModifier):\n            graph.select(points[20:25])\n        np.testing.assert_equal(selectedx(), x[17:25])\n        np.testing.assert_equal(selected_groups(), np.zeros(8))\n        sel_column[20:25] = 1\n        np.testing.assert_equal(annotated(), sel_column)\n        self.assertEqual(annotations(), (\"No\", \"Yes\"))\n\n        # Shift-select (add) 30:35; we have 17:25, 30:35\n        with self.modifiers(Qt.ShiftModifier):\n            graph.select(points[30:35])\n        # ... then Ctrl-Shift-select (add-to-last) 10:17; we have 17:25, 30:40\n        with self.modifiers(Qt.ShiftModifier | Qt.ControlModifier):\n            graph.select(points[35:40])\n        sel_column[:] = 2\n        sel_column[17:25] = 0\n        sel_column[30:40] = 1\n        np.testing.assert_equal(selected_groups(), np.array([0] * 8 + [1] * 10))\n        np.testing.assert_equal(annotated(), sel_column)\n        self.assertEqual(len(annotations()), 3)\n\n    def test_saving_selection(self):\n        self.send_signal(self.widget.Inputs.data, self.data)  # iris\n        self.widget.graph.select_by_rectangle(QRectF(4, 3, 3, 1))\n        selected_inds = np.flatnonzero(self.widget.graph.selection)\n        settings = self.widget.settingsHandler.pack_data(self.widget)\n        np.testing.assert_equal(selected_inds,\n                                [i for i, g in settings[\"selection\"]])\n\n    def test_points_selection(self):\n        # Opening widget with saved selection should restore it\n        self.widget = self.create_widget(\n            OWScatterPlot, stored_settings={\n                \"selection_group\": [(i, 1) for i in range(50)]}\n        )\n        self.send_signal(self.widget.Inputs.data, self.data)  # iris\n        selected_data = self.get_output(self.widget.Outputs.selected_data)\n        self.assertEqual(len(selected_data), 50)\n\n        # Changing the dataset should clear selection\n        titanic = Table(\"titanic\")\n        self.send_signal(self.widget.Inputs.data, titanic)\n        selected_data = self.get_output(self.widget.Outputs.selected_data)\n        self.assertIsNone(selected_data)\n\n    def test_migrate_selection(self):\n        settings = dict(selection=list(range(2)))\n        OWScatterPlot.migrate_settings(settings, 0)\n        self.assertEqual(settings[\"selection_group\"], [(0, 1), (1, 1)])\n\n    def test_invalid_points_selection(self):\n        # if selection contains rows that are not present in the current\n        # dataset, widget should select what can be selected.\n        self.widget = self.create_widget(\n            OWScatterPlot, stored_settings={\n                \"selection_group\": [(i, 1) for i in range(50)]}\n        )\n        data = self.data[:11].copy()\n        with data.unlocked():\n            data[0, 0] = np.nan\n        self.send_signal(self.widget.Inputs.data, data)\n        self.assertIsNone(self.get_output(self.widget.Outputs.selected_data))\n\n    def test_set_strings_settings(self):\n        \"\"\"\n        Test if settings can be loaded as strings and successfully put\n        in new owplotgui combos.\n        \"\"\"\n        self.send_signal(self.widget.Inputs.data, self.data)\n        settings = self.widget.settingsHandler.pack_data(self.widget)\n        plot_settings = settings[\"context_settings\"][0].values\n        plot_settings[\"attr_label\"] = (\"sepal length\", -2)\n        plot_settings[\"attr_color\"] = (\"sepal width\", -2)\n        plot_settings[\"attr_shape\"] = (\"iris\", -2)\n        plot_settings[\"attr_size\"] = (\"petal width\", -2)\n        w = self.create_widget(OWScatterPlot, stored_settings=settings)\n        self.send_signal(self.widget.Inputs.data, self.data, widget=w)\n        self.assertEqual(w.attr_label.name, \"sepal length\")\n        self.assertEqual(w.attr_color.name, \"sepal width\")\n        self.assertEqual(w.attr_shape.name, \"iris\")\n        self.assertEqual(w.attr_size.name, \"petal width\")\n\n    def test_features_and_no_data(self):\n        \"\"\"\n        Prevent crashing when features are sent but no data.\n        \"\"\"\n        domain = Table(\"iris\").domain\n        self.send_signal(self.widget.Inputs.features,\n                         AttributeList(domain.variables))\n        self.send_signal(self.widget.Inputs.features, None)\n\n    def test_features_and_data(self):\n        self.assertTrue(self.widget.attr_box.isEnabled())\n        self.send_signal(self.widget.Inputs.data, self.data)\n        x, y = self.widget.graph.scatterplot_item.getData()\n        np.testing.assert_array_equal(x, self.data.X[:, 0])\n        np.testing.assert_array_equal(y, self.data.X[:, 1])\n        self.send_signal(self.widget.Inputs.features,\n                         AttributeList(self.data.domain[2:]))\n        self.assertIs(self.widget.attr_x, self.data.domain[2])\n        self.assertIs(self.widget.attr_y, self.data.domain[3])\n        self.assertFalse(self.widget.attr_box.isEnabled())\n        self.assertFalse(self.widget.vizrank.isEnabled())\n        x, y = self.widget.graph.scatterplot_item.getData()\n        np.testing.assert_array_equal(x, self.data.X[:, 2])\n        np.testing.assert_array_equal(y, self.data.X[:, 3])\n\n        self.send_signal(self.widget.Inputs.data, None)\n        self.send_signal(self.widget.Inputs.data, self.data)\n        self.assertIs(self.widget.attr_x, self.data.domain[2])\n        self.assertIs(self.widget.attr_y, self.data.domain[3])\n        self.assertFalse(self.widget.attr_box.isEnabled())\n        self.assertFalse(self.widget.vizrank.isEnabled())\n\n        self.send_signal(self.widget.Inputs.features, None)\n        self.assertTrue(self.widget.attr_box.isEnabled())\n        self.assertTrue(self.widget.vizrank.isEnabled())\n\n    def test_output_features(self):\n        data = Table(\"iris\")\n        self.send_signal(self.widget.Inputs.data, data)\n\n        # This doesn't work because combo's callbacks are connected to signal\n        # `activated`, which is only triggered by user interaction, and not to\n        # `currentIndexChanged`\n        # combo_y = self.widget.controls.attr_y\n        # combo_y.setCurrentIndex(combo_y.model().indexOf(data.domain[3]))\n        # This is a workaround\n        self.widget.attr_y = data.domain[3]\n        self.widget.attr_changed()\n\n        features = self.get_output(self.widget.Outputs.features)\n        self.assertEqual(features, [data.domain[0], data.domain[3]])\n\n    def test_vizrank(self):\n        data = Table(\"iris\")\n        self.send_signal(self.widget.Inputs.data, data)\n        vizrank = ScatterPlotVizRank(self.widget)\n        n_states = len(data.domain.attributes)\n        n_states = n_states * (n_states - 1) / 2\n        states = [state for state in vizrank.iterate_states(None)]\n        self.assertEqual(len(states), n_states)\n        self.assertEqual(len(set(states)), n_states)\n        self.assertIsNotNone(vizrank.compute_score(states[0]))\n        self.send_signal(self.widget.Inputs.data, data[:9])\n        self.assertIsNone(vizrank.compute_score(states[0]))\n\n        data = Table(\"housing\")[::10]\n        self.send_signal(self.widget.Inputs.data, data)\n        vizrank = ScatterPlotVizRank(self.widget)\n        states = [state for state in vizrank.iterate_states(None)]\n        self.assertIsNotNone(vizrank.compute_score(states[0]))\n\n    def test_vizrank_class_nan(self):\n        \"\"\"\n        When class values are nan, vizrank should be disabled. It should behave like\n        the class column is missing.\n        GH-2757\n        \"\"\"\n        def assert_vizrank_enabled(data, is_enabled):\n            self.send_signal(self.widget.Inputs.data, data)\n            self.assertEqual(is_enabled, self.widget.vizrank_button.isEnabled())\n\n        data1 = Table(\"iris\")[::30]\n        data2 = Table(\"iris\")[::30].copy()\n        with data2.unlocked():\n            data2.Y[:] = np.nan\n        domain = Domain(\n            attributes=data2.domain.attributes[:4], class_vars=DiscreteVariable(\"iris\", values=()))\n        data2 = Table(domain, data2.X, Y=data2.Y)\n        data3 = Table(\"iris\")[::30].copy()\n        with data3.unlocked():\n            data3.Y[:] = np.nan\n\n        for data, is_enabled in zip([data1, data2, data1, data3, data1],\n                                    [True, False, True, False, True]):\n            assert_vizrank_enabled(data, is_enabled)\n\n    def test_vizrank_nonprimitives(self):\n        \"\"\"VizRank does not try to include non primitive attributes\"\"\"\n        data = Table(\"zoo\")\n        self.send_signal(self.widget.Inputs.data, data)\n        with patch(\"Orange.widgets.visualize.owscatterplot.ReliefF\",\n                   new=lambda *_1, **_2: lambda data: np.arange(len(data))):\n            self.widget.vizrank.score_heuristic()\n\n    def test_vizrank_enabled(self):\n        self.send_signal(self.widget.Inputs.data, self.data)\n        self.assertTrue(self.widget.vizrank_button.isEnabled())\n        self.assertEqual(self.widget.vizrank_button.toolTip(), \"\")\n        self.assertTrue(self.widget.vizrank.button.isEnabled())\n        self.widget.vizrank.button.click()\n\n    def test_vizrank_enabled_no_data(self):\n        self.send_signal(self.widget.Inputs.data, None)\n        self.assertFalse(self.widget.vizrank_button.isEnabled())\n        self.assertEqual(self.widget.vizrank_button.toolTip(), \"No data on input\")\n\n    def test_vizrank_enabled_sparse_data(self):\n        self.send_signal(self.widget.Inputs.data, self.data.to_sparse())\n        self.assertFalse(self.widget.vizrank_button.isEnabled())\n        self.assertEqual(self.widget.vizrank_button.toolTip(), \"Data is sparse\")\n\n    def test_vizrank_enabled_constant_data(self):\n        domain = Domain([ContinuousVariable(\"c1\"),\n                         ContinuousVariable(\"c2\"),\n                         ContinuousVariable(\"c3\"),\n                         ContinuousVariable(\"c4\")],\n                        DiscreteVariable(\"cls\", values=(\"a\", \"b\")))\n        X = np.zeros((10, 4))\n        table = Table(domain, X, np.random.randint(2, size=10))\n        self.send_signal(self.widget.Inputs.data, table)\n        self.assertEqual(self.widget.vizrank_button.toolTip(), \"\")\n        self.assertTrue(self.widget.vizrank_button.isEnabled())\n        self.assertTrue(self.widget.vizrank.button.isEnabled())\n        self.widget.vizrank.button.click()\n\n    def test_vizrank_enabled_two_features(self):\n        self.send_signal(self.widget.Inputs.data, self.data[:, :2])\n        self.assertFalse(self.widget.vizrank_button.isEnabled())\n        self.assertEqual(self.widget.vizrank_button.toolTip(),\n                         \"Not enough features for ranking\")\n\n    def test_vizrank_enabled_no_color_var(self):\n        self.send_signal(self.widget.Inputs.data, self.data[:, :3])\n        self.assertFalse(self.widget.vizrank_button.isEnabled())\n        self.assertEqual(self.widget.vizrank_button.toolTip(),\n                         \"Color variable is not selected\")\n\n    def test_vizrank_enabled_color_var_nans(self):\n        domain = Domain([ContinuousVariable(\"c1\"),\n                         ContinuousVariable(\"c2\"),\n                         ContinuousVariable(\"c3\"),\n                         ContinuousVariable(\"c4\")],\n                        DiscreteVariable(\"cls\", values=(\"a\", \"b\")))\n        table = Table(domain, np.random.random((10, 4)), np.full(10, np.nan))\n        self.send_signal(self.widget.Inputs.data, table)\n        self.assertFalse(self.widget.vizrank_button.isEnabled())\n        self.assertEqual(self.widget.vizrank_button.toolTip(),\n                         \"Color variable has no values\")\n\n    def test_auto_send_selection(self):\n        \"\"\"\n        Scatter Plot automatically sends selection only when the checkbox Send automatically\n        is checked.\n        GH-2649\n        GH-2646\n        \"\"\"\n        data = Table(\"iris\")\n        self.send_signal(self.widget.Inputs.data, data)\n        self.widget.controls.auto_commit.setChecked(False)\n        self.assertFalse(self.widget.controls.auto_commit.isChecked())\n        self._select_data()\n        self.assertIsNone(self.get_output(self.widget.Outputs.selected_data))\n        self.widget.controls.auto_commit.setChecked(True)\n        output = self.get_output(self.widget.Outputs.selected_data)\n        self.assertIsInstance(output, Table)\n\n    def test_color_is_optional(self):\n        zoo = Table(\"zoo\")\n        backbone, breathes, airborne, type = \\\n            [zoo.domain[x] for x in [\"backbone\", \"breathes\", \"airborne\", \"type\"]]\n        default_x, default_y, default_color = \\\n            zoo.domain[0], zoo.domain[1], zoo.domain.class_var\n        attr_x = self.widget.controls.attr_x\n        attr_y = self.widget.controls.attr_y\n        attr_color = self.widget.controls.attr_color\n\n        # Send dataset, ensure defaults are what we expect them to be\n        self.send_signal(self.widget.Inputs.data, zoo)\n        self.assertEqual(attr_x.currentText(), default_x.name)\n        self.assertEqual(attr_y.currentText(), default_y.name)\n        self.assertEqual(attr_color.currentText(), default_color.name)\n        # Select different values\n        simulate.combobox_activate_item(attr_x, backbone.name)\n        simulate.combobox_activate_item(attr_y, breathes.name)\n        simulate.combobox_activate_item(attr_color, airborne.name)\n\n        # Send compatible dataset, values should not change\n        zoo2 = zoo[:, (backbone, breathes, airborne, type)]\n        self.send_signal(self.widget.Inputs.data, zoo2)\n        self.assertEqual(attr_x.currentText(), backbone.name)\n        self.assertEqual(attr_y.currentText(), breathes.name)\n        self.assertEqual(attr_color.currentText(), airborne.name)\n\n        # Send dataset without color variable\n        # x and y should remain, color reset to default\n        zoo3 = zoo[:, (backbone, breathes, type)]\n        self.send_signal(self.widget.Inputs.data, zoo3)\n        self.assertEqual(attr_x.currentText(), backbone.name)\n        self.assertEqual(attr_y.currentText(), breathes.name)\n        self.assertEqual(attr_color.currentText(), default_color.name)\n\n        # Send dataset without x\n        # y and color should be the same as with zoo\n        zoo4 = zoo[:, (default_x, default_y, breathes, airborne, type)]\n        self.send_signal(self.widget.Inputs.data, zoo4)\n        self.assertEqual(attr_x.currentText(), default_x.name)\n        self.assertEqual(attr_y.currentText(), default_y.name)\n        self.assertEqual(attr_color.currentText(), default_color.name)\n\n        # Send dataset compatible with zoo2 and zoo3\n        # Color should reset to one in zoo3, as it was used more\n        # recently\n        zoo5 = zoo[:, (default_x, backbone, breathes, airborne, type)]\n        self.send_signal(self.widget.Inputs.data, zoo5)\n        self.assertEqual(attr_x.currentText(), backbone.name)\n        self.assertEqual(attr_y.currentText(), breathes.name)\n        self.assertEqual(attr_color.currentText(), type.name)\n\n    def test_handle_metas(self):\n        \"\"\"\n        Scatter Plot Graph can handle metas\n        GH-2699\n        \"\"\"\n        w = self.widget\n        data = Table(\"iris\")\n        domain = Domain(\n            attributes=data.domain.attributes[:2],\n            class_vars=data.domain.class_vars,\n            metas=data.domain.attributes[2:]\n        )\n        data = data.transform(domain).copy()\n        # Sometimes floats in metas are saved as objects\n        with data.unlocked():\n            data.metas = data.metas.astype(object)\n        self.send_signal(w.Inputs.data, data)\n        simulate.combobox_activate_item(w.cb_attr_x, data.domain.metas[1].name)\n        simulate.combobox_activate_item(w.controls.attr_color, data.domain.metas[0].name)\n        w.graph.reset_graph()\n\n    def test_subset_data(self):\n        \"\"\"\n        Scatter Plot subset data is sent to Scatter Plot Graph\n        GH-2773\n        \"\"\"\n        data = Table(\"iris\")\n        w = self.widget\n        self.send_signal(w.Inputs.data, data)\n        self.send_signal(w.Inputs.data_subset, data[::30])\n        self.assertEqual(len(w.subset_indices), 5)\n\n    def test_opacity_warning(self):\n        data = Table(\"iris\")\n        w = self.widget\n        self.send_signal(w.Inputs.data, data)\n        w.graph.controls.alpha_value.setSliderPosition(10)\n        self.assertFalse(w.Warning.transparent_subset.is_shown())\n        self.send_signal(w.Inputs.data_subset, data[::30])\n        self.assertTrue(w.Warning.transparent_subset.is_shown())\n        w.graph.controls.alpha_value.setSliderPosition(200)\n        self.assertFalse(w.Warning.transparent_subset.is_shown())\n        w.graph.controls.alpha_value.setSliderPosition(10)\n        self.assertTrue(w.Warning.transparent_subset.is_shown())\n        self.send_signal(w.Inputs.data_subset, None)\n        self.assertFalse(w.Warning.transparent_subset.is_shown())\n\n    def test_jittering(self):\n        self.send_signal(self.widget.Inputs.data, self.data)\n        self.widget.graph.controls.jitter_continuous.setChecked(True)\n        self.widget.graph.controls.jitter_size.setValue(1)\n\n    def test_metas_zero_column(self):\n        \"\"\"\n        Prevent crash when metas column is zero.\n        GH-2775\n        \"\"\"\n        data = Table(\"iris\")\n        domain = data.domain\n        domain = Domain(domain.attributes[:3], domain.class_vars, domain.attributes[3:])\n        data = data.transform(domain).copy()\n        with data.unlocked():\n            data.metas[:, 0] = 0\n        w = self.widget\n        self.send_signal(w.Inputs.data, data)\n        simulate.combobox_activate_item(w.controls.attr_x, domain.metas[0].name)\n\n    def test_tooltip(self):\n        # The test tests presence of some data,\n        # but avoids checking the exact format\n        data = Table(\"heart_disease\")\n        self.send_signal(self.widget.Inputs.data, data)\n        widget = self.widget\n        graph = widget.graph\n        scatterplot_item = graph.scatterplot_item\n\n        widget.controls.attr_x = data.domain[\"chest pain\"]\n        widget.controls.attr_y = data.domain[\"cholesterol\"]\n        all_points = scatterplot_item.points()\n\n        event = MagicMock()\n        with patch.object(scatterplot_item, \"mapFromScene\"), \\\n                patch.object(QToolTip, \"showText\") as show_text:\n\n            # Single point hovered\n            with patch.object(scatterplot_item, \"pointsAt\",\n                              return_value=[all_points[42]]):\n                # Show just x and y attribute\n                widget.tooltip_shows_all = False\n                self.assertTrue(graph.help_event(event))\n                (_, text), _ = show_text.call_args\n                self.assertIn(\"age = {}\".format(data[42, \"age\"]), text)\n                self.assertIn(\"gender = {}\".format(data[42, \"gender\"]), text)\n                self.assertNotIn(\"max HR = {}\".format(data[42, \"max HR\"]), text)\n                self.assertNotIn(\"others\", text)\n\n                # Show all attributes\n                widget.tooltip_shows_all = True\n                self.assertTrue(graph.help_event(event))\n                (_, text), _ = show_text.call_args\n                self.assertIn(\"age = {}\".format(data[42, \"age\"]), text)\n                self.assertIn(\"gender = {}\".format(data[42, \"gender\"]), text)\n                self.assertIn(\"max HR = {}\".format(data[42, \"max HR\"]), text)\n                self.assertIn(\"... and 4 others\", text)\n\n            # Two points hovered\n            with patch.object(scatterplot_item, \"pointsAt\",\n                              return_value=[all_points[42], all_points[100]]):\n                self.assertTrue(graph.help_event(event))\n                (_, text), _ = show_text.call_args\n                self.assertIn(\"age = {}\".format(data[42, \"age\"]), text)\n                self.assertIn(\"gender = {}\".format(data[42, \"gender\"]), text)\n                self.assertIn(\"age = {}\".format(data[100, \"age\"]), text)\n                self.assertIn(\"gender = {}\".format(data[100, \"gender\"]), text)\n\n            # No points hovered\n            with patch.object(scatterplot_item, \"pointsAt\",\n                              return_value=[]):\n                show_text.reset_mock()\n                self.assertFalse(graph.help_event(event))\n                self.assertEqual(show_text.call_count, 0)\n\n    def test_many_discrete_values(self):\n        \"\"\"\n        Do not show all discrete values if there are too many.\n        Also test for values with a nan.\n        GH-2804\n        \"\"\"\n        def prepare_data():\n            data = Table(\"iris\")\n            values = list(range(15))\n            class_var = DiscreteVariable(\"iris5\", values=[str(v) for v in values])\n            data = data.transform(\n                Domain(attributes=data.domain.attributes,\n                       class_vars=[class_var])).copy()\n            with data.unlocked():\n                data.Y = np.array(values * 10, dtype=float)\n            return data\n\n        def assert_equal(data, max):\n            self.send_signal(self.widget.Inputs.data, data)\n            pen_data, brush_data = self.widget.graph.get_colors()\n            self.assertEqual(max, len(np.unique([id(p) for p in pen_data])), )\n\n        assert_equal(prepare_data(), MAX_COLORS)\n        # data with nan value\n        data = prepare_data()\n        with data.unlocked():\n            data.Y[42] = np.nan\n        assert_equal(data, MAX_COLORS + 1)\n\n    def test_invalidated_same_features(self):\n        self.widget.setup_plot = Mock()\n        # send data and set default features\n        self.send_signal(self.widget.Inputs.data, self.data)\n        self.widget.setup_plot.assert_called_once()\n        self.assertListEqual(self.widget.effective_variables,\n                             list(self.data.domain.attributes[:2]))\n\n        # send the same features as already set\n        self.widget.setup_plot.reset_mock()\n        self.send_signal(self.widget.Inputs.features,\n                         AttributeList(self.data.domain.attributes[:2]))\n        self.widget.setup_plot.assert_not_called()\n        self.assertListEqual(self.widget.effective_variables,\n                             list(self.data.domain.attributes[:2]))\n\n    def test_invalidated_same_time(self):\n        self.widget.setup_plot = Mock()\n        # send data and features at the same time (data first)\n        features = self.data.domain.attributes[:2]\n        signals = [(self.widget.Inputs.data, self.data),\n                   (self.widget.Inputs.features, AttributeList(features))]\n        self.send_signals(signals)\n        self.widget.setup_plot.assert_called_once()\n        self.assertListEqual(self.widget.effective_variables, list(features))\n\n    def test_invalidated_features_first(self):\n        self.widget.setup_plot = Mock()\n        # send features (same as default ones)\n        self.send_signal(self.widget.Inputs.features,\n                         AttributeList(self.data.domain.attributes[:2]))\n        self.assertListEqual(self.widget.effective_variables, [])\n        self.widget.setup_plot.assert_called_once()\n\n        # send data\n        self.widget.setup_plot.reset_mock()\n        self.send_signal(self.widget.Inputs.data, self.data)\n        self.widget.setup_plot.assert_called()\n        self.assertListEqual(self.widget.effective_variables,\n                             list(self.data.domain.attributes[:2]))\n\n    def test_invalidated_same_time_features_first(self):\n        self.widget.setup_plot = Mock()\n        # send features and data at the same time (features first)\n        features = self.data.domain.attributes[:2]\n        signals = [(self.widget.Inputs.features, AttributeList(features)),\n                   (self.widget.Inputs.data, self.data)]\n        self.send_signals(signals)\n        self.widget.setup_plot.assert_called_once()\n        self.assertListEqual(self.widget.effective_variables, list(features))\n\n    def test_invalidated_diff_features(self):\n        self.widget.setup_plot = Mock()\n        # send data and set default features\n        self.send_signal(self.widget.Inputs.data, self.data)\n        self.widget.setup_plot.assert_called_once()\n        self.assertListEqual(self.widget.effective_variables,\n                             list(self.data.domain.attributes[:2]))\n\n        # send different features\n        self.widget.setup_plot.reset_mock()\n        self.send_signal(self.widget.Inputs.features,\n                         AttributeList(self.data.domain.attributes[2:4]))\n        self.widget.setup_plot.assert_called_once()\n        self.assertListEqual(self.widget.effective_variables,\n                             list(self.data.domain.attributes[2:4]))\n\n    def test_invalidated_diff_features_same_time(self):\n        self.widget.setup_plot = Mock()\n        # send data and different features at the same time (data first)\n        features = self.data.domain.attributes[2:4]\n        signals = [(self.widget.Inputs.data, self.data),\n                   (self.widget.Inputs.features, AttributeList(features))]\n        self.send_signals(signals)\n        self.widget.setup_plot.assert_called_once()\n        self.assertListEqual(self.widget.effective_variables, list(features))\n\n    def test_invalidated_diff_features_features_first(self):\n        self.widget.setup_plot = Mock()\n        # send features (not the same as defaults)\n        self.send_signal(self.widget.Inputs.features,\n                         AttributeList(self.data.domain.attributes[2:4]))\n        self.assertListEqual(self.widget.effective_variables, [])\n        self.widget.setup_plot.assert_called_once()\n\n        # send data\n        self.widget.setup_plot.reset_mock()\n        self.send_signal(self.widget.Inputs.data, self.data)\n        self.widget.setup_plot.assert_called_once()\n        self.assertListEqual(self.widget.effective_variables,\n                             list(self.data.domain.attributes[2:4]))\n\n    def test_invalidated_diff_features_same_time_features_first(self):\n        self.widget.setup_plot = Mock()\n        # send data and different features at the same time (features first)\n        features = self.data.domain.attributes[2:4]\n        signals = [(self.widget.Inputs.features, AttributeList(features)),\n                   (self.widget.Inputs.data, self.data)]\n        self.send_signals(signals)\n        self.widget.setup_plot.assert_called_once()\n        self.assertListEqual(self.widget.effective_variables, list(features))\n\n    @patch('Orange.widgets.visualize.owscatterplot.ScatterPlotVizRank.'\n           'on_manual_change')\n    def test_vizrank_receives_manual_change(self, on_manual_change):\n        # Recreate the widget so the patch kicks in\n        self.widget = self.create_widget(OWScatterPlot)\n        data = Table(\"iris.tab\")\n        self.send_signal(self.widget.Inputs.data, data)\n        model = self.widget.controls.attr_x.model()\n        self.widget.attr_x = model[0]\n        self.widget.attr_y = model[1]\n        simulate.combobox_activate_index(self.widget.controls.attr_x, 2)\n        self.assertIs(self.widget.attr_x, model[2])\n        on_manual_change.assert_called_with(model[2], model[1])\n\n    def test_on_manual_change(self):\n        data = Table(\"iris.tab\")\n        self.send_signal(self.widget.Inputs.data, data)\n        vizrank = self.widget.vizrank\n        vizrank.toggle()\n        self.process_events(until=lambda: not vizrank.keep_running)\n\n        model = vizrank.rank_model\n        attrs = model.data(model.index(3, 0), vizrank._AttrRole)\n        vizrank.on_manual_change(*attrs)\n        selection = vizrank.rank_table.selectedIndexes()\n        self.assertEqual(len(selection), 1)\n        self.assertEqual(selection[0].row(), 3)\n\n        vizrank.on_manual_change(*attrs[::-1])\n        selection = vizrank.rank_table.selectedIndexes()\n        self.assertEqual(len(selection), 0)\n\n    def test_regression_lines_appear(self):\n        self.widget.graph.controls.show_reg_line.setChecked(True)\n        self.assertEqual(len(self.widget.graph.reg_line_items), 0)\n        self.send_signal(self.widget.Inputs.data, self.data)\n        self.assertEqual(len(self.widget.graph.reg_line_items), 4)\n        simulate.combobox_activate_index(self.widget.controls.attr_color, 0)\n        self.assertEqual(len(self.widget.graph.reg_line_items), 1)\n        data = self.data.copy()\n        with data.unlocked():\n            data[:, 0] = np.nan\n        self.send_signal(self.widget.Inputs.data, data)\n        self.assertEqual(len(self.widget.graph.reg_line_items), 0)\n\n    def test_regression_line_coeffs(self):\n        widget = self.widget\n        graph = widget.graph\n        xy = np.array([[0, 0], [1, 0], [1, 2], [2, 2],\n                       [0, 1], [1, 3], [2, 5]], dtype=float)\n        colors = np.array([0, 0, 0, 0, 1, 1, 1], dtype=float)\n        widget.get_coordinates_data = lambda: xy.T\n        widget.can_draw_regresssion_line = lambda: True\n        widget.get_color_data = lambda: colors\n        widget.is_continuous_color = lambda: False\n        graph.palette = DefaultRGBColors\n        graph.controls.show_reg_line.setChecked(True)\n\n        graph.update_regression_line()\n\n        line1 = graph.reg_line_items[1]\n        self.assertEqual(line1.pos().x(), 0)\n        self.assertEqual(line1.pos().y(), 0)\n        self.assertEqual(line1.angle, 45)\n        self.assertEqual(line1.pen.color().hue(), graph.palette[0].hue())\n\n        line2 = graph.reg_line_items[2]\n        self.assertEqual(line2.pos().x(), 0)\n        self.assertEqual(line2.pos().y(), 1)\n        self.assertAlmostEqual(line2.angle, np.degrees(np.arctan2(2, 1)))\n        self.assertEqual(line2.pen.color().hue(), graph.palette[1].hue())\n\n        graph.orthonormal_regression = True\n        graph.update_regression_line()\n\n        line1 = graph.reg_line_items[1]\n        self.assertEqual(line1.pos().x(), 0)\n        self.assertAlmostEqual(line1.pos().y(), -0.6180339887498949)\n        self.assertAlmostEqual(line1.angle, 58.28252558853899)\n        self.assertEqual(line1.pen.color().hue(), graph.palette[0].hue())\n\n        line2 = graph.reg_line_items[2]\n        self.assertEqual(line2.pos().x(), 0)\n        self.assertEqual(line2.pos().y(), 1)\n        self.assertAlmostEqual(line2.angle, np.degrees(np.arctan2(2, 1)))\n        self.assertEqual(line2.pen.color().hue(), graph.palette[1].hue())\n\n    def test_orthonormal_line(self):\n        color = QColor(1, 2, 3)\n        width = 42\n        # Normal line\n        line = OWScatterPlotGraph._orthonormal_line(\n            np.array([0, 1, 1, 2]), np.array([0, 0, 2, 2]), color, width)\n        self.assertEqual(line.pos().x(), 0)\n        self.assertAlmostEqual(line.pos().y(), -0.6180339887498949)\n        self.assertAlmostEqual(line.angle, 58.28252558853899)\n        self.assertEqual(line.pen.color(), color)\n        self.assertEqual(line.pen.width(), width)\n\n        # Normal line, negative slope\n        line = OWScatterPlotGraph._orthonormal_line(\n            np.array([1, 2, 3]), np.array([3, 2, 1]), color, width)\n        self.assertEqual(line.pos().x(), 1)\n        self.assertEqual(line.pos().y(), 3)\n        self.assertEqual(line.angle % 360, 315)\n\n        # Horizontal line\n        line = OWScatterPlotGraph._orthonormal_line(\n            np.array([10, 11, 12]), np.array([42, 42, 42]), color, width)\n        self.assertEqual(line.pos().x(), 10)\n        self.assertEqual(line.pos().y(), 42)\n        self.assertEqual(line.angle, 0)\n\n        # Vertical line\n        line = OWScatterPlotGraph._orthonormal_line(\n            np.array([42, 42, 42]), np.array([10, 11, 12]), color, width)\n        self.assertEqual(line.pos().x(), 42)\n        self.assertEqual(line.pos().y(), 10)\n        self.assertEqual(line.angle, 90)\n\n        # No line because all points coincide\n        line = OWScatterPlotGraph._orthonormal_line(\n            np.array([1, 1, 1]), np.array([42, 42, 42]), color, width)\n        self.assertIsNone(line)\n\n        # No line because the group is symmetric\n        line = OWScatterPlotGraph._orthonormal_line(\n            np.array([1, 1, 2, 2]), np.array([42, 5, 5, 42]), color, width)\n        self.assertIsNone(line)\n\n    def test_regression_line(self):\n        color = QColor(1, 2, 3)\n        width = 42\n        # Normal line\n        line = OWScatterPlotGraph._regression_line(\n            np.array([0, 1, 1, 2]), np.array([0, 0, 2, 2]), color, width)\n        self.assertEqual(line.pos().x(), 0)\n        self.assertAlmostEqual(line.pos().y(), 0)\n        self.assertEqual(line.angle, 45)\n        self.assertEqual(line.pen.color(), color)\n        self.assertEqual(line.pen.width(), width)\n\n        # Normal line, negative slope\n        line = OWScatterPlotGraph._regression_line(\n            np.array([1, 2, 3]), np.array([3, 2, 1]), color, width)\n        self.assertEqual(line.pos().x(), 1)\n        self.assertEqual(line.pos().y(), 3)\n        self.assertEqual(line.angle % 360, 315)\n\n        # Horizontal line\n        line = OWScatterPlotGraph._regression_line(\n            np.array([10, 11, 12]), np.array([42, 42, 42]), color, width)\n        self.assertEqual(line.pos().x(), 10)\n        self.assertEqual(line.pos().y(), 42)\n        self.assertEqual(line.angle, 0)\n\n        # Vertical line\n        line = OWScatterPlotGraph._regression_line(\n            np.array([42, 42, 42]), np.array([10, 11, 12]), color, width)\n        self.assertIsNone(line)\n\n        # No line because all points coincide\n        line = OWScatterPlotGraph._regression_line(\n            np.array([1, 1, 1]), np.array([42, 42, 42]), color, width)\n        self.assertIsNone(line)\n\n    def test_add_line_calls_proper_regressor(self):\n        graph = self.widget.graph\n        graph._orthonormal_line = Mock(return_value=None)\n        graph._regression_line = Mock(return_value=None)\n        x, y, c = Mock(), Mock(), Mock()\n\n        graph.orthonormal_regression = True\n        graph._add_line(x, y, c)\n        graph._orthonormal_line.assert_called_once_with(x, y, c, 3, 1)\n        graph._regression_line.assert_not_called()\n        graph._orthonormal_line.reset_mock()\n\n        graph.orthonormal_regression = False\n        graph._add_line(x, y, c)\n        graph._regression_line.assert_called_with(x, y, c, 3, 1)\n        graph._orthonormal_line.assert_not_called()\n\n    def test_no_regression_line(self):\n        graph = self.widget.graph\n        graph._orthonormal_line = lambda *_: None\n        graph.orthonormal_regression = True\n\n        graph.plot_widget.addItem = Mock()\n\n        x, y, c = Mock(), Mock(), Mock()\n        graph._add_line(x, y, c)\n        graph.plot_widget.addItem.assert_not_called()\n        self.assertEqual(graph.reg_line_items, [])\n\n    def test_update_regression_line_calls_add_line(self):\n        widget = self.widget\n        graph = widget.graph\n        x, y = np.array([[0, 0], [1, 0], [1, 2], [2, 2],\n                         [0, 1], [1, 3], [2, 5]], dtype=float).T\n        colors = np.array([0, 0, 0, 0, 1, 1, 1], dtype=float)\n        widget.get_coordinates_data = lambda: (x, y)\n        widget.can_draw_regresssion_line = lambda: True\n        widget.get_color_data = lambda: colors\n        widget.is_continuous_color = lambda: False\n        graph.palette = DefaultRGBColors\n        graph.controls.show_reg_line.setChecked(True)\n\n        graph._add_line = Mock()\n\n        graph.update_regression_line()\n        (args1, _), (args2, _), (args3, _) = graph._add_line.call_args_list\n        np.testing.assert_equal(args1[0], x)\n        np.testing.assert_equal(args1[1], y)\n        self.assertEqual(args1[2], QColor(\"#505050\"))\n\n        np.testing.assert_equal(args2[0], x[:4])\n        np.testing.assert_equal(args2[1], y[:4])\n        self.assertEqual(args2[2].hue(), graph.palette[0].hue())\n\n        np.testing.assert_equal(args3[0], x[4:])\n        np.testing.assert_equal(args3[1], y[4:])\n        self.assertEqual(args3[2].hue(), graph.palette[1].hue())\n        graph._add_line.reset_mock()\n\n        # Continuous color - just a single line\n        widget.is_continuous_color = lambda: True\n        graph.update_regression_line()\n        graph._add_line.assert_called_once()\n        args1, _ = graph._add_line.call_args_list[0]\n        np.testing.assert_equal(args1[0], x)\n        np.testing.assert_equal(args1[1], y)\n        self.assertEqual(args1[2].hue(), QColor(\"#505050\").hue())\n        graph._add_line.reset_mock()\n        widget.is_continuous_color = lambda: False\n\n        # No palette - just a single line\n        graph.palette = None\n        graph.update_regression_line()\n        graph._add_line.assert_called_once()\n        graph._add_line.reset_mock()\n        graph.palette = DefaultRGBColors\n\n        # Regression line is disabled\n        graph.show_reg_line = False\n        graph.update_regression_line()\n        graph._add_line.assert_not_called()\n        graph.show_reg_line = True\n\n        # No colors - just one line\n        widget.get_color_data = lambda: None\n        graph.update_regression_line()\n        graph._add_line.assert_called_once()\n        graph._add_line.reset_mock()\n\n        # No data\n        widget.get_coordinates_data = lambda: (None, None)\n        graph.update_regression_line()\n        graph._add_line.assert_not_called()\n        graph.show_reg_line = True\n        widget.get_coordinates_data = lambda: (x, y)\n\n        # One color group contains just one point - skip that line\n        widget.get_color_data = lambda: np.array([0] + [1] * (len(x) - 1))\n\n        graph.update_regression_line()\n        (args1, _), (args2, _) = graph._add_line.call_args_list\n        np.testing.assert_equal(args1[0], x)\n        np.testing.assert_equal(args1[1], y)\n        self.assertEqual(args1[2].hue(), QColor(\"#505050\").hue())\n\n        np.testing.assert_equal(args2[0], x[1:])\n        np.testing.assert_equal(args2[1], y[1:])\n        self.assertEqual(args2[2].hue(), graph.palette[1].hue())\n\n    def test_update_regression_line_is_called(self):\n        widget = self.widget\n        graph = widget.graph\n        urline = graph.update_regression_line = Mock()\n\n        self.send_signal(widget.Inputs.data, self.data)\n        urline.assert_called_once()\n        urline.reset_mock()\n\n        self.send_signal(widget.Inputs.data, None)\n        urline.assert_called_once()\n        urline.reset_mock()\n\n        self.send_signal(widget.Inputs.data, self.data)\n        urline.assert_called_once()\n        urline.reset_mock()\n\n        simulate.combobox_activate_index(self.widget.controls.attr_color, 0)\n        urline.assert_called_once()\n        urline.reset_mock()\n\n        simulate.combobox_activate_index(self.widget.controls.attr_color, 2)\n        urline.assert_called_once()\n        urline.reset_mock()\n\n        simulate.combobox_activate_index(self.widget.controls.attr_x, 3)\n        urline.assert_called_once()\n        urline.reset_mock()\n\n    def test_time_axis(self):\n        a = np.array([[1581953776, 1], [1581963776, 2], [1582953776, 3]])\n        d1 = Domain([ContinuousVariable(\"time\"), ContinuousVariable(\"value\")])\n        data = Table.from_numpy(d1, a)\n        d2 = Domain([TimeVariable(\"time\"), ContinuousVariable(\"value\")])\n        data_time = Table.from_numpy(d2, a)\n\n        x_axis = self.widget.graph.plot_widget.plotItem.getAxis(\"bottom\")\n\n        self.send_signal(self.widget.Inputs.data, data)\n        self.assertFalse(x_axis._use_time)\n        _ticks = x_axis.tickValues(1581953776, 1582953776, 1000)\n        ticks = x_axis.tickStrings(_ticks[0][1], 1, _ticks[0][0])\n        try:\n            float(ticks[0])\n        except ValueError:\n            self.fail(\"axis should display floats\")\n\n        self.send_signal(self.widget.Inputs.data, data_time)\n        self.assertTrue(x_axis._use_time)\n        _ticks = x_axis.tickValues(1581953776, 1582953776, 1000)\n        ticks = x_axis.tickStrings(_ticks[0][1], 1, _ticks[0][0])\n        with self.assertRaises(ValueError):\n            float(ticks[0])\n\n    def test_visual_settings(self):\n        super().test_visual_settings()\n\n        graph = self.widget.graph\n        font = QFont()\n        font.setItalic(True)\n        font.setFamily(\"Helvetica\")\n\n        key, value = ('Fonts', 'Axis title', 'Font size'), 16\n        self.widget.set_visual_settings(key, value)\n        key, value = ('Fonts', 'Axis title', 'Italic'), True\n        self.widget.set_visual_settings(key, value)\n        font.setPointSize(16)\n        for item in graph.parameter_setter.axis_items:\n            self.assertFontEqual(item.label.font(), font)\n\n        key, value = ('Fonts', 'Axis ticks', 'Font size'), 15\n        self.widget.set_visual_settings(key, value)\n        key, value = ('Fonts', 'Axis ticks', 'Italic'), True\n        self.widget.set_visual_settings(key, value)\n        font.setPointSize(15)\n        for item in graph.parameter_setter.axis_items:\n            self.assertFontEqual(item.style[\"tickFont\"], font)\n\n        self.widget.graph.controls.show_reg_line.setChecked(True)\n        self.assertGreater(len(graph.parameter_setter.reg_line_label_items), 0)\n\n        key, value = ('Fonts', 'Line label', 'Font size'), 16\n        self.widget.set_visual_settings(key, value)\n        key, value = ('Fonts', 'Line label', 'Italic'), True\n        self.widget.set_visual_settings(key, value)\n        font.setPointSize(16)\n        for label in graph.parameter_setter.reg_line_label_items:\n            self.assertFontEqual(label.textItem.font(), font)\n\n        key, value = ('Figure', 'Lines', 'Width'), 10\n        self.widget.set_visual_settings(key, value)\n        for item in graph.reg_line_items:\n            self.assertEqual(item.pen.width(), 10)\n\n\nif __name__ == \"__main__\":\n    import unittest\n    unittest.main()\n",
            "new_file_content": "# Test methods with long descriptive names can omit docstrings\n# pylint: disable=missing-docstring,too-many-public-methods,protected-access\n# pylint: disable=too-many-lines\nfrom unittest.mock import MagicMock, patch, Mock\nimport numpy as np\n\nfrom AnyQt.QtCore import QRectF, Qt\nfrom AnyQt.QtWidgets import QToolTip\nfrom AnyQt.QtGui import QColor, QFont\n\nfrom Orange.data import (\n    Table, Domain, ContinuousVariable, DiscreteVariable, TimeVariable\n)\nfrom Orange.widgets.tests.base import (\n    WidgetTest, WidgetOutputsTestMixin, datasets, ProjectionWidgetTestMixin\n)\nfrom Orange.widgets.tests.utils import simulate\nfrom Orange.widgets.utils.colorpalettes import DefaultRGBColors\nfrom Orange.widgets.visualize.owscatterplot import (\n    OWScatterPlot, ScatterPlotVizRank, OWScatterPlotGraph)\nfrom Orange.widgets.visualize.utils.widget import MAX_COLORS\nfrom Orange.widgets.widget import AttributeList\n\n\nclass TestOWScatterPlot(WidgetTest, ProjectionWidgetTestMixin,\n                        WidgetOutputsTestMixin):\n    @classmethod\n    def setUpClass(cls):\n        super().setUpClass()\n        WidgetOutputsTestMixin.init(cls)\n        cls.same_input_output_domain = False\n\n        cls.signal_name = \"Data\"\n        cls.signal_data = cls.data\n\n    def setUp(self):\n        self.widget = self.create_widget(OWScatterPlot)\n\n    def test_set_data(self):\n        # Connect iris to scatter plot\n        self.send_signal(self.widget.Inputs.data, self.data)\n\n        # First two attribute should be selected as x an y\n        self.assertEqual(self.widget.attr_x, self.data.domain[0])\n        self.assertEqual(self.widget.attr_y, self.data.domain[1])\n\n        # Class var should be selected as color\n        self.assertIs(self.widget.attr_color, self.data.domain.class_var)\n\n        # Change which attributes are displayed\n        self.widget.attr_x = self.data.domain[2]\n        self.widget.attr_y = self.data.domain[3]\n\n        # Disconnect the data\n        self.send_signal(self.widget.Inputs.data, None)\n\n        # removing data should have cleared attributes\n        self.assertIsNone(self.widget.attr_x)\n        self.assertIsNone(self.widget.attr_y)\n        self.assertIsNone(self.widget.attr_color)\n\n        # and remove the legend\n        self.assertEqual(len(self.widget.graph.color_legend.items), 0)\n\n        # Connect iris again\n        # same attributes that were used last time should be selected\n        self.send_signal(self.widget.Inputs.data, self.data)\n\n        self.assertIs(self.widget.attr_x, self.data.domain[2])\n        self.assertIs(self.widget.attr_y, self.data.domain[3])\n\n    def test_score_heuristics(self):\n        domain = Domain([ContinuousVariable(c) for c in \"abcd\"],\n                        DiscreteVariable(\"e\", values=\"ab\"))\n        a = np.arange(10).reshape((10, 1))\n        data = Table(domain, np.hstack([a, a, a, a]), a >= 5)\n        self.send_signal(self.widget.Inputs.data, data)\n        vizrank = ScatterPlotVizRank(self.widget)\n        self.assertEqual([x.name for x in vizrank.score_heuristic()],\n                         list(\"abcd\"))\n\n    def test_optional_combos(self):\n        domain = self.data.domain\n        d1 = Domain(domain.attributes[:2], domain.class_var,\n                    [domain.attributes[2]])\n        t1 = self.data.transform(d1)\n        self.send_signal(self.widget.Inputs.data, t1)\n        self.widget.graph.attr_size = domain.attributes[2]\n\n        d2 = Domain(domain.attributes[:2], domain.class_var,\n                    [domain.attributes[3]])\n        t2 = self.data.transform(d2)\n        self.send_signal(self.widget.Inputs.data, t2)\n\n    def test_error_message(self):\n        \"\"\"Check if error message appears and then disappears when\n        data is removed from input\"\"\"\n        data = self.data.copy()\n        with data.unlocked():\n            data.X[:, 0] = np.nan\n        self.send_signal(self.widget.Inputs.data, data)\n        self.assertTrue(self.widget.Warning.missing_coords.is_shown())\n        self.send_signal(self.widget.Inputs.data, None)\n        self.assertFalse(self.widget.Warning.missing_coords.is_shown())\n\n    def test_report_on_empty(self):\n        self.widget.report_plot = MagicMock()\n        self.widget.report_caption = MagicMock()\n        self.widget.report_items = MagicMock()\n        self.widget.send_report()  # Essentially, don't crash\n        self.widget.report_plot.assert_not_called()\n        self.widget.report_caption.assert_not_called()\n        self.widget.report_items.assert_not_called()\n\n    def test_data_column_nans(self):\n        \"\"\"\n        ValueError cannot convert float NaN to integer.\n        In case when all column values are NaN then it throws that error.\n        GH-2061\n        \"\"\"\n        table = datasets.data_one_column_nans()\n        self.send_signal(self.widget.Inputs.data, table)\n        cb_attr_color = self.widget.controls.attr_color\n        simulate.combobox_activate_item(cb_attr_color, \"b\")\n        simulate.combobox_activate_item(self.widget.cb_attr_x, \"a\")\n        simulate.combobox_activate_item(self.widget.cb_attr_y, \"a\")\n\n        #self.widget.update_graph()\n        self.widget.graph.reset_graph()\n\n    def test_data_column_infs(self):\n        \"\"\"\n        Scatter Plot should not crash on data with infinity values\n        GH-2707\n        GH-2684\n        \"\"\"\n        table = datasets.data_one_column_infs()\n        self.send_signal(self.widget.Inputs.data, table)\n        attr_x = self.widget.controls.attr_x\n        simulate.combobox_activate_item(attr_x, \"b\")\n\n    def test_regression_line(self):\n        \"\"\"It is possible to draw the line only for pair of continuous attrs\"\"\"\n        self.send_signal(self.widget.Inputs.data, self.data)\n        self.assertTrue(self.widget.cb_reg_line.isEnabled())\n        self.assertIsNone(self.widget.graph.reg_line_item)\n        self.widget.cb_reg_line.setChecked(True)\n        self.assertIsNotNone(self.widget.graph.reg_line_item)\n        self.widget.cb_attr_y.activated.emit(4)\n        self.widget.cb_attr_y.setCurrentIndex(4)\n        self.assertFalse(self.widget.cb_reg_line.isEnabled())\n        self.assertIsNone(self.widget.graph.reg_line_item)\n\n    def test_points_combo_boxes(self):\n        \"\"\"Check Point box combo models and values\"\"\"\n        self.send_signal(self.widget.Inputs.data, self.data)\n        controls = self.widget.controls\n\n        # color and label should contain all variables\n        # size should contain only continuous variables\n        # shape should contain only discrete variables\n        for var in self.data.domain.variables + self.data.domain.metas:\n            self.assertIn(var, controls.attr_color.model())\n            self.assertIn(var, controls.attr_label.model())\n            if var.is_continuous:\n                self.assertIn(var, controls.attr_size.model())\n                self.assertNotIn(var, controls.attr_shape.model())\n            if var.is_discrete:\n                self.assertNotIn(var, controls.attr_size.model())\n                self.assertIn(var, controls.attr_shape.model())\n\n        widget = self.create_widget(OWScatterPlot)\n        self.send_signal(self.widget.Inputs.data, self.data, widget=widget)\n        self.assertEqual(controls.attr_color.currentText(),\n                         self.data.domain.class_var.name)\n\n    def test_group_selections(self):\n        self.send_signal(self.widget.Inputs.data, self.data)\n        graph = self.widget.graph\n        points = graph.scatterplot_item.points()\n        sel_column = np.zeros((len(self.data), 1))\n\n        x = self.data.X\n\n        def selectedx():\n            return self.get_output(self.widget.Outputs.selected_data).X\n\n        def selected_groups():\n            return self.get_output(self.widget.Outputs.selected_data).metas[:, 0]\n\n        def annotated():\n            return self.get_output(self.widget.Outputs.annotated_data).metas\n\n        def annotations():\n            return self.get_output(self.widget.Outputs.annotated_data).domain.metas[0].values\n\n        # Select 0:5\n        graph.select(points[:5])\n        np.testing.assert_equal(selectedx(), x[:5])\n        np.testing.assert_equal(selected_groups(), np.zeros(5))\n        sel_column[:5] = 1\n        np.testing.assert_equal(annotated(), sel_column)\n        self.assertEqual(annotations(), (\"No\", \"Yes\", ))\n\n        # Shift-select 5:10; now we have groups 0:5 and 5:10\n        with self.modifiers(Qt.ShiftModifier):\n            graph.select(points[5:10])\n        np.testing.assert_equal(selectedx(), x[:10])\n        np.testing.assert_equal(selected_groups(), np.array([0] * 5 + [1] * 5))\n        sel_column[:5] = 0\n        sel_column[5:10] = 1\n        sel_column[10:] = 2\n        np.testing.assert_equal(annotated(), sel_column)\n        self.assertEqual(len(annotations()), 3)\n\n        # Select: 15:20; we have 15:20\n        graph.select(points[15:20])\n        sel_column = np.zeros((len(self.data), 1))\n        sel_column[15:20] = 1\n        np.testing.assert_equal(selectedx(), x[15:20])\n        np.testing.assert_equal(selected_groups(), np.zeros(5))\n        self.assertEqual(annotations(), (\"No\", \"Yes\"))\n\n        # Alt-select (remove) 10:17; we have 17:20\n        with self.modifiers(Qt.AltModifier):\n            graph.select(points[10:17])\n        np.testing.assert_equal(selectedx(), x[17:20])\n        np.testing.assert_equal(selected_groups(), np.zeros(3))\n        sel_column[15:17] = 0\n        np.testing.assert_equal(annotated(), sel_column)\n        self.assertEqual(annotations(), (\"No\", \"Yes\"))\n\n        # Ctrl-Shift-select (add-to-last) 10:17; we have 17:25\n        with self.modifiers(Qt.ShiftModifier | Qt.ControlModifier):\n            graph.select(points[20:25])\n        np.testing.assert_equal(selectedx(), x[17:25])\n        np.testing.assert_equal(selected_groups(), np.zeros(8))\n        sel_column[20:25] = 1\n        np.testing.assert_equal(annotated(), sel_column)\n        self.assertEqual(annotations(), (\"No\", \"Yes\"))\n\n        # Shift-select (add) 30:35; we have 17:25, 30:35\n        with self.modifiers(Qt.ShiftModifier):\n            graph.select(points[30:35])\n        # ... then Ctrl-Shift-select (add-to-last) 10:17; we have 17:25, 30:40\n        with self.modifiers(Qt.ShiftModifier | Qt.ControlModifier):\n            graph.select(points[35:40])\n        sel_column[:] = 2\n        sel_column[17:25] = 0\n        sel_column[30:40] = 1\n        np.testing.assert_equal(selected_groups(), np.array([0] * 8 + [1] * 10))\n        np.testing.assert_equal(annotated(), sel_column)\n        self.assertEqual(len(annotations()), 3)\n\n    def test_saving_selection(self):\n        self.send_signal(self.widget.Inputs.data, self.data)  # iris\n        self.widget.graph.select_by_rectangle(QRectF(4, 3, 3, 1))\n        selected_inds = np.flatnonzero(self.widget.graph.selection)\n        settings = self.widget.settingsHandler.pack_data(self.widget)\n        np.testing.assert_equal(selected_inds,\n                                [i for i, g in settings[\"selection\"]])\n\n    def test_points_selection(self):\n        # Opening widget with saved selection should restore it\n        self.widget = self.create_widget(\n            OWScatterPlot, stored_settings={\n                \"selection_group\": [(i, 1) for i in range(50)]}\n        )\n        self.send_signal(self.widget.Inputs.data, self.data)  # iris\n        selected_data = self.get_output(self.widget.Outputs.selected_data)\n        self.assertEqual(len(selected_data), 50)\n\n        # Changing the dataset should clear selection\n        titanic = Table(\"titanic\")\n        self.send_signal(self.widget.Inputs.data, titanic)\n        selected_data = self.get_output(self.widget.Outputs.selected_data)\n        self.assertIsNone(selected_data)\n\n    def test_migrate_selection(self):\n        settings = dict(selection=list(range(2)))\n        OWScatterPlot.migrate_settings(settings, 0)\n        self.assertEqual(settings[\"selection_group\"], [(0, 1), (1, 1)])\n\n    def test_invalid_points_selection(self):\n        # if selection contains rows that are not present in the current\n        # dataset, widget should select what can be selected.\n        self.widget = self.create_widget(\n            OWScatterPlot, stored_settings={\n                \"selection_group\": [(i, 1) for i in range(50)]}\n        )\n        data = self.data[:11].copy()\n        with data.unlocked():\n            data[0, 0] = np.nan\n        self.send_signal(self.widget.Inputs.data, data)\n        self.assertIsNone(self.get_output(self.widget.Outputs.selected_data))\n\n    def test_set_strings_settings(self):\n        \"\"\"\n        Test if settings can be loaded as strings and successfully put\n        in new owplotgui combos.\n        \"\"\"\n        self.send_signal(self.widget.Inputs.data, self.data)\n        settings = self.widget.settingsHandler.pack_data(self.widget)\n        plot_settings = settings[\"context_settings\"][0].values\n        plot_settings[\"attr_label\"] = (\"sepal length\", -2)\n        plot_settings[\"attr_color\"] = (\"sepal width\", -2)\n        plot_settings[\"attr_shape\"] = (\"iris\", -2)\n        plot_settings[\"attr_size\"] = (\"petal width\", -2)\n        w = self.create_widget(OWScatterPlot, stored_settings=settings)\n        self.send_signal(self.widget.Inputs.data, self.data, widget=w)\n        self.assertEqual(w.attr_label.name, \"sepal length\")\n        self.assertEqual(w.attr_color.name, \"sepal width\")\n        self.assertEqual(w.attr_shape.name, \"iris\")\n        self.assertEqual(w.attr_size.name, \"petal width\")\n\n    def test_features_and_no_data(self):\n        \"\"\"\n        Prevent crashing when features are sent but no data.\n        \"\"\"\n        domain = Table(\"iris\").domain\n        self.send_signal(self.widget.Inputs.features,\n                         AttributeList(domain.variables))\n        self.send_signal(self.widget.Inputs.features, None)\n\n    def test_features_and_data(self):\n        self.assertTrue(self.widget.attr_box.isEnabled())\n        self.send_signal(self.widget.Inputs.data, self.data)\n        x, y = self.widget.graph.scatterplot_item.getData()\n        np.testing.assert_array_equal(x, self.data.X[:, 0])\n        np.testing.assert_array_equal(y, self.data.X[:, 1])\n        self.send_signal(self.widget.Inputs.features,\n                         AttributeList(self.data.domain[2:]))\n        self.assertIs(self.widget.attr_x, self.data.domain[2])\n        self.assertIs(self.widget.attr_y, self.data.domain[3])\n        self.assertFalse(self.widget.attr_box.isEnabled())\n        self.assertFalse(self.widget.vizrank.isEnabled())\n        x, y = self.widget.graph.scatterplot_item.getData()\n        np.testing.assert_array_equal(x, self.data.X[:, 2])\n        np.testing.assert_array_equal(y, self.data.X[:, 3])\n\n        self.send_signal(self.widget.Inputs.data, None)\n        self.send_signal(self.widget.Inputs.data, self.data)\n        self.assertIs(self.widget.attr_x, self.data.domain[2])\n        self.assertIs(self.widget.attr_y, self.data.domain[3])\n        self.assertFalse(self.widget.attr_box.isEnabled())\n        self.assertFalse(self.widget.vizrank.isEnabled())\n\n        self.send_signal(self.widget.Inputs.features, None)\n        self.assertTrue(self.widget.attr_box.isEnabled())\n        self.assertTrue(self.widget.vizrank.isEnabled())\n\n    def test_output_features(self):\n        data = Table(\"iris\")\n        self.send_signal(self.widget.Inputs.data, data)\n\n        # This doesn't work because combo's callbacks are connected to signal\n        # `activated`, which is only triggered by user interaction, and not to\n        # `currentIndexChanged`\n        # combo_y = self.widget.controls.attr_y\n        # combo_y.setCurrentIndex(combo_y.model().indexOf(data.domain[3]))\n        # This is a workaround\n        self.widget.attr_y = data.domain[3]\n        self.widget.attr_changed()\n\n        features = self.get_output(self.widget.Outputs.features)\n        self.assertEqual(features, [data.domain[0], data.domain[3]])\n\n    def test_vizrank(self):\n        data = Table(\"iris\")\n        self.send_signal(self.widget.Inputs.data, data)\n        vizrank = ScatterPlotVizRank(self.widget)\n        n_states = len(data.domain.attributes)\n        n_states = n_states * (n_states - 1) / 2\n        states = [state for state in vizrank.iterate_states(None)]\n        self.assertEqual(len(states), n_states)\n        self.assertEqual(len(set(states)), n_states)\n        self.assertIsNotNone(vizrank.compute_score(states[0]))\n        self.send_signal(self.widget.Inputs.data, data[:9])\n        self.assertIsNone(vizrank.compute_score(states[0]))\n\n        data = Table(\"housing\")[::10]\n        self.send_signal(self.widget.Inputs.data, data)\n        vizrank = ScatterPlotVizRank(self.widget)\n        states = [state for state in vizrank.iterate_states(None)]\n        self.assertIsNotNone(vizrank.compute_score(states[0]))\n\n    def test_vizrank_class_nan(self):\n        \"\"\"\n        When class values are nan, vizrank should be disabled. It should behave like\n        the class column is missing.\n        GH-2757\n        \"\"\"\n        def assert_vizrank_enabled(data, is_enabled):\n            self.send_signal(self.widget.Inputs.data, data)\n            self.assertEqual(is_enabled, self.widget.vizrank_button.isEnabled())\n\n        data1 = Table(\"iris\")[::30]\n        data2 = Table(\"iris\")[::30].copy()\n        with data2.unlocked():\n            data2.Y[:] = np.nan\n        domain = Domain(\n            attributes=data2.domain.attributes[:4], class_vars=DiscreteVariable(\"iris\", values=()))\n        data2 = Table(domain, data2.X, Y=data2.Y)\n        data3 = Table(\"iris\")[::30].copy()\n        with data3.unlocked():\n            data3.Y[:] = np.nan\n\n        for data, is_enabled in zip([data1, data2, data1, data3, data1],\n                                    [True, False, True, False, True]):\n            assert_vizrank_enabled(data, is_enabled)\n\n    def test_vizrank_nonprimitives(self):\n        \"\"\"VizRank does not try to include non primitive attributes\"\"\"\n        data = Table(\"zoo\")\n        self.send_signal(self.widget.Inputs.data, data)\n        with patch(\"Orange.widgets.visualize.owscatterplot.ReliefF\",\n                   new=lambda *_1, **_2: lambda data: np.arange(len(data))):\n            self.widget.vizrank.score_heuristic()\n\n    def test_vizrank_enabled(self):\n        self.send_signal(self.widget.Inputs.data, self.data)\n        self.assertTrue(self.widget.vizrank_button.isEnabled())\n        self.assertEqual(self.widget.vizrank_button.toolTip(), \"\")\n        self.assertTrue(self.widget.vizrank.button.isEnabled())\n        self.widget.vizrank.button.click()\n\n    def test_vizrank_enabled_no_data(self):\n        self.send_signal(self.widget.Inputs.data, None)\n        self.assertFalse(self.widget.vizrank_button.isEnabled())\n        self.assertEqual(self.widget.vizrank_button.toolTip(), \"No data on input\")\n\n    def test_vizrank_enabled_sparse_data(self):\n        self.send_signal(self.widget.Inputs.data, self.data.to_sparse())\n        self.assertFalse(self.widget.vizrank_button.isEnabled())\n        self.assertEqual(self.widget.vizrank_button.toolTip(), \"Data is sparse\")\n\n    def test_vizrank_enabled_constant_data(self):\n        domain = Domain([ContinuousVariable(\"c1\"),\n                         ContinuousVariable(\"c2\"),\n                         ContinuousVariable(\"c3\"),\n                         ContinuousVariable(\"c4\")],\n                        DiscreteVariable(\"cls\", values=(\"a\", \"b\")))\n        X = np.zeros((10, 4))\n        table = Table(domain, X, np.random.randint(2, size=10))\n        self.send_signal(self.widget.Inputs.data, table)\n        self.assertEqual(self.widget.vizrank_button.toolTip(), \"\")\n        self.assertTrue(self.widget.vizrank_button.isEnabled())\n        self.assertTrue(self.widget.vizrank.button.isEnabled())\n        self.widget.vizrank.button.click()\n\n    def test_vizrank_enabled_two_features(self):\n        self.send_signal(self.widget.Inputs.data, self.data[:, :2])\n        self.assertFalse(self.widget.vizrank_button.isEnabled())\n        self.assertEqual(self.widget.vizrank_button.toolTip(),\n                         \"Not enough features for ranking\")\n\n    def test_vizrank_enabled_no_color_var(self):\n        self.send_signal(self.widget.Inputs.data, self.data[:, :3])\n        self.assertFalse(self.widget.vizrank_button.isEnabled())\n        self.assertEqual(self.widget.vizrank_button.toolTip(),\n                         \"Color variable is not selected\")\n\n    def test_vizrank_enabled_color_var_nans(self):\n        domain = Domain([ContinuousVariable(\"c1\"),\n                         ContinuousVariable(\"c2\"),\n                         ContinuousVariable(\"c3\"),\n                         ContinuousVariable(\"c4\")],\n                        DiscreteVariable(\"cls\", values=(\"a\", \"b\")))\n        table = Table(domain, np.random.random((10, 4)), np.full(10, np.nan))\n        self.send_signal(self.widget.Inputs.data, table)\n        self.assertFalse(self.widget.vizrank_button.isEnabled())\n        self.assertEqual(self.widget.vizrank_button.toolTip(),\n                         \"Color variable has no values\")\n\n    def test_auto_send_selection(self):\n        \"\"\"\n        Scatter Plot automatically sends selection only when the checkbox Send automatically\n        is checked.\n        GH-2649\n        GH-2646\n        \"\"\"\n        data = Table(\"iris\")\n        self.send_signal(self.widget.Inputs.data, data)\n        self.widget.controls.auto_commit.setChecked(False)\n        self.assertFalse(self.widget.controls.auto_commit.isChecked())\n        self._select_data()\n        self.assertIsNone(self.get_output(self.widget.Outputs.selected_data))\n        self.widget.controls.auto_commit.setChecked(True)\n        output = self.get_output(self.widget.Outputs.selected_data)\n        self.assertIsInstance(output, Table)\n\n    def test_color_is_optional(self):\n        zoo = Table(\"zoo\")\n        backbone, breathes, airborne, type = \\\n            [zoo.domain[x] for x in [\"backbone\", \"breathes\", \"airborne\", \"type\"]]\n        default_x, default_y, default_color = \\\n            zoo.domain[0], zoo.domain[1], zoo.domain.class_var\n        attr_x = self.widget.controls.attr_x\n        attr_y = self.widget.controls.attr_y\n        attr_color = self.widget.controls.attr_color\n\n        # Send dataset, ensure defaults are what we expect them to be\n        self.send_signal(self.widget.Inputs.data, zoo)\n        self.assertEqual(attr_x.currentText(), default_x.name)\n        self.assertEqual(attr_y.currentText(), default_y.name)\n        self.assertEqual(attr_color.currentText(), default_color.name)\n        # Select different values\n        simulate.combobox_activate_item(attr_x, backbone.name)\n        simulate.combobox_activate_item(attr_y, breathes.name)\n        simulate.combobox_activate_item(attr_color, airborne.name)\n\n        # Send compatible dataset, values should not change\n        zoo2 = zoo[:, (backbone, breathes, airborne, type)]\n        self.send_signal(self.widget.Inputs.data, zoo2)\n        self.assertEqual(attr_x.currentText(), backbone.name)\n        self.assertEqual(attr_y.currentText(), breathes.name)\n        self.assertEqual(attr_color.currentText(), airborne.name)\n\n        # Send dataset without color variable\n        # x and y should remain, color reset to default\n        zoo3 = zoo[:, (backbone, breathes, type)]\n        self.send_signal(self.widget.Inputs.data, zoo3)\n        self.assertEqual(attr_x.currentText(), backbone.name)\n        self.assertEqual(attr_y.currentText(), breathes.name)\n        self.assertEqual(attr_color.currentText(), default_color.name)\n\n        # Send dataset without x\n        # y and color should be the same as with zoo\n        zoo4 = zoo[:, (default_x, default_y, breathes, airborne, type)]\n        self.send_signal(self.widget.Inputs.data, zoo4)\n        self.assertEqual(attr_x.currentText(), default_x.name)\n        self.assertEqual(attr_y.currentText(), default_y.name)\n        self.assertEqual(attr_color.currentText(), default_color.name)\n\n        # Send dataset compatible with zoo2 and zoo3\n        # Color should reset to one in zoo3, as it was used more\n        # recently\n        zoo5 = zoo[:, (default_x, backbone, breathes, airborne, type)]\n        self.send_signal(self.widget.Inputs.data, zoo5)\n        self.assertEqual(attr_x.currentText(), backbone.name)\n        self.assertEqual(attr_y.currentText(), breathes.name)\n        self.assertEqual(attr_color.currentText(), type.name)\n\n    def test_handle_metas(self):\n        \"\"\"\n        Scatter Plot Graph can handle metas\n        GH-2699\n        \"\"\"\n        w = self.widget\n        data = Table(\"iris\")\n        domain = Domain(\n            attributes=data.domain.attributes[:2],\n            class_vars=data.domain.class_vars,\n            metas=data.domain.attributes[2:]\n        )\n        data = data.transform(domain).copy()\n        # Sometimes floats in metas are saved as objects\n        with data.unlocked():\n            data.metas = data.metas.astype(object)\n        self.send_signal(w.Inputs.data, data)\n        simulate.combobox_activate_item(w.cb_attr_x, data.domain.metas[1].name)\n        simulate.combobox_activate_item(w.controls.attr_color, data.domain.metas[0].name)\n        w.graph.reset_graph()\n\n    def test_subset_data(self):\n        \"\"\"\n        Scatter Plot subset data is sent to Scatter Plot Graph\n        GH-2773\n        \"\"\"\n        data = Table(\"iris\")\n        w = self.widget\n        self.send_signal(w.Inputs.data, data)\n        self.send_signal(w.Inputs.data_subset, data[::30])\n        self.assertEqual(len(w.subset_indices), 5)\n\n    def test_opacity_warning(self):\n        data = Table(\"iris\")\n        w = self.widget\n        self.send_signal(w.Inputs.data, data)\n        w.graph.controls.alpha_value.setSliderPosition(10)\n        self.assertFalse(w.Warning.transparent_subset.is_shown())\n        self.send_signal(w.Inputs.data_subset, data[::30])\n        self.assertTrue(w.Warning.transparent_subset.is_shown())\n        w.graph.controls.alpha_value.setSliderPosition(200)\n        self.assertFalse(w.Warning.transparent_subset.is_shown())\n        w.graph.controls.alpha_value.setSliderPosition(10)\n        self.assertTrue(w.Warning.transparent_subset.is_shown())\n        self.send_signal(w.Inputs.data_subset, None)\n        self.assertFalse(w.Warning.transparent_subset.is_shown())\n\n    def test_jittering(self):\n        self.send_signal(self.widget.Inputs.data, self.data)\n        self.widget.graph.controls.jitter_continuous.setChecked(True)\n        self.widget.graph.controls.jitter_size.setValue(1)\n\n    def test_metas_zero_column(self):\n        \"\"\"\n        Prevent crash when metas column is zero.\n        GH-2775\n        \"\"\"\n        data = Table(\"iris\")\n        domain = data.domain\n        domain = Domain(domain.attributes[:3], domain.class_vars, domain.attributes[3:])\n        data = data.transform(domain).copy()\n        with data.unlocked():\n            data.metas[:, 0] = 0\n        w = self.widget\n        self.send_signal(w.Inputs.data, data)\n        simulate.combobox_activate_item(w.controls.attr_x, domain.metas[0].name)\n\n    def test_tooltip(self):\n        # The test tests presence of some data,\n        # but avoids checking the exact format\n        data = Table(\"heart_disease\")\n        self.send_signal(self.widget.Inputs.data, data)\n        widget = self.widget\n        graph = widget.graph\n        scatterplot_item = graph.scatterplot_item\n\n        widget.controls.attr_x = data.domain[\"chest pain\"]\n        widget.controls.attr_y = data.domain[\"cholesterol\"]\n        all_points = scatterplot_item.points()\n\n        event = MagicMock()\n        with patch.object(scatterplot_item, \"mapFromScene\"), \\\n                patch.object(QToolTip, \"showText\") as show_text:\n\n            # Single point hovered\n            with patch.object(scatterplot_item, \"pointsAt\",\n                              return_value=[all_points[42]]):\n                # Show just x and y attribute\n                widget.tooltip_shows_all = False\n                self.assertTrue(graph.help_event(event))\n                (_, text), _ = show_text.call_args\n                self.assertIn(\"age = {}\".format(data[42, \"age\"]), text)\n                self.assertIn(\"gender = {}\".format(data[42, \"gender\"]), text)\n                self.assertNotIn(\"max HR = {}\".format(data[42, \"max HR\"]), text)\n                self.assertNotIn(\"others\", text)\n\n                # Show all attributes\n                widget.tooltip_shows_all = True\n                self.assertTrue(graph.help_event(event))\n                (_, text), _ = show_text.call_args\n                self.assertIn(\"age = {}\".format(data[42, \"age\"]), text)\n                self.assertIn(\"gender = {}\".format(data[42, \"gender\"]), text)\n                self.assertIn(\"max HR = {}\".format(data[42, \"max HR\"]), text)\n                self.assertIn(\"... and 4 others\", text)\n\n            # Two points hovered\n            with patch.object(scatterplot_item, \"pointsAt\",\n                              return_value=[all_points[42], all_points[100]]):\n                self.assertTrue(graph.help_event(event))\n                (_, text), _ = show_text.call_args\n                self.assertIn(\"age = {}\".format(data[42, \"age\"]), text)\n                self.assertIn(\"gender = {}\".format(data[42, \"gender\"]), text)\n                self.assertIn(\"age = {}\".format(data[100, \"age\"]), text)\n                self.assertIn(\"gender = {}\".format(data[100, \"gender\"]), text)\n\n            # No points hovered\n            with patch.object(scatterplot_item, \"pointsAt\",\n                              return_value=[]):\n                show_text.reset_mock()\n                self.assertFalse(graph.help_event(event))\n                self.assertEqual(show_text.call_count, 0)\n\n    def test_many_discrete_values(self):\n        \"\"\"\n        Do not show all discrete values if there are too many.\n        Also test for values with a nan.\n        GH-2804\n        \"\"\"\n        def prepare_data():\n            data = Table(\"iris\")\n            values = list(range(15))\n            class_var = DiscreteVariable(\"iris5\", values=[str(v) for v in values])\n            data = data.transform(\n                Domain(attributes=data.domain.attributes,\n                       class_vars=[class_var])).copy()\n            with data.unlocked():\n                data.Y = np.array(values * 10, dtype=float)\n            return data\n\n        def assert_equal(data, max):\n            self.send_signal(self.widget.Inputs.data, data)\n            pen_data, brush_data = self.widget.graph.get_colors()\n            self.assertEqual(max, len(np.unique([id(p) for p in pen_data])), )\n\n        assert_equal(prepare_data(), MAX_COLORS)\n        # data with nan value\n        data = prepare_data()\n        with data.unlocked():\n            data.Y[42] = np.nan\n        assert_equal(data, MAX_COLORS + 1)\n\n    def test_invalidated_same_features(self):\n        self.widget.setup_plot = Mock()\n        # send data and set default features\n        self.send_signal(self.widget.Inputs.data, self.data)\n        self.widget.setup_plot.assert_called_once()\n        self.assertListEqual(self.widget.effective_variables,\n                             list(self.data.domain.attributes[:2]))\n\n        # send the same features as already set\n        self.widget.setup_plot.reset_mock()\n        self.send_signal(self.widget.Inputs.features,\n                         AttributeList(self.data.domain.attributes[:2]))\n        self.widget.setup_plot.assert_not_called()\n        self.assertListEqual(self.widget.effective_variables,\n                             list(self.data.domain.attributes[:2]))\n\n    def test_invalidated_same_time(self):\n        self.widget.setup_plot = Mock()\n        # send data and features at the same time (data first)\n        features = self.data.domain.attributes[:2]\n        signals = [(self.widget.Inputs.data, self.data),\n                   (self.widget.Inputs.features, AttributeList(features))]\n        self.send_signals(signals)\n        self.widget.setup_plot.assert_called_once()\n        self.assertListEqual(self.widget.effective_variables, list(features))\n\n    def test_invalidated_features_first(self):\n        self.widget.setup_plot = Mock()\n        # send features (same as default ones)\n        self.send_signal(self.widget.Inputs.features,\n                         AttributeList(self.data.domain.attributes[:2]))\n        self.assertListEqual(self.widget.effective_variables, [])\n        self.widget.setup_plot.assert_called_once()\n\n        # send data\n        self.widget.setup_plot.reset_mock()\n        self.send_signal(self.widget.Inputs.data, self.data)\n        self.widget.setup_plot.assert_called()\n        self.assertListEqual(self.widget.effective_variables,\n                             list(self.data.domain.attributes[:2]))\n\n    def test_invalidated_same_time_features_first(self):\n        self.widget.setup_plot = Mock()\n        # send features and data at the same time (features first)\n        features = self.data.domain.attributes[:2]\n        signals = [(self.widget.Inputs.features, AttributeList(features)),\n                   (self.widget.Inputs.data, self.data)]\n        self.send_signals(signals)\n        self.widget.setup_plot.assert_called_once()\n        self.assertListEqual(self.widget.effective_variables, list(features))\n\n        self.widget.setup_plot.reset_mock()\n        features = self.data.domain.attributes[2:]\n        signals = [(self.widget.Inputs.features, AttributeList(features)),\n                   (self.widget.Inputs.data, self.data)]\n        self.send_signals(signals)\n        self.widget.setup_plot.assert_called_once()\n\n    def test_invalidated_diff_features(self):\n        self.widget.setup_plot = Mock()\n        # send data and set default features\n        self.send_signal(self.widget.Inputs.data, self.data)\n        self.widget.setup_plot.assert_called_once()\n        self.assertListEqual(self.widget.effective_variables,\n                             list(self.data.domain.attributes[:2]))\n\n        # send different features\n        self.widget.setup_plot.reset_mock()\n        self.send_signal(self.widget.Inputs.features,\n                         AttributeList(self.data.domain.attributes[2:4]))\n        self.widget.setup_plot.assert_called_once()\n        self.assertListEqual(self.widget.effective_variables,\n                             list(self.data.domain.attributes[2:4]))\n\n    def test_invalidated_diff_features_same_time(self):\n        self.widget.setup_plot = Mock()\n        # send data and different features at the same time (data first)\n        features = self.data.domain.attributes[2:4]\n        signals = [(self.widget.Inputs.data, self.data),\n                   (self.widget.Inputs.features, AttributeList(features))]\n        self.send_signals(signals)\n        self.widget.setup_plot.assert_called_once()\n        self.assertListEqual(self.widget.effective_variables, list(features))\n\n    def test_invalidated_diff_features_features_first(self):\n        self.widget.setup_plot = Mock()\n        # send features (not the same as defaults)\n        self.send_signal(self.widget.Inputs.features,\n                         AttributeList(self.data.domain.attributes[2:4]))\n        self.assertListEqual(self.widget.effective_variables, [])\n        self.widget.setup_plot.assert_called_once()\n\n        # send data\n        self.widget.setup_plot.reset_mock()\n        self.send_signal(self.widget.Inputs.data, self.data)\n        self.widget.setup_plot.assert_called_once()\n        self.assertListEqual(self.widget.effective_variables,\n                             list(self.data.domain.attributes[2:4]))\n\n    def test_invalidated_diff_features_same_time_features_first(self):\n        self.widget.setup_plot = Mock()\n        # send data and different features at the same time (features first)\n        features = self.data.domain.attributes[2:4]\n        signals = [(self.widget.Inputs.features, AttributeList(features)),\n                   (self.widget.Inputs.data, self.data)]\n        self.send_signals(signals)\n        self.widget.setup_plot.assert_called_once()\n        self.assertListEqual(self.widget.effective_variables, list(features))\n\n    @patch('Orange.widgets.visualize.owscatterplot.ScatterPlotVizRank.'\n           'on_manual_change')\n    def test_vizrank_receives_manual_change(self, on_manual_change):\n        # Recreate the widget so the patch kicks in\n        self.widget = self.create_widget(OWScatterPlot)\n        data = Table(\"iris.tab\")\n        self.send_signal(self.widget.Inputs.data, data)\n        model = self.widget.controls.attr_x.model()\n        self.widget.attr_x = model[0]\n        self.widget.attr_y = model[1]\n        simulate.combobox_activate_index(self.widget.controls.attr_x, 2)\n        self.assertIs(self.widget.attr_x, model[2])\n        on_manual_change.assert_called_with(model[2], model[1])\n\n    def test_on_manual_change(self):\n        data = Table(\"iris.tab\")\n        self.send_signal(self.widget.Inputs.data, data)\n        vizrank = self.widget.vizrank\n        vizrank.toggle()\n        self.process_events(until=lambda: not vizrank.keep_running)\n\n        model = vizrank.rank_model\n        attrs = model.data(model.index(3, 0), vizrank._AttrRole)\n        vizrank.on_manual_change(*attrs)\n        selection = vizrank.rank_table.selectedIndexes()\n        self.assertEqual(len(selection), 1)\n        self.assertEqual(selection[0].row(), 3)\n\n        vizrank.on_manual_change(*attrs[::-1])\n        selection = vizrank.rank_table.selectedIndexes()\n        self.assertEqual(len(selection), 0)\n\n    def test_regression_lines_appear(self):\n        self.widget.graph.controls.show_reg_line.setChecked(True)\n        self.assertEqual(len(self.widget.graph.reg_line_items), 0)\n        self.send_signal(self.widget.Inputs.data, self.data)\n        self.assertEqual(len(self.widget.graph.reg_line_items), 4)\n        simulate.combobox_activate_index(self.widget.controls.attr_color, 0)\n        self.assertEqual(len(self.widget.graph.reg_line_items), 1)\n        data = self.data.copy()\n        with data.unlocked():\n            data[:, 0] = np.nan\n        self.send_signal(self.widget.Inputs.data, data)\n        self.assertEqual(len(self.widget.graph.reg_line_items), 0)\n\n    def test_regression_line_coeffs(self):\n        widget = self.widget\n        graph = widget.graph\n        xy = np.array([[0, 0], [1, 0], [1, 2], [2, 2],\n                       [0, 1], [1, 3], [2, 5]], dtype=float)\n        colors = np.array([0, 0, 0, 0, 1, 1, 1], dtype=float)\n        widget.get_coordinates_data = lambda: xy.T\n        widget.can_draw_regresssion_line = lambda: True\n        widget.get_color_data = lambda: colors\n        widget.is_continuous_color = lambda: False\n        graph.palette = DefaultRGBColors\n        graph.controls.show_reg_line.setChecked(True)\n\n        graph.update_regression_line()\n\n        line1 = graph.reg_line_items[1]\n        self.assertEqual(line1.pos().x(), 0)\n        self.assertEqual(line1.pos().y(), 0)\n        self.assertEqual(line1.angle, 45)\n        self.assertEqual(line1.pen.color().hue(), graph.palette[0].hue())\n\n        line2 = graph.reg_line_items[2]\n        self.assertEqual(line2.pos().x(), 0)\n        self.assertEqual(line2.pos().y(), 1)\n        self.assertAlmostEqual(line2.angle, np.degrees(np.arctan2(2, 1)))\n        self.assertEqual(line2.pen.color().hue(), graph.palette[1].hue())\n\n        graph.orthonormal_regression = True\n        graph.update_regression_line()\n\n        line1 = graph.reg_line_items[1]\n        self.assertEqual(line1.pos().x(), 0)\n        self.assertAlmostEqual(line1.pos().y(), -0.6180339887498949)\n        self.assertAlmostEqual(line1.angle, 58.28252558853899)\n        self.assertEqual(line1.pen.color().hue(), graph.palette[0].hue())\n\n        line2 = graph.reg_line_items[2]\n        self.assertEqual(line2.pos().x(), 0)\n        self.assertEqual(line2.pos().y(), 1)\n        self.assertAlmostEqual(line2.angle, np.degrees(np.arctan2(2, 1)))\n        self.assertEqual(line2.pen.color().hue(), graph.palette[1].hue())\n\n    def test_orthonormal_line(self):\n        color = QColor(1, 2, 3)\n        width = 42\n        # Normal line\n        line = OWScatterPlotGraph._orthonormal_line(\n            np.array([0, 1, 1, 2]), np.array([0, 0, 2, 2]), color, width)\n        self.assertEqual(line.pos().x(), 0)\n        self.assertAlmostEqual(line.pos().y(), -0.6180339887498949)\n        self.assertAlmostEqual(line.angle, 58.28252558853899)\n        self.assertEqual(line.pen.color(), color)\n        self.assertEqual(line.pen.width(), width)\n\n        # Normal line, negative slope\n        line = OWScatterPlotGraph._orthonormal_line(\n            np.array([1, 2, 3]), np.array([3, 2, 1]), color, width)\n        self.assertEqual(line.pos().x(), 1)\n        self.assertEqual(line.pos().y(), 3)\n        self.assertEqual(line.angle % 360, 315)\n\n        # Horizontal line\n        line = OWScatterPlotGraph._orthonormal_line(\n            np.array([10, 11, 12]), np.array([42, 42, 42]), color, width)\n        self.assertEqual(line.pos().x(), 10)\n        self.assertEqual(line.pos().y(), 42)\n        self.assertEqual(line.angle, 0)\n\n        # Vertical line\n        line = OWScatterPlotGraph._orthonormal_line(\n            np.array([42, 42, 42]), np.array([10, 11, 12]), color, width)\n        self.assertEqual(line.pos().x(), 42)\n        self.assertEqual(line.pos().y(), 10)\n        self.assertEqual(line.angle, 90)\n\n        # No line because all points coincide\n        line = OWScatterPlotGraph._orthonormal_line(\n            np.array([1, 1, 1]), np.array([42, 42, 42]), color, width)\n        self.assertIsNone(line)\n\n        # No line because the group is symmetric\n        line = OWScatterPlotGraph._orthonormal_line(\n            np.array([1, 1, 2, 2]), np.array([42, 5, 5, 42]), color, width)\n        self.assertIsNone(line)\n\n    def test_regression_line(self):\n        color = QColor(1, 2, 3)\n        width = 42\n        # Normal line\n        line = OWScatterPlotGraph._regression_line(\n            np.array([0, 1, 1, 2]), np.array([0, 0, 2, 2]), color, width)\n        self.assertEqual(line.pos().x(), 0)\n        self.assertAlmostEqual(line.pos().y(), 0)\n        self.assertEqual(line.angle, 45)\n        self.assertEqual(line.pen.color(), color)\n        self.assertEqual(line.pen.width(), width)\n\n        # Normal line, negative slope\n        line = OWScatterPlotGraph._regression_line(\n            np.array([1, 2, 3]), np.array([3, 2, 1]), color, width)\n        self.assertEqual(line.pos().x(), 1)\n        self.assertEqual(line.pos().y(), 3)\n        self.assertEqual(line.angle % 360, 315)\n\n        # Horizontal line\n        line = OWScatterPlotGraph._regression_line(\n            np.array([10, 11, 12]), np.array([42, 42, 42]), color, width)\n        self.assertEqual(line.pos().x(), 10)\n        self.assertEqual(line.pos().y(), 42)\n        self.assertEqual(line.angle, 0)\n\n        # Vertical line\n        line = OWScatterPlotGraph._regression_line(\n            np.array([42, 42, 42]), np.array([10, 11, 12]), color, width)\n        self.assertIsNone(line)\n\n        # No line because all points coincide\n        line = OWScatterPlotGraph._regression_line(\n            np.array([1, 1, 1]), np.array([42, 42, 42]), color, width)\n        self.assertIsNone(line)\n\n    def test_add_line_calls_proper_regressor(self):\n        graph = self.widget.graph\n        graph._orthonormal_line = Mock(return_value=None)\n        graph._regression_line = Mock(return_value=None)\n        x, y, c = Mock(), Mock(), Mock()\n\n        graph.orthonormal_regression = True\n        graph._add_line(x, y, c)\n        graph._orthonormal_line.assert_called_once_with(x, y, c, 3, 1)\n        graph._regression_line.assert_not_called()\n        graph._orthonormal_line.reset_mock()\n\n        graph.orthonormal_regression = False\n        graph._add_line(x, y, c)\n        graph._regression_line.assert_called_with(x, y, c, 3, 1)\n        graph._orthonormal_line.assert_not_called()\n\n    def test_no_regression_line(self):\n        graph = self.widget.graph\n        graph._orthonormal_line = lambda *_: None\n        graph.orthonormal_regression = True\n\n        graph.plot_widget.addItem = Mock()\n\n        x, y, c = Mock(), Mock(), Mock()\n        graph._add_line(x, y, c)\n        graph.plot_widget.addItem.assert_not_called()\n        self.assertEqual(graph.reg_line_items, [])\n\n    def test_update_regression_line_calls_add_line(self):\n        widget = self.widget\n        graph = widget.graph\n        x, y = np.array([[0, 0], [1, 0], [1, 2], [2, 2],\n                         [0, 1], [1, 3], [2, 5]], dtype=float).T\n        colors = np.array([0, 0, 0, 0, 1, 1, 1], dtype=float)\n        widget.get_coordinates_data = lambda: (x, y)\n        widget.can_draw_regresssion_line = lambda: True\n        widget.get_color_data = lambda: colors\n        widget.is_continuous_color = lambda: False\n        graph.palette = DefaultRGBColors\n        graph.controls.show_reg_line.setChecked(True)\n\n        graph._add_line = Mock()\n\n        graph.update_regression_line()\n        (args1, _), (args2, _), (args3, _) = graph._add_line.call_args_list\n        np.testing.assert_equal(args1[0], x)\n        np.testing.assert_equal(args1[1], y)\n        self.assertEqual(args1[2], QColor(\"#505050\"))\n\n        np.testing.assert_equal(args2[0], x[:4])\n        np.testing.assert_equal(args2[1], y[:4])\n        self.assertEqual(args2[2].hue(), graph.palette[0].hue())\n\n        np.testing.assert_equal(args3[0], x[4:])\n        np.testing.assert_equal(args3[1], y[4:])\n        self.assertEqual(args3[2].hue(), graph.palette[1].hue())\n        graph._add_line.reset_mock()\n\n        # Continuous color - just a single line\n        widget.is_continuous_color = lambda: True\n        graph.update_regression_line()\n        graph._add_line.assert_called_once()\n        args1, _ = graph._add_line.call_args_list[0]\n        np.testing.assert_equal(args1[0], x)\n        np.testing.assert_equal(args1[1], y)\n        self.assertEqual(args1[2].hue(), QColor(\"#505050\").hue())\n        graph._add_line.reset_mock()\n        widget.is_continuous_color = lambda: False\n\n        # No palette - just a single line\n        graph.palette = None\n        graph.update_regression_line()\n        graph._add_line.assert_called_once()\n        graph._add_line.reset_mock()\n        graph.palette = DefaultRGBColors\n\n        # Regression line is disabled\n        graph.show_reg_line = False\n        graph.update_regression_line()\n        graph._add_line.assert_not_called()\n        graph.show_reg_line = True\n\n        # No colors - just one line\n        widget.get_color_data = lambda: None\n        graph.update_regression_line()\n        graph._add_line.assert_called_once()\n        graph._add_line.reset_mock()\n\n        # No data\n        widget.get_coordinates_data = lambda: (None, None)\n        graph.update_regression_line()\n        graph._add_line.assert_not_called()\n        graph.show_reg_line = True\n        widget.get_coordinates_data = lambda: (x, y)\n\n        # One color group contains just one point - skip that line\n        widget.get_color_data = lambda: np.array([0] + [1] * (len(x) - 1))\n\n        graph.update_regression_line()\n        (args1, _), (args2, _) = graph._add_line.call_args_list\n        np.testing.assert_equal(args1[0], x)\n        np.testing.assert_equal(args1[1], y)\n        self.assertEqual(args1[2].hue(), QColor(\"#505050\").hue())\n\n        np.testing.assert_equal(args2[0], x[1:])\n        np.testing.assert_equal(args2[1], y[1:])\n        self.assertEqual(args2[2].hue(), graph.palette[1].hue())\n\n    def test_update_regression_line_is_called(self):\n        widget = self.widget\n        graph = widget.graph\n        urline = graph.update_regression_line = Mock()\n\n        self.send_signal(widget.Inputs.data, self.data)\n        urline.assert_called_once()\n        urline.reset_mock()\n\n        self.send_signal(widget.Inputs.data, None)\n        urline.assert_called_once()\n        urline.reset_mock()\n\n        self.send_signal(widget.Inputs.data, self.data)\n        urline.assert_called_once()\n        urline.reset_mock()\n\n        simulate.combobox_activate_index(self.widget.controls.attr_color, 0)\n        urline.assert_called_once()\n        urline.reset_mock()\n\n        simulate.combobox_activate_index(self.widget.controls.attr_color, 2)\n        urline.assert_called_once()\n        urline.reset_mock()\n\n        simulate.combobox_activate_index(self.widget.controls.attr_x, 3)\n        urline.assert_called_once()\n        urline.reset_mock()\n\n    def test_time_axis(self):\n        a = np.array([[1581953776, 1], [1581963776, 2], [1582953776, 3]])\n        d1 = Domain([ContinuousVariable(\"time\"), ContinuousVariable(\"value\")])\n        data = Table.from_numpy(d1, a)\n        d2 = Domain([TimeVariable(\"time\"), ContinuousVariable(\"value\")])\n        data_time = Table.from_numpy(d2, a)\n\n        x_axis = self.widget.graph.plot_widget.plotItem.getAxis(\"bottom\")\n\n        self.send_signal(self.widget.Inputs.data, data)\n        self.assertFalse(x_axis._use_time)\n        _ticks = x_axis.tickValues(1581953776, 1582953776, 1000)\n        ticks = x_axis.tickStrings(_ticks[0][1], 1, _ticks[0][0])\n        try:\n            float(ticks[0])\n        except ValueError:\n            self.fail(\"axis should display floats\")\n\n        self.send_signal(self.widget.Inputs.data, data_time)\n        self.assertTrue(x_axis._use_time)\n        _ticks = x_axis.tickValues(1581953776, 1582953776, 1000)\n        ticks = x_axis.tickStrings(_ticks[0][1], 1, _ticks[0][0])\n        with self.assertRaises(ValueError):\n            float(ticks[0])\n\n    def test_visual_settings(self):\n        super().test_visual_settings()\n\n        graph = self.widget.graph\n        font = QFont()\n        font.setItalic(True)\n        font.setFamily(\"Helvetica\")\n\n        key, value = ('Fonts', 'Axis title', 'Font size'), 16\n        self.widget.set_visual_settings(key, value)\n        key, value = ('Fonts', 'Axis title', 'Italic'), True\n        self.widget.set_visual_settings(key, value)\n        font.setPointSize(16)\n        for item in graph.parameter_setter.axis_items:\n            self.assertFontEqual(item.label.font(), font)\n\n        key, value = ('Fonts', 'Axis ticks', 'Font size'), 15\n        self.widget.set_visual_settings(key, value)\n        key, value = ('Fonts', 'Axis ticks', 'Italic'), True\n        self.widget.set_visual_settings(key, value)\n        font.setPointSize(15)\n        for item in graph.parameter_setter.axis_items:\n            self.assertFontEqual(item.style[\"tickFont\"], font)\n\n        self.widget.graph.controls.show_reg_line.setChecked(True)\n        self.assertGreater(len(graph.parameter_setter.reg_line_label_items), 0)\n\n        key, value = ('Fonts', 'Line label', 'Font size'), 16\n        self.widget.set_visual_settings(key, value)\n        key, value = ('Fonts', 'Line label', 'Italic'), True\n        self.widget.set_visual_settings(key, value)\n        font.setPointSize(16)\n        for label in graph.parameter_setter.reg_line_label_items:\n            self.assertFontEqual(label.textItem.font(), font)\n\n        key, value = ('Figure', 'Lines', 'Width'), 10\n        self.widget.set_visual_settings(key, value)\n        for item in graph.reg_line_items:\n            self.assertEqual(item.pen.width(), 10)\n\n\nif __name__ == \"__main__\":\n    import unittest\n    unittest.main()\n",
            "header": {
                "file": {
                    "path": "Orange/widgets/visualize/tests/test_owscatterplot.py"
                },
                "misc_line": null
            },
            "index_line": {
                "old_commit_hash": "9b6d4591f",
                "new_commit_hash": "b2974508b",
                "mode": "100644"
            },
            "is_binary_file": false,
            "binary_line": null,
            "minus_file": {
                "path": "a/Orange/widgets/visualize/tests/test_owscatterplot.py"
            },
            "plus_file": {
                "path": "b/Orange/widgets/visualize/tests/test_owscatterplot.py"
            },
            "hunks": [
                {
                    "descriptor": {
                        "old_range": {
                            "start": 743,
                            "length": 6
                        },
                        "new_range": {
                            "start": 743,
                            "length": 13
                        },
                        "section": "class TestOWScatterPlot(WidgetTest, ProjectionWidgetTestMixin,"
                    },
                    "line_group": {
                        "all_lines": [
                            {
                                "content": "        self.widget.setup_plot.assert_called_once()",
                                "type": "context"
                            },
                            {
                                "content": "        self.assertListEqual(self.widget.effective_variables, list(features))",
                                "type": "context"
                            },
                            {
                                "content": "",
                                "type": "context"
                            },
                            {
                                "content": "        self.widget.setup_plot.reset_mock()",
                                "type": "added"
                            },
                            {
                                "content": "        features = self.data.domain.attributes[2:]",
                                "type": "added"
                            },
                            {
                                "content": "        signals = [(self.widget.Inputs.features, AttributeList(features)),",
                                "type": "added"
                            },
                            {
                                "content": "                   (self.widget.Inputs.data, self.data)]",
                                "type": "added"
                            },
                            {
                                "content": "        self.send_signals(signals)",
                                "type": "added"
                            },
                            {
                                "content": "        self.widget.setup_plot.assert_called_once()",
                                "type": "added"
                            },
                            {
                                "content": "",
                                "type": "added"
                            },
                            {
                                "content": "    def test_invalidated_diff_features(self):",
                                "type": "context"
                            },
                            {
                                "content": "        self.widget.setup_plot = Mock()",
                                "type": "context"
                            },
                            {
                                "content": "        # send data and set default features",
                                "type": "context"
                            },
                            {
                                "content": "",
                                "type": "context"
                            }
                        ]
                    },
                    "modified_entities": [
                        {
                            "file_name": "Orange/widgets/visualize/tests/test_owscatterplot.py",
                            "type": "class",
                            "name": "TestOWScatterPlot",
                            "content": "class TestOWScatterPlot(WidgetTest, ProjectionWidgetTestMixin,\n                        WidgetOutputsTestMixin):\n    @classmethod\n    def setUpClass(cls):\n        super().setUpClass()\n        WidgetOutputsTestMixin.init(cls)\n        cls.same_input_output_domain = False\n\n        cls.signal_name = \"Data\"\n        cls.signal_data = cls.data\n\n    def setUp(self):\n        self.widget = self.create_widget(OWScatterPlot)\n\n    def test_set_data(self):\n        # Connect iris to scatter plot\n        self.send_signal(self.widget.Inputs.data, self.data)\n\n        # First two attribute should be selected as x an y\n        self.assertEqual(self.widget.attr_x, self.data.domain[0])\n        self.assertEqual(self.widget.attr_y, self.data.domain[1])\n\n        # Class var should be selected as color\n        self.assertIs(self.widget.attr_color, self.data.domain.class_var)\n\n        # Change which attributes are displayed\n        self.widget.attr_x = self.data.domain[2]\n        self.widget.attr_y = self.data.domain[3]\n\n        # Disconnect the data\n        self.send_signal(self.widget.Inputs.data, None)\n\n        # removing data should have cleared attributes\n        self.assertIsNone(self.widget.attr_x)\n        self.assertIsNone(self.widget.attr_y)\n        self.assertIsNone(self.widget.attr_color)\n\n        # and remove the legend\n        self.assertEqual(len(self.widget.graph.color_legend.items), 0)\n\n        # Connect iris again\n        # same attributes that were used last time should be selected\n        self.send_signal(self.widget.Inputs.data, self.data)\n\n        self.assertIs(self.widget.attr_x, self.data.domain[2])\n        self.assertIs(self.widget.attr_y, self.data.domain[3])\n\n    def test_score_heuristics(self):\n        domain = Domain([ContinuousVariable(c) for c in \"abcd\"],\n                        DiscreteVariable(\"e\", values=\"ab\"))\n        a = np.arange(10).reshape((10, 1))\n        data = Table(domain, np.hstack([a, a, a, a]), a >= 5)\n        self.send_signal(self.widget.Inputs.data, data)\n        vizrank = ScatterPlotVizRank(self.widget)\n        self.assertEqual([x.name for x in vizrank.score_heuristic()],\n                         list(\"abcd\"))\n\n    def test_optional_combos(self):\n        domain = self.data.domain\n        d1 = Domain(domain.attributes[:2], domain.class_var,\n                    [domain.attributes[2]])\n        t1 = self.data.transform(d1)\n        self.send_signal(self.widget.Inputs.data, t1)\n        self.widget.graph.attr_size = domain.attributes[2]\n\n        d2 = Domain(domain.attributes[:2], domain.class_var,\n                    [domain.attributes[3]])\n        t2 = self.data.transform(d2)\n        self.send_signal(self.widget.Inputs.data, t2)\n\n    def test_error_message(self):\n        \"\"\"Check if error message appears and then disappears when\n        data is removed from input\"\"\"\n        data = self.data.copy()\n        with data.unlocked():\n            data.X[:, 0] = np.nan\n        self.send_signal(self.widget.Inputs.data, data)\n        self.assertTrue(self.widget.Warning.missing_coords.is_shown())\n        self.send_signal(self.widget.Inputs.data, None)\n        self.assertFalse(self.widget.Warning.missing_coords.is_shown())\n\n    def test_report_on_empty(self):\n        self.widget.report_plot = MagicMock()\n        self.widget.report_caption = MagicMock()\n        self.widget.report_items = MagicMock()\n        self.widget.send_report()  # Essentially, don't crash\n        self.widget.report_plot.assert_not_called()\n        self.widget.report_caption.assert_not_called()\n        self.widget.report_items.assert_not_called()\n\n    def test_data_column_nans(self):\n        \"\"\"\n        ValueError cannot convert float NaN to integer.\n        In case when all column values are NaN then it throws that error.\n        GH-2061\n        \"\"\"\n        table = datasets.data_one_column_nans()\n        self.send_signal(self.widget.Inputs.data, table)\n        cb_attr_color = self.widget.controls.attr_color\n        simulate.combobox_activate_item(cb_attr_color, \"b\")\n        simulate.combobox_activate_item(self.widget.cb_attr_x, \"a\")\n        simulate.combobox_activate_item(self.widget.cb_attr_y, \"a\")\n\n        #self.widget.update_graph()\n        self.widget.graph.reset_graph()\n\n    def test_data_column_infs(self):\n        \"\"\"\n        Scatter Plot should not crash on data with infinity values\n        GH-2707\n        GH-2684\n        \"\"\"\n        table = datasets.data_one_column_infs()\n        self.send_signal(self.widget.Inputs.data, table)\n        attr_x = self.widget.controls.attr_x\n        simulate.combobox_activate_item(attr_x, \"b\")\n\n    def test_regression_line(self):\n        \"\"\"It is possible to draw the line only for pair of continuous attrs\"\"\"\n        self.send_signal(self.widget.Inputs.data, self.data)\n        self.assertTrue(self.widget.cb_reg_line.isEnabled())\n        self.assertIsNone(self.widget.graph.reg_line_item)\n        self.widget.cb_reg_line.setChecked(True)\n        self.assertIsNotNone(self.widget.graph.reg_line_item)\n        self.widget.cb_attr_y.activated.emit(4)\n        self.widget.cb_attr_y.setCurrentIndex(4)\n        self.assertFalse(self.widget.cb_reg_line.isEnabled())\n        self.assertIsNone(self.widget.graph.reg_line_item)\n\n    def test_points_combo_boxes(self):\n        \"\"\"Check Point box combo models and values\"\"\"\n        self.send_signal(self.widget.Inputs.data, self.data)\n        controls = self.widget.controls\n\n        # color and label should contain all variables\n        # size should contain only continuous variables\n        # shape should contain only discrete variables\n        for var in self.data.domain.variables + self.data.domain.metas:\n            self.assertIn(var, controls.attr_color.model())\n            self.assertIn(var, controls.attr_label.model())\n            if var.is_continuous:\n                self.assertIn(var, controls.attr_size.model())\n                self.assertNotIn(var, controls.attr_shape.model())\n            if var.is_discrete:\n                self.assertNotIn(var, controls.attr_size.model())\n                self.assertIn(var, controls.attr_shape.model())\n\n        widget = self.create_widget(OWScatterPlot)\n        self.send_signal(self.widget.Inputs.data, self.data, widget=widget)\n        self.assertEqual(controls.attr_color.currentText(),\n                         self.data.domain.class_var.name)\n\n    def test_group_selections(self):\n        self.send_signal(self.widget.Inputs.data, self.data)\n        graph = self.widget.graph\n        points = graph.scatterplot_item.points()\n        sel_column = np.zeros((len(self.data), 1))\n\n        x = self.data.X\n\n        def selectedx():\n            return self.get_output(self.widget.Outputs.selected_data).X\n\n        def selected_groups():\n            return self.get_output(self.widget.Outputs.selected_data).metas[:, 0]\n\n        def annotated():\n            return self.get_output(self.widget.Outputs.annotated_data).metas\n\n        def annotations():\n            return self.get_output(self.widget.Outputs.annotated_data).domain.metas[0].values\n\n        # Select 0:5\n        graph.select(points[:5])\n        np.testing.assert_equal(selectedx(), x[:5])\n        np.testing.assert_equal(selected_groups(), np.zeros(5))\n        sel_column[:5] = 1\n        np.testing.assert_equal(annotated(), sel_column)\n        self.assertEqual(annotations(), (\"No\", \"Yes\", ))\n\n        # Shift-select 5:10; now we have groups 0:5 and 5:10\n        with self.modifiers(Qt.ShiftModifier):\n            graph.select(points[5:10])\n        np.testing.assert_equal(selectedx(), x[:10])\n        np.testing.assert_equal(selected_groups(), np.array([0] * 5 + [1] * 5))\n        sel_column[:5] = 0\n        sel_column[5:10] = 1\n        sel_column[10:] = 2\n        np.testing.assert_equal(annotated(), sel_column)\n        self.assertEqual(len(annotations()), 3)\n\n        # Select: 15:20; we have 15:20\n        graph.select(points[15:20])\n        sel_column = np.zeros((len(self.data), 1))\n        sel_column[15:20] = 1\n        np.testing.assert_equal(selectedx(), x[15:20])\n        np.testing.assert_equal(selected_groups(), np.zeros(5))\n        self.assertEqual(annotations(), (\"No\", \"Yes\"))\n\n        # Alt-select (remove) 10:17; we have 17:20\n        with self.modifiers(Qt.AltModifier):\n            graph.select(points[10:17])\n        np.testing.assert_equal(selectedx(), x[17:20])\n        np.testing.assert_equal(selected_groups(), np.zeros(3))\n        sel_column[15:17] = 0\n        np.testing.assert_equal(annotated(), sel_column)\n        self.assertEqual(annotations(), (\"No\", \"Yes\"))\n\n        # Ctrl-Shift-select (add-to-last) 10:17; we have 17:25\n        with self.modifiers(Qt.ShiftModifier | Qt.ControlModifier):\n            graph.select(points[20:25])\n        np.testing.assert_equal(selectedx(), x[17:25])\n        np.testing.assert_equal(selected_groups(), np.zeros(8))\n        sel_column[20:25] = 1\n        np.testing.assert_equal(annotated(), sel_column)\n        self.assertEqual(annotations(), (\"No\", \"Yes\"))\n\n        # Shift-select (add) 30:35; we have 17:25, 30:35\n        with self.modifiers(Qt.ShiftModifier):\n            graph.select(points[30:35])\n        # ... then Ctrl-Shift-select (add-to-last) 10:17; we have 17:25, 30:40\n        with self.modifiers(Qt.ShiftModifier | Qt.ControlModifier):\n            graph.select(points[35:40])\n        sel_column[:] = 2\n        sel_column[17:25] = 0\n        sel_column[30:40] = 1\n        np.testing.assert_equal(selected_groups(), np.array([0] * 8 + [1] * 10))\n        np.testing.assert_equal(annotated(), sel_column)\n        self.assertEqual(len(annotations()), 3)\n\n    def test_saving_selection(self):\n        self.send_signal(self.widget.Inputs.data, self.data)  # iris\n        self.widget.graph.select_by_rectangle(QRectF(4, 3, 3, 1))\n        selected_inds = np.flatnonzero(self.widget.graph.selection)\n        settings = self.widget.settingsHandler.pack_data(self.widget)\n        np.testing.assert_equal(selected_inds,\n                                [i for i, g in settings[\"selection\"]])\n\n    def test_points_selection(self):\n        # Opening widget with saved selection should restore it\n        self.widget = self.create_widget(\n            OWScatterPlot, stored_settings={\n                \"selection_group\": [(i, 1) for i in range(50)]}\n        )\n        self.send_signal(self.widget.Inputs.data, self.data)  # iris\n        selected_data = self.get_output(self.widget.Outputs.selected_data)\n        self.assertEqual(len(selected_data), 50)\n\n        # Changing the dataset should clear selection\n        titanic = Table(\"titanic\")\n        self.send_signal(self.widget.Inputs.data, titanic)\n        selected_data = self.get_output(self.widget.Outputs.selected_data)\n        self.assertIsNone(selected_data)\n\n    def test_migrate_selection(self):\n        settings = dict(selection=list(range(2)))\n        OWScatterPlot.migrate_settings(settings, 0)\n        self.assertEqual(settings[\"selection_group\"], [(0, 1), (1, 1)])\n\n    def test_invalid_points_selection(self):\n        # if selection contains rows that are not present in the current\n        # dataset, widget should select what can be selected.\n        self.widget = self.create_widget(\n            OWScatterPlot, stored_settings={\n                \"selection_group\": [(i, 1) for i in range(50)]}\n        )\n        data = self.data[:11].copy()\n        with data.unlocked():\n            data[0, 0] = np.nan\n        self.send_signal(self.widget.Inputs.data, data)\n        self.assertIsNone(self.get_output(self.widget.Outputs.selected_data))\n\n    def test_set_strings_settings(self):\n        \"\"\"\n        Test if settings can be loaded as strings and successfully put\n        in new owplotgui combos.\n        \"\"\"\n        self.send_signal(self.widget.Inputs.data, self.data)\n        settings = self.widget.settingsHandler.pack_data(self.widget)\n        plot_settings = settings[\"context_settings\"][0].values\n        plot_settings[\"attr_label\"] = (\"sepal length\", -2)\n        plot_settings[\"attr_color\"] = (\"sepal width\", -2)\n        plot_settings[\"attr_shape\"] = (\"iris\", -2)\n        plot_settings[\"attr_size\"] = (\"petal width\", -2)\n        w = self.create_widget(OWScatterPlot, stored_settings=settings)\n        self.send_signal(self.widget.Inputs.data, self.data, widget=w)\n        self.assertEqual(w.attr_label.name, \"sepal length\")\n        self.assertEqual(w.attr_color.name, \"sepal width\")\n        self.assertEqual(w.attr_shape.name, \"iris\")\n        self.assertEqual(w.attr_size.name, \"petal width\")\n\n    def test_features_and_no_data(self):\n        \"\"\"\n        Prevent crashing when features are sent but no data.\n        \"\"\"\n        domain = Table(\"iris\").domain\n        self.send_signal(self.widget.Inputs.features,\n                         AttributeList(domain.variables))\n        self.send_signal(self.widget.Inputs.features, None)\n\n    def test_features_and_data(self):\n        self.assertTrue(self.widget.attr_box.isEnabled())\n        self.send_signal(self.widget.Inputs.data, self.data)\n        x, y = self.widget.graph.scatterplot_item.getData()\n        np.testing.assert_array_equal(x, self.data.X[:, 0])\n        np.testing.assert_array_equal(y, self.data.X[:, 1])\n        self.send_signal(self.widget.Inputs.features,\n                         AttributeList(self.data.domain[2:]))\n        self.assertIs(self.widget.attr_x, self.data.domain[2])\n        self.assertIs(self.widget.attr_y, self.data.domain[3])\n        self.assertFalse(self.widget.attr_box.isEnabled())\n        self.assertFalse(self.widget.vizrank.isEnabled())\n        x, y = self.widget.graph.scatterplot_item.getData()\n        np.testing.assert_array_equal(x, self.data.X[:, 2])\n        np.testing.assert_array_equal(y, self.data.X[:, 3])\n\n        self.send_signal(self.widget.Inputs.data, None)\n        self.send_signal(self.widget.Inputs.data, self.data)\n        self.assertIs(self.widget.attr_x, self.data.domain[2])\n        self.assertIs(self.widget.attr_y, self.data.domain[3])\n        self.assertFalse(self.widget.attr_box.isEnabled())\n        self.assertFalse(self.widget.vizrank.isEnabled())\n\n        self.send_signal(self.widget.Inputs.features, None)\n        self.assertTrue(self.widget.attr_box.isEnabled())\n        self.assertTrue(self.widget.vizrank.isEnabled())\n\n    def test_output_features(self):\n        data = Table(\"iris\")\n        self.send_signal(self.widget.Inputs.data, data)\n\n        # This doesn't work because combo's callbacks are connected to signal\n        # `activated`, which is only triggered by user interaction, and not to\n        # `currentIndexChanged`\n        # combo_y = self.widget.controls.attr_y\n        # combo_y.setCurrentIndex(combo_y.model().indexOf(data.domain[3]))\n        # This is a workaround\n        self.widget.attr_y = data.domain[3]\n        self.widget.attr_changed()\n\n        features = self.get_output(self.widget.Outputs.features)\n        self.assertEqual(features, [data.domain[0], data.domain[3]])\n\n    def test_vizrank(self):\n        data = Table(\"iris\")\n        self.send_signal(self.widget.Inputs.data, data)\n        vizrank = ScatterPlotVizRank(self.widget)\n        n_states = len(data.domain.attributes)\n        n_states = n_states * (n_states - 1) / 2\n        states = [state for state in vizrank.iterate_states(None)]\n        self.assertEqual(len(states), n_states)\n        self.assertEqual(len(set(states)), n_states)\n        self.assertIsNotNone(vizrank.compute_score(states[0]))\n        self.send_signal(self.widget.Inputs.data, data[:9])\n        self.assertIsNone(vizrank.compute_score(states[0]))\n\n        data = Table(\"housing\")[::10]\n        self.send_signal(self.widget.Inputs.data, data)\n        vizrank = ScatterPlotVizRank(self.widget)\n        states = [state for state in vizrank.iterate_states(None)]\n        self.assertIsNotNone(vizrank.compute_score(states[0]))\n\n    def test_vizrank_class_nan(self):\n        \"\"\"\n        When class values are nan, vizrank should be disabled. It should behave like\n        the class column is missing.\n        GH-2757\n        \"\"\"\n        def assert_vizrank_enabled(data, is_enabled):\n            self.send_signal(self.widget.Inputs.data, data)\n            self.assertEqual(is_enabled, self.widget.vizrank_button.isEnabled())\n\n        data1 = Table(\"iris\")[::30]\n        data2 = Table(\"iris\")[::30].copy()\n        with data2.unlocked():\n            data2.Y[:] = np.nan\n        domain = Domain(\n            attributes=data2.domain.attributes[:4], class_vars=DiscreteVariable(\"iris\", values=()))\n        data2 = Table(domain, data2.X, Y=data2.Y)\n        data3 = Table(\"iris\")[::30].copy()\n        with data3.unlocked():\n            data3.Y[:] = np.nan\n\n        for data, is_enabled in zip([data1, data2, data1, data3, data1],\n                                    [True, False, True, False, True]):\n            assert_vizrank_enabled(data, is_enabled)\n\n    def test_vizrank_nonprimitives(self):\n        \"\"\"VizRank does not try to include non primitive attributes\"\"\"\n        data = Table(\"zoo\")\n        self.send_signal(self.widget.Inputs.data, data)\n        with patch(\"Orange.widgets.visualize.owscatterplot.ReliefF\",\n                   new=lambda *_1, **_2: lambda data: np.arange(len(data))):\n            self.widget.vizrank.score_heuristic()\n\n    def test_vizrank_enabled(self):\n        self.send_signal(self.widget.Inputs.data, self.data)\n        self.assertTrue(self.widget.vizrank_button.isEnabled())\n        self.assertEqual(self.widget.vizrank_button.toolTip(), \"\")\n        self.assertTrue(self.widget.vizrank.button.isEnabled())\n        self.widget.vizrank.button.click()\n\n    def test_vizrank_enabled_no_data(self):\n        self.send_signal(self.widget.Inputs.data, None)\n        self.assertFalse(self.widget.vizrank_button.isEnabled())\n        self.assertEqual(self.widget.vizrank_button.toolTip(), \"No data on input\")\n\n    def test_vizrank_enabled_sparse_data(self):\n        self.send_signal(self.widget.Inputs.data, self.data.to_sparse())\n        self.assertFalse(self.widget.vizrank_button.isEnabled())\n        self.assertEqual(self.widget.vizrank_button.toolTip(), \"Data is sparse\")\n\n    def test_vizrank_enabled_constant_data(self):\n        domain = Domain([ContinuousVariable(\"c1\"),\n                         ContinuousVariable(\"c2\"),\n                         ContinuousVariable(\"c3\"),\n                         ContinuousVariable(\"c4\")],\n                        DiscreteVariable(\"cls\", values=(\"a\", \"b\")))\n        X = np.zeros((10, 4))\n        table = Table(domain, X, np.random.randint(2, size=10))\n        self.send_signal(self.widget.Inputs.data, table)\n        self.assertEqual(self.widget.vizrank_button.toolTip(), \"\")\n        self.assertTrue(self.widget.vizrank_button.isEnabled())\n        self.assertTrue(self.widget.vizrank.button.isEnabled())\n        self.widget.vizrank.button.click()\n\n    def test_vizrank_enabled_two_features(self):\n        self.send_signal(self.widget.Inputs.data, self.data[:, :2])\n        self.assertFalse(self.widget.vizrank_button.isEnabled())\n        self.assertEqual(self.widget.vizrank_button.toolTip(),\n                         \"Not enough features for ranking\")\n\n    def test_vizrank_enabled_no_color_var(self):\n        self.send_signal(self.widget.Inputs.data, self.data[:, :3])\n        self.assertFalse(self.widget.vizrank_button.isEnabled())\n        self.assertEqual(self.widget.vizrank_button.toolTip(),\n                         \"Color variable is not selected\")\n\n    def test_vizrank_enabled_color_var_nans(self):\n        domain = Domain([ContinuousVariable(\"c1\"),\n                         ContinuousVariable(\"c2\"),\n                         ContinuousVariable(\"c3\"),\n                         ContinuousVariable(\"c4\")],\n                        DiscreteVariable(\"cls\", values=(\"a\", \"b\")))\n        table = Table(domain, np.random.random((10, 4)), np.full(10, np.nan))\n        self.send_signal(self.widget.Inputs.data, table)\n        self.assertFalse(self.widget.vizrank_button.isEnabled())\n        self.assertEqual(self.widget.vizrank_button.toolTip(),\n                         \"Color variable has no values\")\n\n    def test_auto_send_selection(self):\n        \"\"\"\n        Scatter Plot automatically sends selection only when the checkbox Send automatically\n        is checked.\n        GH-2649\n        GH-2646\n        \"\"\"\n        data = Table(\"iris\")\n        self.send_signal(self.widget.Inputs.data, data)\n        self.widget.controls.auto_commit.setChecked(False)\n        self.assertFalse(self.widget.controls.auto_commit.isChecked())\n        self._select_data()\n        self.assertIsNone(self.get_output(self.widget.Outputs.selected_data))\n        self.widget.controls.auto_commit.setChecked(True)\n        output = self.get_output(self.widget.Outputs.selected_data)\n        self.assertIsInstance(output, Table)\n\n    def test_color_is_optional(self):\n        zoo = Table(\"zoo\")\n        backbone, breathes, airborne, type = \\\n            [zoo.domain[x] for x in [\"backbone\", \"breathes\", \"airborne\", \"type\"]]\n        default_x, default_y, default_color = \\\n            zoo.domain[0], zoo.domain[1], zoo.domain.class_var\n        attr_x = self.widget.controls.attr_x\n        attr_y = self.widget.controls.attr_y\n        attr_color = self.widget.controls.attr_color\n\n        # Send dataset, ensure defaults are what we expect them to be\n        self.send_signal(self.widget.Inputs.data, zoo)\n        self.assertEqual(attr_x.currentText(), default_x.name)\n        self.assertEqual(attr_y.currentText(), default_y.name)\n        self.assertEqual(attr_color.currentText(), default_color.name)\n        # Select different values\n        simulate.combobox_activate_item(attr_x, backbone.name)\n        simulate.combobox_activate_item(attr_y, breathes.name)\n        simulate.combobox_activate_item(attr_color, airborne.name)\n\n        # Send compatible dataset, values should not change\n        zoo2 = zoo[:, (backbone, breathes, airborne, type)]\n        self.send_signal(self.widget.Inputs.data, zoo2)\n        self.assertEqual(attr_x.currentText(), backbone.name)\n        self.assertEqual(attr_y.currentText(), breathes.name)\n        self.assertEqual(attr_color.currentText(), airborne.name)\n\n        # Send dataset without color variable\n        # x and y should remain, color reset to default\n        zoo3 = zoo[:, (backbone, breathes, type)]\n        self.send_signal(self.widget.Inputs.data, zoo3)\n        self.assertEqual(attr_x.currentText(), backbone.name)\n        self.assertEqual(attr_y.currentText(), breathes.name)\n        self.assertEqual(attr_color.currentText(), default_color.name)\n\n        # Send dataset without x\n        # y and color should be the same as with zoo\n        zoo4 = zoo[:, (default_x, default_y, breathes, airborne, type)]\n        self.send_signal(self.widget.Inputs.data, zoo4)\n        self.assertEqual(attr_x.currentText(), default_x.name)\n        self.assertEqual(attr_y.currentText(), default_y.name)\n        self.assertEqual(attr_color.currentText(), default_color.name)\n\n        # Send dataset compatible with zoo2 and zoo3\n        # Color should reset to one in zoo3, as it was used more\n        # recently\n        zoo5 = zoo[:, (default_x, backbone, breathes, airborne, type)]\n        self.send_signal(self.widget.Inputs.data, zoo5)\n        self.assertEqual(attr_x.currentText(), backbone.name)\n        self.assertEqual(attr_y.currentText(), breathes.name)\n        self.assertEqual(attr_color.currentText(), type.name)\n\n    def test_handle_metas(self):\n        \"\"\"\n        Scatter Plot Graph can handle metas\n        GH-2699\n        \"\"\"\n        w = self.widget\n        data = Table(\"iris\")\n        domain = Domain(\n            attributes=data.domain.attributes[:2],\n            class_vars=data.domain.class_vars,\n            metas=data.domain.attributes[2:]\n        )\n        data = data.transform(domain).copy()\n        # Sometimes floats in metas are saved as objects\n        with data.unlocked():\n            data.metas = data.metas.astype(object)\n        self.send_signal(w.Inputs.data, data)\n        simulate.combobox_activate_item(w.cb_attr_x, data.domain.metas[1].name)\n        simulate.combobox_activate_item(w.controls.attr_color, data.domain.metas[0].name)\n        w.graph.reset_graph()\n\n    def test_subset_data(self):\n        \"\"\"\n        Scatter Plot subset data is sent to Scatter Plot Graph\n        GH-2773\n        \"\"\"\n        data = Table(\"iris\")\n        w = self.widget\n        self.send_signal(w.Inputs.data, data)\n        self.send_signal(w.Inputs.data_subset, data[::30])\n        self.assertEqual(len(w.subset_indices), 5)\n\n    def test_opacity_warning(self):\n        data = Table(\"iris\")\n        w = self.widget\n        self.send_signal(w.Inputs.data, data)\n        w.graph.controls.alpha_value.setSliderPosition(10)\n        self.assertFalse(w.Warning.transparent_subset.is_shown())\n        self.send_signal(w.Inputs.data_subset, data[::30])\n        self.assertTrue(w.Warning.transparent_subset.is_shown())\n        w.graph.controls.alpha_value.setSliderPosition(200)\n        self.assertFalse(w.Warning.transparent_subset.is_shown())\n        w.graph.controls.alpha_value.setSliderPosition(10)\n        self.assertTrue(w.Warning.transparent_subset.is_shown())\n        self.send_signal(w.Inputs.data_subset, None)\n        self.assertFalse(w.Warning.transparent_subset.is_shown())\n\n    def test_jittering(self):\n        self.send_signal(self.widget.Inputs.data, self.data)\n        self.widget.graph.controls.jitter_continuous.setChecked(True)\n        self.widget.graph.controls.jitter_size.setValue(1)\n\n    def test_metas_zero_column(self):\n        \"\"\"\n        Prevent crash when metas column is zero.\n        GH-2775\n        \"\"\"\n        data = Table(\"iris\")\n        domain = data.domain\n        domain = Domain(domain.attributes[:3], domain.class_vars, domain.attributes[3:])\n        data = data.transform(domain).copy()\n        with data.unlocked():\n            data.metas[:, 0] = 0\n        w = self.widget\n        self.send_signal(w.Inputs.data, data)\n        simulate.combobox_activate_item(w.controls.attr_x, domain.metas[0].name)\n\n    def test_tooltip(self):\n        # The test tests presence of some data,\n        # but avoids checking the exact format\n        data = Table(\"heart_disease\")\n        self.send_signal(self.widget.Inputs.data, data)\n        widget = self.widget\n        graph = widget.graph\n        scatterplot_item = graph.scatterplot_item\n\n        widget.controls.attr_x = data.domain[\"chest pain\"]\n        widget.controls.attr_y = data.domain[\"cholesterol\"]\n        all_points = scatterplot_item.points()\n\n        event = MagicMock()\n        with patch.object(scatterplot_item, \"mapFromScene\"), \\\n                patch.object(QToolTip, \"showText\") as show_text:\n\n            # Single point hovered\n            with patch.object(scatterplot_item, \"pointsAt\",\n                              return_value=[all_points[42]]):\n                # Show just x and y attribute\n                widget.tooltip_shows_all = False\n                self.assertTrue(graph.help_event(event))\n                (_, text), _ = show_text.call_args\n                self.assertIn(\"age = {}\".format(data[42, \"age\"]), text)\n                self.assertIn(\"gender = {}\".format(data[42, \"gender\"]), text)\n                self.assertNotIn(\"max HR = {}\".format(data[42, \"max HR\"]), text)\n                self.assertNotIn(\"others\", text)\n\n                # Show all attributes\n                widget.tooltip_shows_all = True\n                self.assertTrue(graph.help_event(event))\n                (_, text), _ = show_text.call_args\n                self.assertIn(\"age = {}\".format(data[42, \"age\"]), text)\n                self.assertIn(\"gender = {}\".format(data[42, \"gender\"]), text)\n                self.assertIn(\"max HR = {}\".format(data[42, \"max HR\"]), text)\n                self.assertIn(\"... and 4 others\", text)\n\n            # Two points hovered\n            with patch.object(scatterplot_item, \"pointsAt\",\n                              return_value=[all_points[42], all_points[100]]):\n                self.assertTrue(graph.help_event(event))\n                (_, text), _ = show_text.call_args\n                self.assertIn(\"age = {}\".format(data[42, \"age\"]), text)\n                self.assertIn(\"gender = {}\".format(data[42, \"gender\"]), text)\n                self.assertIn(\"age = {}\".format(data[100, \"age\"]), text)\n                self.assertIn(\"gender = {}\".format(data[100, \"gender\"]), text)\n\n            # No points hovered\n            with patch.object(scatterplot_item, \"pointsAt\",\n                              return_value=[]):\n                show_text.reset_mock()\n                self.assertFalse(graph.help_event(event))\n                self.assertEqual(show_text.call_count, 0)\n\n    def test_many_discrete_values(self):\n        \"\"\"\n        Do not show all discrete values if there are too many.\n        Also test for values with a nan.\n        GH-2804\n        \"\"\"\n        def prepare_data():\n            data = Table(\"iris\")\n            values = list(range(15))\n            class_var = DiscreteVariable(\"iris5\", values=[str(v) for v in values])\n            data = data.transform(\n                Domain(attributes=data.domain.attributes,\n                       class_vars=[class_var])).copy()\n            with data.unlocked():\n                data.Y = np.array(values * 10, dtype=float)\n            return data\n\n        def assert_equal(data, max):\n            self.send_signal(self.widget.Inputs.data, data)\n            pen_data, brush_data = self.widget.graph.get_colors()\n            self.assertEqual(max, len(np.unique([id(p) for p in pen_data])), )\n\n        assert_equal(prepare_data(), MAX_COLORS)\n        # data with nan value\n        data = prepare_data()\n        with data.unlocked():\n            data.Y[42] = np.nan\n        assert_equal(data, MAX_COLORS + 1)\n\n    def test_invalidated_same_features(self):\n        self.widget.setup_plot = Mock()\n        # send data and set default features\n        self.send_signal(self.widget.Inputs.data, self.data)\n        self.widget.setup_plot.assert_called_once()\n        self.assertListEqual(self.widget.effective_variables,\n                             list(self.data.domain.attributes[:2]))\n\n        # send the same features as already set\n        self.widget.setup_plot.reset_mock()\n        self.send_signal(self.widget.Inputs.features,\n                         AttributeList(self.data.domain.attributes[:2]))\n        self.widget.setup_plot.assert_not_called()\n        self.assertListEqual(self.widget.effective_variables,\n                             list(self.data.domain.attributes[:2]))\n\n    def test_invalidated_same_time(self):\n        self.widget.setup_plot = Mock()\n        # send data and features at the same time (data first)\n        features = self.data.domain.attributes[:2]\n        signals = [(self.widget.Inputs.data, self.data),\n                   (self.widget.Inputs.features, AttributeList(features))]\n        self.send_signals(signals)\n        self.widget.setup_plot.assert_called_once()\n        self.assertListEqual(self.widget.effective_variables, list(features))\n\n    def test_invalidated_features_first(self):\n        self.widget.setup_plot = Mock()\n        # send features (same as default ones)\n        self.send_signal(self.widget.Inputs.features,\n                         AttributeList(self.data.domain.attributes[:2]))\n        self.assertListEqual(self.widget.effective_variables, [])\n        self.widget.setup_plot.assert_called_once()\n\n        # send data\n        self.widget.setup_plot.reset_mock()\n        self.send_signal(self.widget.Inputs.data, self.data)\n        self.widget.setup_plot.assert_called()\n        self.assertListEqual(self.widget.effective_variables,\n                             list(self.data.domain.attributes[:2]))\n\n    def test_invalidated_same_time_features_first(self):\n        self.widget.setup_plot = Mock()\n        # send features and data at the same time (features first)\n        features = self.data.domain.attributes[:2]\n        signals = [(self.widget.Inputs.features, AttributeList(features)),\n                   (self.widget.Inputs.data, self.data)]\n        self.send_signals(signals)\n        self.widget.setup_plot.assert_called_once()\n        self.assertListEqual(self.widget.effective_variables, list(features))\n\n        self.widget.setup_plot.reset_mock()\n        features = self.data.domain.attributes[2:]\n        signals = [(self.widget.Inputs.features, AttributeList(features)),\n                   (self.widget.Inputs.data, self.data)]\n        self.send_signals(signals)\n        self.widget.setup_plot.assert_called_once()\n\n    def test_invalidated_diff_features(self):\n        self.widget.setup_plot = Mock()\n        # send data and set default features\n        self.send_signal(self.widget.Inputs.data, self.data)\n        self.widget.setup_plot.assert_called_once()\n        self.assertListEqual(self.widget.effective_variables,\n                             list(self.data.domain.attributes[:2]))\n\n        # send different features\n        self.widget.setup_plot.reset_mock()\n        self.send_signal(self.widget.Inputs.features,\n                         AttributeList(self.data.domain.attributes[2:4]))\n        self.widget.setup_plot.assert_called_once()\n        self.assertListEqual(self.widget.effective_variables,\n                             list(self.data.domain.attributes[2:4]))\n\n    def test_invalidated_diff_features_same_time(self):\n        self.widget.setup_plot = Mock()\n        # send data and different features at the same time (data first)\n        features = self.data.domain.attributes[2:4]\n        signals = [(self.widget.Inputs.data, self.data),\n                   (self.widget.Inputs.features, AttributeList(features))]\n        self.send_signals(signals)\n        self.widget.setup_plot.assert_called_once()\n        self.assertListEqual(self.widget.effective_variables, list(features))\n\n    def test_invalidated_diff_features_features_first(self):\n        self.widget.setup_plot = Mock()\n        # send features (not the same as defaults)\n        self.send_signal(self.widget.Inputs.features,\n                         AttributeList(self.data.domain.attributes[2:4]))\n        self.assertListEqual(self.widget.effective_variables, [])\n        self.widget.setup_plot.assert_called_once()\n\n        # send data\n        self.widget.setup_plot.reset_mock()\n        self.send_signal(self.widget.Inputs.data, self.data)\n        self.widget.setup_plot.assert_called_once()\n        self.assertListEqual(self.widget.effective_variables,\n                             list(self.data.domain.attributes[2:4]))\n\n    def test_invalidated_diff_features_same_time_features_first(self):\n        self.widget.setup_plot = Mock()\n        # send data and different features at the same time (features first)\n        features = self.data.domain.attributes[2:4]\n        signals = [(self.widget.Inputs.features, AttributeList(features)),\n                   (self.widget.Inputs.data, self.data)]\n        self.send_signals(signals)\n        self.widget.setup_plot.assert_called_once()\n        self.assertListEqual(self.widget.effective_variables, list(features))\n\n    @patch('Orange.widgets.visualize.owscatterplot.ScatterPlotVizRank.'\n           'on_manual_change')\n    def test_vizrank_receives_manual_change(self, on_manual_change):\n        # Recreate the widget so the patch kicks in\n        self.widget = self.create_widget(OWScatterPlot)\n        data = Table(\"iris.tab\")\n        self.send_signal(self.widget.Inputs.data, data)\n        model = self.widget.controls.attr_x.model()\n        self.widget.attr_x = model[0]\n        self.widget.attr_y = model[1]\n        simulate.combobox_activate_index(self.widget.controls.attr_x, 2)\n        self.assertIs(self.widget.attr_x, model[2])\n        on_manual_change.assert_called_with(model[2], model[1])\n\n    def test_on_manual_change(self):\n        data = Table(\"iris.tab\")\n        self.send_signal(self.widget.Inputs.data, data)\n        vizrank = self.widget.vizrank\n        vizrank.toggle()\n        self.process_events(until=lambda: not vizrank.keep_running)\n\n        model = vizrank.rank_model\n        attrs = model.data(model.index(3, 0), vizrank._AttrRole)\n        vizrank.on_manual_change(*attrs)\n        selection = vizrank.rank_table.selectedIndexes()\n        self.assertEqual(len(selection), 1)\n        self.assertEqual(selection[0].row(), 3)\n\n        vizrank.on_manual_change(*attrs[::-1])\n        selection = vizrank.rank_table.selectedIndexes()\n        self.assertEqual(len(selection), 0)\n\n    def test_regression_lines_appear(self):\n        self.widget.graph.controls.show_reg_line.setChecked(True)\n        self.assertEqual(len(self.widget.graph.reg_line_items), 0)\n        self.send_signal(self.widget.Inputs.data, self.data)\n        self.assertEqual(len(self.widget.graph.reg_line_items), 4)\n        simulate.combobox_activate_index(self.widget.controls.attr_color, 0)\n        self.assertEqual(len(self.widget.graph.reg_line_items), 1)\n        data = self.data.copy()\n        with data.unlocked():\n            data[:, 0] = np.nan\n        self.send_signal(self.widget.Inputs.data, data)\n        self.assertEqual(len(self.widget.graph.reg_line_items), 0)\n\n    def test_regression_line_coeffs(self):\n        widget = self.widget\n        graph = widget.graph\n        xy = np.array([[0, 0], [1, 0], [1, 2], [2, 2],\n                       [0, 1], [1, 3], [2, 5]], dtype=float)\n        colors = np.array([0, 0, 0, 0, 1, 1, 1], dtype=float)\n        widget.get_coordinates_data = lambda: xy.T\n        widget.can_draw_regresssion_line = lambda: True\n        widget.get_color_data = lambda: colors\n        widget.is_continuous_color = lambda: False\n        graph.palette = DefaultRGBColors\n        graph.controls.show_reg_line.setChecked(True)\n\n        graph.update_regression_line()\n\n        line1 = graph.reg_line_items[1]\n        self.assertEqual(line1.pos().x(), 0)\n        self.assertEqual(line1.pos().y(), 0)\n        self.assertEqual(line1.angle, 45)\n        self.assertEqual(line1.pen.color().hue(), graph.palette[0].hue())\n\n        line2 = graph.reg_line_items[2]\n        self.assertEqual(line2.pos().x(), 0)\n        self.assertEqual(line2.pos().y(), 1)\n        self.assertAlmostEqual(line2.angle, np.degrees(np.arctan2(2, 1)))\n        self.assertEqual(line2.pen.color().hue(), graph.palette[1].hue())\n\n        graph.orthonormal_regression = True\n        graph.update_regression_line()\n\n        line1 = graph.reg_line_items[1]\n        self.assertEqual(line1.pos().x(), 0)\n        self.assertAlmostEqual(line1.pos().y(), -0.6180339887498949)\n        self.assertAlmostEqual(line1.angle, 58.28252558853899)\n        self.assertEqual(line1.pen.color().hue(), graph.palette[0].hue())\n\n        line2 = graph.reg_line_items[2]\n        self.assertEqual(line2.pos().x(), 0)\n        self.assertEqual(line2.pos().y(), 1)\n        self.assertAlmostEqual(line2.angle, np.degrees(np.arctan2(2, 1)))\n        self.assertEqual(line2.pen.color().hue(), graph.palette[1].hue())\n\n    def test_orthonormal_line(self):\n        color = QColor(1, 2, 3)\n        width = 42\n        # Normal line\n        line = OWScatterPlotGraph._orthonormal_line(\n            np.array([0, 1, 1, 2]), np.array([0, 0, 2, 2]), color, width)\n        self.assertEqual(line.pos().x(), 0)\n        self.assertAlmostEqual(line.pos().y(), -0.6180339887498949)\n        self.assertAlmostEqual(line.angle, 58.28252558853899)\n        self.assertEqual(line.pen.color(), color)\n        self.assertEqual(line.pen.width(), width)\n\n        # Normal line, negative slope\n        line = OWScatterPlotGraph._orthonormal_line(\n            np.array([1, 2, 3]), np.array([3, 2, 1]), color, width)\n        self.assertEqual(line.pos().x(), 1)\n        self.assertEqual(line.pos().y(), 3)\n        self.assertEqual(line.angle % 360, 315)\n\n        # Horizontal line\n        line = OWScatterPlotGraph._orthonormal_line(\n            np.array([10, 11, 12]), np.array([42, 42, 42]), color, width)\n        self.assertEqual(line.pos().x(), 10)\n        self.assertEqual(line.pos().y(), 42)\n        self.assertEqual(line.angle, 0)\n\n        # Vertical line\n        line = OWScatterPlotGraph._orthonormal_line(\n            np.array([42, 42, 42]), np.array([10, 11, 12]), color, width)\n        self.assertEqual(line.pos().x(), 42)\n        self.assertEqual(line.pos().y(), 10)\n        self.assertEqual(line.angle, 90)\n\n        # No line because all points coincide\n        line = OWScatterPlotGraph._orthonormal_line(\n            np.array([1, 1, 1]), np.array([42, 42, 42]), color, width)\n        self.assertIsNone(line)\n\n        # No line because the group is symmetric\n        line = OWScatterPlotGraph._orthonormal_line(\n            np.array([1, 1, 2, 2]), np.array([42, 5, 5, 42]), color, width)\n        self.assertIsNone(line)\n\n    def test_regression_line(self):\n        color = QColor(1, 2, 3)\n        width = 42\n        # Normal line\n        line = OWScatterPlotGraph._regression_line(\n            np.array([0, 1, 1, 2]), np.array([0, 0, 2, 2]), color, width)\n        self.assertEqual(line.pos().x(), 0)\n        self.assertAlmostEqual(line.pos().y(), 0)\n        self.assertEqual(line.angle, 45)\n        self.assertEqual(line.pen.color(), color)\n        self.assertEqual(line.pen.width(), width)\n\n        # Normal line, negative slope\n        line = OWScatterPlotGraph._regression_line(\n            np.array([1, 2, 3]), np.array([3, 2, 1]), color, width)\n        self.assertEqual(line.pos().x(), 1)\n        self.assertEqual(line.pos().y(), 3)\n        self.assertEqual(line.angle % 360, 315)\n\n        # Horizontal line\n        line = OWScatterPlotGraph._regression_line(\n            np.array([10, 11, 12]), np.array([42, 42, 42]), color, width)\n        self.assertEqual(line.pos().x(), 10)\n        self.assertEqual(line.pos().y(), 42)\n        self.assertEqual(line.angle, 0)\n\n        # Vertical line\n        line = OWScatterPlotGraph._regression_line(\n            np.array([42, 42, 42]), np.array([10, 11, 12]), color, width)\n        self.assertIsNone(line)\n\n        # No line because all points coincide\n        line = OWScatterPlotGraph._regression_line(\n            np.array([1, 1, 1]), np.array([42, 42, 42]), color, width)\n        self.assertIsNone(line)\n\n    def test_add_line_calls_proper_regressor(self):\n        graph = self.widget.graph\n        graph._orthonormal_line = Mock(return_value=None)\n        graph._regression_line = Mock(return_value=None)\n        x, y, c = Mock(), Mock(), Mock()\n\n        graph.orthonormal_regression = True\n        graph._add_line(x, y, c)\n        graph._orthonormal_line.assert_called_once_with(x, y, c, 3, 1)\n        graph._regression_line.assert_not_called()\n        graph._orthonormal_line.reset_mock()\n\n        graph.orthonormal_regression = False\n        graph._add_line(x, y, c)\n        graph._regression_line.assert_called_with(x, y, c, 3, 1)\n        graph._orthonormal_line.assert_not_called()\n\n    def test_no_regression_line(self):\n        graph = self.widget.graph\n        graph._orthonormal_line = lambda *_: None\n        graph.orthonormal_regression = True\n\n        graph.plot_widget.addItem = Mock()\n\n        x, y, c = Mock(), Mock(), Mock()\n        graph._add_line(x, y, c)\n        graph.plot_widget.addItem.assert_not_called()\n        self.assertEqual(graph.reg_line_items, [])\n\n    def test_update_regression_line_calls_add_line(self):\n        widget = self.widget\n        graph = widget.graph\n        x, y = np.array([[0, 0], [1, 0], [1, 2], [2, 2],\n                         [0, 1], [1, 3], [2, 5]], dtype=float).T\n        colors = np.array([0, 0, 0, 0, 1, 1, 1], dtype=float)\n        widget.get_coordinates_data = lambda: (x, y)\n        widget.can_draw_regresssion_line = lambda: True\n        widget.get_color_data = lambda: colors\n        widget.is_continuous_color = lambda: False\n        graph.palette = DefaultRGBColors\n        graph.controls.show_reg_line.setChecked(True)\n\n        graph._add_line = Mock()\n\n        graph.update_regression_line()\n        (args1, _), (args2, _), (args3, _) = graph._add_line.call_args_list\n        np.testing.assert_equal(args1[0], x)\n        np.testing.assert_equal(args1[1], y)\n        self.assertEqual(args1[2], QColor(\"#505050\"))\n\n        np.testing.assert_equal(args2[0], x[:4])\n        np.testing.assert_equal(args2[1], y[:4])\n        self.assertEqual(args2[2].hue(), graph.palette[0].hue())\n\n        np.testing.assert_equal(args3[0], x[4:])\n        np.testing.assert_equal(args3[1], y[4:])\n        self.assertEqual(args3[2].hue(), graph.palette[1].hue())\n        graph._add_line.reset_mock()\n\n        # Continuous color - just a single line\n        widget.is_continuous_color = lambda: True\n        graph.update_regression_line()\n        graph._add_line.assert_called_once()\n        args1, _ = graph._add_line.call_args_list[0]\n        np.testing.assert_equal(args1[0], x)\n        np.testing.assert_equal(args1[1], y)\n        self.assertEqual(args1[2].hue(), QColor(\"#505050\").hue())\n        graph._add_line.reset_mock()\n        widget.is_continuous_color = lambda: False\n\n        # No palette - just a single line\n        graph.palette = None\n        graph.update_regression_line()\n        graph._add_line.assert_called_once()\n        graph._add_line.reset_mock()\n        graph.palette = DefaultRGBColors\n\n        # Regression line is disabled\n        graph.show_reg_line = False\n        graph.update_regression_line()\n        graph._add_line.assert_not_called()\n        graph.show_reg_line = True\n\n        # No colors - just one line\n        widget.get_color_data = lambda: None\n        graph.update_regression_line()\n        graph._add_line.assert_called_once()\n        graph._add_line.reset_mock()\n\n        # No data\n        widget.get_coordinates_data = lambda: (None, None)\n        graph.update_regression_line()\n        graph._add_line.assert_not_called()\n        graph.show_reg_line = True\n        widget.get_coordinates_data = lambda: (x, y)\n\n        # One color group contains just one point - skip that line\n        widget.get_color_data = lambda: np.array([0] + [1] * (len(x) - 1))\n\n        graph.update_regression_line()\n        (args1, _), (args2, _) = graph._add_line.call_args_list\n        np.testing.assert_equal(args1[0], x)\n        np.testing.assert_equal(args1[1], y)\n        self.assertEqual(args1[2].hue(), QColor(\"#505050\").hue())\n\n        np.testing.assert_equal(args2[0], x[1:])\n        np.testing.assert_equal(args2[1], y[1:])\n        self.assertEqual(args2[2].hue(), graph.palette[1].hue())\n\n    def test_update_regression_line_is_called(self):\n        widget = self.widget\n        graph = widget.graph\n        urline = graph.update_regression_line = Mock()\n\n        self.send_signal(widget.Inputs.data, self.data)\n        urline.assert_called_once()\n        urline.reset_mock()\n\n        self.send_signal(widget.Inputs.data, None)\n        urline.assert_called_once()\n        urline.reset_mock()\n\n        self.send_signal(widget.Inputs.data, self.data)\n        urline.assert_called_once()\n        urline.reset_mock()\n\n        simulate.combobox_activate_index(self.widget.controls.attr_color, 0)\n        urline.assert_called_once()\n        urline.reset_mock()\n\n        simulate.combobox_activate_index(self.widget.controls.attr_color, 2)\n        urline.assert_called_once()\n        urline.reset_mock()\n\n        simulate.combobox_activate_index(self.widget.controls.attr_x, 3)\n        urline.assert_called_once()\n        urline.reset_mock()\n\n    def test_time_axis(self):\n        a = np.array([[1581953776, 1], [1581963776, 2], [1582953776, 3]])\n        d1 = Domain([ContinuousVariable(\"time\"), ContinuousVariable(\"value\")])\n        data = Table.from_numpy(d1, a)\n        d2 = Domain([TimeVariable(\"time\"), ContinuousVariable(\"value\")])\n        data_time = Table.from_numpy(d2, a)\n\n        x_axis = self.widget.graph.plot_widget.plotItem.getAxis(\"bottom\")\n\n        self.send_signal(self.widget.Inputs.data, data)\n        self.assertFalse(x_axis._use_time)\n        _ticks = x_axis.tickValues(1581953776, 1582953776, 1000)\n        ticks = x_axis.tickStrings(_ticks[0][1], 1, _ticks[0][0])\n        try:\n            float(ticks[0])\n        except ValueError:\n            self.fail(\"axis should display floats\")\n\n        self.send_signal(self.widget.Inputs.data, data_time)\n        self.assertTrue(x_axis._use_time)\n        _ticks = x_axis.tickValues(1581953776, 1582953776, 1000)\n        ticks = x_axis.tickStrings(_ticks[0][1], 1, _ticks[0][0])\n        with self.assertRaises(ValueError):\n            float(ticks[0])\n\n    def test_visual_settings(self):\n        super().test_visual_settings()\n\n        graph = self.widget.graph\n        font = QFont()\n        font.setItalic(True)\n        font.setFamily(\"Helvetica\")\n\n        key, value = ('Fonts', 'Axis title', 'Font size'), 16\n        self.widget.set_visual_settings(key, value)\n        key, value = ('Fonts', 'Axis title', 'Italic'), True\n        self.widget.set_visual_settings(key, value)\n        font.setPointSize(16)\n        for item in graph.parameter_setter.axis_items:\n            self.assertFontEqual(item.label.font(), font)\n\n        key, value = ('Fonts', 'Axis ticks', 'Font size'), 15\n        self.widget.set_visual_settings(key, value)\n        key, value = ('Fonts', 'Axis ticks', 'Italic'), True\n        self.widget.set_visual_settings(key, value)\n        font.setPointSize(15)\n        for item in graph.parameter_setter.axis_items:\n            self.assertFontEqual(item.style[\"tickFont\"], font)\n\n        self.widget.graph.controls.show_reg_line.setChecked(True)\n        self.assertGreater(len(graph.parameter_setter.reg_line_label_items), 0)\n\n        key, value = ('Fonts', 'Line label', 'Font size'), 16\n        self.widget.set_visual_settings(key, value)\n        key, value = ('Fonts', 'Line label', 'Italic'), True\n        self.widget.set_visual_settings(key, value)\n        font.setPointSize(16)\n        for label in graph.parameter_setter.reg_line_label_items:\n            self.assertFontEqual(label.textItem.font(), font)\n\n        key, value = ('Figure', 'Lines', 'Width'), 10\n        self.widget.set_visual_settings(key, value)\n        for item in graph.reg_line_items:\n            self.assertEqual(item.pen.width(), 10)",
                            "ast_type_str": "ClassDef",
                            "start_lineno": 25,
                            "end_lineno": 1171,
                            "parent": null
                        },
                        {
                            "file_name": "Orange/widgets/visualize/tests/test_owscatterplot.py",
                            "type": "method",
                            "name": "TestOWScatterPlot.test_invalidated_same_time_features_first",
                            "content": "class TestOWScatterPlot(WidgetTest, ProjectionWidgetTestMixin,\n                        WidgetOutputsTestMixin):\n    @classmethod\n    def setUpClass(cls):\n        super().setUpClass()\n        WidgetOutputsTestMixin.init(cls)\n        cls.same_input_output_domain = False\n\n        cls.signal_name = \"Data\"\n        cls.signal_data = cls.data\n\n    def setUp(self):\n        self.widget = self.create_widget(OWScatterPlot)\n\n    def test_set_data(self):\n        # Connect iris to scatter plot\n        self.send_signal(self.widget.Inputs.data, self.data)\n\n        # First two attribute should be selected as x an y\n        self.assertEqual(self.widget.attr_x, self.data.domain[0])\n        self.assertEqual(self.widget.attr_y, self.data.domain[1])\n\n        # Class var should be selected as color\n        self.assertIs(self.widget.attr_color, self.data.domain.class_var)\n\n        # Change which attributes are displayed\n        self.widget.attr_x = self.data.domain[2]\n        self.widget.attr_y = self.data.domain[3]\n\n        # Disconnect the data\n        self.send_signal(self.widget.Inputs.data, None)\n\n        # removing data should have cleared attributes\n        self.assertIsNone(self.widget.attr_x)\n        self.assertIsNone(self.widget.attr_y)\n        self.assertIsNone(self.widget.attr_color)\n\n        # and remove the legend\n        self.assertEqual(len(self.widget.graph.color_legend.items), 0)\n\n        # Connect iris again\n        # same attributes that were used last time should be selected\n        self.send_signal(self.widget.Inputs.data, self.data)\n\n        self.assertIs(self.widget.attr_x, self.data.domain[2])\n        self.assertIs(self.widget.attr_y, self.data.domain[3])\n\n    def test_score_heuristics(self):\n        domain = Domain([ContinuousVariable(c) for c in \"abcd\"],\n                        DiscreteVariable(\"e\", values=\"ab\"))\n        a = np.arange(10).reshape((10, 1))\n        data = Table(domain, np.hstack([a, a, a, a]), a >= 5)\n        self.send_signal(self.widget.Inputs.data, data)\n        vizrank = ScatterPlotVizRank(self.widget)\n        self.assertEqual([x.name for x in vizrank.score_heuristic()],\n                         list(\"abcd\"))\n\n    def test_optional_combos(self):\n        domain = self.data.domain\n        d1 = Domain(domain.attributes[:2], domain.class_var,\n                    [domain.attributes[2]])\n        t1 = self.data.transform(d1)\n        self.send_signal(self.widget.Inputs.data, t1)\n        self.widget.graph.attr_size = domain.attributes[2]\n\n        d2 = Domain(domain.attributes[:2], domain.class_var,\n                    [domain.attributes[3]])\n        t2 = self.data.transform(d2)\n        self.send_signal(self.widget.Inputs.data, t2)\n\n    def test_error_message(self):\n        \"\"\"Check if error message appears and then disappears when\n        data is removed from input\"\"\"\n        data = self.data.copy()\n        with data.unlocked():\n            data.X[:, 0] = np.nan\n        self.send_signal(self.widget.Inputs.data, data)\n        self.assertTrue(self.widget.Warning.missing_coords.is_shown())\n        self.send_signal(self.widget.Inputs.data, None)\n        self.assertFalse(self.widget.Warning.missing_coords.is_shown())\n\n    def test_report_on_empty(self):\n        self.widget.report_plot = MagicMock()\n        self.widget.report_caption = MagicMock()\n        self.widget.report_items = MagicMock()\n        self.widget.send_report()  # Essentially, don't crash\n        self.widget.report_plot.assert_not_called()\n        self.widget.report_caption.assert_not_called()\n        self.widget.report_items.assert_not_called()\n\n    def test_data_column_nans(self):\n        \"\"\"\n        ValueError cannot convert float NaN to integer.\n        In case when all column values are NaN then it throws that error.\n        GH-2061\n        \"\"\"\n        table = datasets.data_one_column_nans()\n        self.send_signal(self.widget.Inputs.data, table)\n        cb_attr_color = self.widget.controls.attr_color\n        simulate.combobox_activate_item(cb_attr_color, \"b\")\n        simulate.combobox_activate_item(self.widget.cb_attr_x, \"a\")\n        simulate.combobox_activate_item(self.widget.cb_attr_y, \"a\")\n\n        #self.widget.update_graph()\n        self.widget.graph.reset_graph()\n\n    def test_data_column_infs(self):\n        \"\"\"\n        Scatter Plot should not crash on data with infinity values\n        GH-2707\n        GH-2684\n        \"\"\"\n        table = datasets.data_one_column_infs()\n        self.send_signal(self.widget.Inputs.data, table)\n        attr_x = self.widget.controls.attr_x\n        simulate.combobox_activate_item(attr_x, \"b\")\n\n    def test_regression_line(self):\n        \"\"\"It is possible to draw the line only for pair of continuous attrs\"\"\"\n        self.send_signal(self.widget.Inputs.data, self.data)\n        self.assertTrue(self.widget.cb_reg_line.isEnabled())\n        self.assertIsNone(self.widget.graph.reg_line_item)\n        self.widget.cb_reg_line.setChecked(True)\n        self.assertIsNotNone(self.widget.graph.reg_line_item)\n        self.widget.cb_attr_y.activated.emit(4)\n        self.widget.cb_attr_y.setCurrentIndex(4)\n        self.assertFalse(self.widget.cb_reg_line.isEnabled())\n        self.assertIsNone(self.widget.graph.reg_line_item)\n\n    def test_points_combo_boxes(self):\n        \"\"\"Check Point box combo models and values\"\"\"\n        self.send_signal(self.widget.Inputs.data, self.data)\n        controls = self.widget.controls\n\n        # color and label should contain all variables\n        # size should contain only continuous variables\n        # shape should contain only discrete variables\n        for var in self.data.domain.variables + self.data.domain.metas:\n            self.assertIn(var, controls.attr_color.model())\n            self.assertIn(var, controls.attr_label.model())\n            if var.is_continuous:\n                self.assertIn(var, controls.attr_size.model())\n                self.assertNotIn(var, controls.attr_shape.model())\n            if var.is_discrete:\n                self.assertNotIn(var, controls.attr_size.model())\n                self.assertIn(var, controls.attr_shape.model())\n\n        widget = self.create_widget(OWScatterPlot)\n        self.send_signal(self.widget.Inputs.data, self.data, widget=widget)\n        self.assertEqual(controls.attr_color.currentText(),\n                         self.data.domain.class_var.name)\n\n    def test_group_selections(self):\n        self.send_signal(self.widget.Inputs.data, self.data)\n        graph = self.widget.graph\n        points = graph.scatterplot_item.points()\n        sel_column = np.zeros((len(self.data), 1))\n\n        x = self.data.X\n\n        def selectedx():\n            return self.get_output(self.widget.Outputs.selected_data).X\n\n        def selected_groups():\n            return self.get_output(self.widget.Outputs.selected_data).metas[:, 0]\n\n        def annotated():\n            return self.get_output(self.widget.Outputs.annotated_data).metas\n\n        def annotations():\n            return self.get_output(self.widget.Outputs.annotated_data).domain.metas[0].values\n\n        # Select 0:5\n        graph.select(points[:5])\n        np.testing.assert_equal(selectedx(), x[:5])\n        np.testing.assert_equal(selected_groups(), np.zeros(5))\n        sel_column[:5] = 1\n        np.testing.assert_equal(annotated(), sel_column)\n        self.assertEqual(annotations(), (\"No\", \"Yes\", ))\n\n        # Shift-select 5:10; now we have groups 0:5 and 5:10\n        with self.modifiers(Qt.ShiftModifier):\n            graph.select(points[5:10])\n        np.testing.assert_equal(selectedx(), x[:10])\n        np.testing.assert_equal(selected_groups(), np.array([0] * 5 + [1] * 5))\n        sel_column[:5] = 0\n        sel_column[5:10] = 1\n        sel_column[10:] = 2\n        np.testing.assert_equal(annotated(), sel_column)\n        self.assertEqual(len(annotations()), 3)\n\n        # Select: 15:20; we have 15:20\n        graph.select(points[15:20])\n        sel_column = np.zeros((len(self.data), 1))\n        sel_column[15:20] = 1\n        np.testing.assert_equal(selectedx(), x[15:20])\n        np.testing.assert_equal(selected_groups(), np.zeros(5))\n        self.assertEqual(annotations(), (\"No\", \"Yes\"))\n\n        # Alt-select (remove) 10:17; we have 17:20\n        with self.modifiers(Qt.AltModifier):\n            graph.select(points[10:17])\n        np.testing.assert_equal(selectedx(), x[17:20])\n        np.testing.assert_equal(selected_groups(), np.zeros(3))\n        sel_column[15:17] = 0\n        np.testing.assert_equal(annotated(), sel_column)\n        self.assertEqual(annotations(), (\"No\", \"Yes\"))\n\n        # Ctrl-Shift-select (add-to-last) 10:17; we have 17:25\n        with self.modifiers(Qt.ShiftModifier | Qt.ControlModifier):\n            graph.select(points[20:25])\n        np.testing.assert_equal(selectedx(), x[17:25])\n        np.testing.assert_equal(selected_groups(), np.zeros(8))\n        sel_column[20:25] = 1\n        np.testing.assert_equal(annotated(), sel_column)\n        self.assertEqual(annotations(), (\"No\", \"Yes\"))\n\n        # Shift-select (add) 30:35; we have 17:25, 30:35\n        with self.modifiers(Qt.ShiftModifier):\n            graph.select(points[30:35])\n        # ... then Ctrl-Shift-select (add-to-last) 10:17; we have 17:25, 30:40\n        with self.modifiers(Qt.ShiftModifier | Qt.ControlModifier):\n            graph.select(points[35:40])\n        sel_column[:] = 2\n        sel_column[17:25] = 0\n        sel_column[30:40] = 1\n        np.testing.assert_equal(selected_groups(), np.array([0] * 8 + [1] * 10))\n        np.testing.assert_equal(annotated(), sel_column)\n        self.assertEqual(len(annotations()), 3)\n\n    def test_saving_selection(self):\n        self.send_signal(self.widget.Inputs.data, self.data)  # iris\n        self.widget.graph.select_by_rectangle(QRectF(4, 3, 3, 1))\n        selected_inds = np.flatnonzero(self.widget.graph.selection)\n        settings = self.widget.settingsHandler.pack_data(self.widget)\n        np.testing.assert_equal(selected_inds,\n                                [i for i, g in settings[\"selection\"]])\n\n    def test_points_selection(self):\n        # Opening widget with saved selection should restore it\n        self.widget = self.create_widget(\n            OWScatterPlot, stored_settings={\n                \"selection_group\": [(i, 1) for i in range(50)]}\n        )\n        self.send_signal(self.widget.Inputs.data, self.data)  # iris\n        selected_data = self.get_output(self.widget.Outputs.selected_data)\n        self.assertEqual(len(selected_data), 50)\n\n        # Changing the dataset should clear selection\n        titanic = Table(\"titanic\")\n        self.send_signal(self.widget.Inputs.data, titanic)\n        selected_data = self.get_output(self.widget.Outputs.selected_data)\n        self.assertIsNone(selected_data)\n\n    def test_migrate_selection(self):\n        settings = dict(selection=list(range(2)))\n        OWScatterPlot.migrate_settings(settings, 0)\n        self.assertEqual(settings[\"selection_group\"], [(0, 1), (1, 1)])\n\n    def test_invalid_points_selection(self):\n        # if selection contains rows that are not present in the current\n        # dataset, widget should select what can be selected.\n        self.widget = self.create_widget(\n            OWScatterPlot, stored_settings={\n                \"selection_group\": [(i, 1) for i in range(50)]}\n        )\n        data = self.data[:11].copy()\n        with data.unlocked():\n            data[0, 0] = np.nan\n        self.send_signal(self.widget.Inputs.data, data)\n        self.assertIsNone(self.get_output(self.widget.Outputs.selected_data))\n\n    def test_set_strings_settings(self):\n        \"\"\"\n        Test if settings can be loaded as strings and successfully put\n        in new owplotgui combos.\n        \"\"\"\n        self.send_signal(self.widget.Inputs.data, self.data)\n        settings = self.widget.settingsHandler.pack_data(self.widget)\n        plot_settings = settings[\"context_settings\"][0].values\n        plot_settings[\"attr_label\"] = (\"sepal length\", -2)\n        plot_settings[\"attr_color\"] = (\"sepal width\", -2)\n        plot_settings[\"attr_shape\"] = (\"iris\", -2)\n        plot_settings[\"attr_size\"] = (\"petal width\", -2)\n        w = self.create_widget(OWScatterPlot, stored_settings=settings)\n        self.send_signal(self.widget.Inputs.data, self.data, widget=w)\n        self.assertEqual(w.attr_label.name, \"sepal length\")\n        self.assertEqual(w.attr_color.name, \"sepal width\")\n        self.assertEqual(w.attr_shape.name, \"iris\")\n        self.assertEqual(w.attr_size.name, \"petal width\")\n\n    def test_features_and_no_data(self):\n        \"\"\"\n        Prevent crashing when features are sent but no data.\n        \"\"\"\n        domain = Table(\"iris\").domain\n        self.send_signal(self.widget.Inputs.features,\n                         AttributeList(domain.variables))\n        self.send_signal(self.widget.Inputs.features, None)\n\n    def test_features_and_data(self):\n        self.assertTrue(self.widget.attr_box.isEnabled())\n        self.send_signal(self.widget.Inputs.data, self.data)\n        x, y = self.widget.graph.scatterplot_item.getData()\n        np.testing.assert_array_equal(x, self.data.X[:, 0])\n        np.testing.assert_array_equal(y, self.data.X[:, 1])\n        self.send_signal(self.widget.Inputs.features,\n                         AttributeList(self.data.domain[2:]))\n        self.assertIs(self.widget.attr_x, self.data.domain[2])\n        self.assertIs(self.widget.attr_y, self.data.domain[3])\n        self.assertFalse(self.widget.attr_box.isEnabled())\n        self.assertFalse(self.widget.vizrank.isEnabled())\n        x, y = self.widget.graph.scatterplot_item.getData()\n        np.testing.assert_array_equal(x, self.data.X[:, 2])\n        np.testing.assert_array_equal(y, self.data.X[:, 3])\n\n        self.send_signal(self.widget.Inputs.data, None)\n        self.send_signal(self.widget.Inputs.data, self.data)\n        self.assertIs(self.widget.attr_x, self.data.domain[2])\n        self.assertIs(self.widget.attr_y, self.data.domain[3])\n        self.assertFalse(self.widget.attr_box.isEnabled())\n        self.assertFalse(self.widget.vizrank.isEnabled())\n\n        self.send_signal(self.widget.Inputs.features, None)\n        self.assertTrue(self.widget.attr_box.isEnabled())\n        self.assertTrue(self.widget.vizrank.isEnabled())\n\n    def test_output_features(self):\n        data = Table(\"iris\")\n        self.send_signal(self.widget.Inputs.data, data)\n\n        # This doesn't work because combo's callbacks are connected to signal\n        # `activated`, which is only triggered by user interaction, and not to\n        # `currentIndexChanged`\n        # combo_y = self.widget.controls.attr_y\n        # combo_y.setCurrentIndex(combo_y.model().indexOf(data.domain[3]))\n        # This is a workaround\n        self.widget.attr_y = data.domain[3]\n        self.widget.attr_changed()\n\n        features = self.get_output(self.widget.Outputs.features)\n        self.assertEqual(features, [data.domain[0], data.domain[3]])\n\n    def test_vizrank(self):\n        data = Table(\"iris\")\n        self.send_signal(self.widget.Inputs.data, data)\n        vizrank = ScatterPlotVizRank(self.widget)\n        n_states = len(data.domain.attributes)\n        n_states = n_states * (n_states - 1) / 2\n        states = [state for state in vizrank.iterate_states(None)]\n        self.assertEqual(len(states), n_states)\n        self.assertEqual(len(set(states)), n_states)\n        self.assertIsNotNone(vizrank.compute_score(states[0]))\n        self.send_signal(self.widget.Inputs.data, data[:9])\n        self.assertIsNone(vizrank.compute_score(states[0]))\n\n        data = Table(\"housing\")[::10]\n        self.send_signal(self.widget.Inputs.data, data)\n        vizrank = ScatterPlotVizRank(self.widget)\n        states = [state for state in vizrank.iterate_states(None)]\n        self.assertIsNotNone(vizrank.compute_score(states[0]))\n\n    def test_vizrank_class_nan(self):\n        \"\"\"\n        When class values are nan, vizrank should be disabled. It should behave like\n        the class column is missing.\n        GH-2757\n        \"\"\"\n        def assert_vizrank_enabled(data, is_enabled):\n            self.send_signal(self.widget.Inputs.data, data)\n            self.assertEqual(is_enabled, self.widget.vizrank_button.isEnabled())\n\n        data1 = Table(\"iris\")[::30]\n        data2 = Table(\"iris\")[::30].copy()\n        with data2.unlocked():\n            data2.Y[:] = np.nan\n        domain = Domain(\n            attributes=data2.domain.attributes[:4], class_vars=DiscreteVariable(\"iris\", values=()))\n        data2 = Table(domain, data2.X, Y=data2.Y)\n        data3 = Table(\"iris\")[::30].copy()\n        with data3.unlocked():\n            data3.Y[:] = np.nan\n\n        for data, is_enabled in zip([data1, data2, data1, data3, data1],\n                                    [True, False, True, False, True]):\n            assert_vizrank_enabled(data, is_enabled)\n\n    def test_vizrank_nonprimitives(self):\n        \"\"\"VizRank does not try to include non primitive attributes\"\"\"\n        data = Table(\"zoo\")\n        self.send_signal(self.widget.Inputs.data, data)\n        with patch(\"Orange.widgets.visualize.owscatterplot.ReliefF\",\n                   new=lambda *_1, **_2: lambda data: np.arange(len(data))):\n            self.widget.vizrank.score_heuristic()\n\n    def test_vizrank_enabled(self):\n        self.send_signal(self.widget.Inputs.data, self.data)\n        self.assertTrue(self.widget.vizrank_button.isEnabled())\n        self.assertEqual(self.widget.vizrank_button.toolTip(), \"\")\n        self.assertTrue(self.widget.vizrank.button.isEnabled())\n        self.widget.vizrank.button.click()\n\n    def test_vizrank_enabled_no_data(self):\n        self.send_signal(self.widget.Inputs.data, None)\n        self.assertFalse(self.widget.vizrank_button.isEnabled())\n        self.assertEqual(self.widget.vizrank_button.toolTip(), \"No data on input\")\n\n    def test_vizrank_enabled_sparse_data(self):\n        self.send_signal(self.widget.Inputs.data, self.data.to_sparse())\n        self.assertFalse(self.widget.vizrank_button.isEnabled())\n        self.assertEqual(self.widget.vizrank_button.toolTip(), \"Data is sparse\")\n\n    def test_vizrank_enabled_constant_data(self):\n        domain = Domain([ContinuousVariable(\"c1\"),\n                         ContinuousVariable(\"c2\"),\n                         ContinuousVariable(\"c3\"),\n                         ContinuousVariable(\"c4\")],\n                        DiscreteVariable(\"cls\", values=(\"a\", \"b\")))\n        X = np.zeros((10, 4))\n        table = Table(domain, X, np.random.randint(2, size=10))\n        self.send_signal(self.widget.Inputs.data, table)\n        self.assertEqual(self.widget.vizrank_button.toolTip(), \"\")\n        self.assertTrue(self.widget.vizrank_button.isEnabled())\n        self.assertTrue(self.widget.vizrank.button.isEnabled())\n        self.widget.vizrank.button.click()\n\n    def test_vizrank_enabled_two_features(self):\n        self.send_signal(self.widget.Inputs.data, self.data[:, :2])\n        self.assertFalse(self.widget.vizrank_button.isEnabled())\n        self.assertEqual(self.widget.vizrank_button.toolTip(),\n                         \"Not enough features for ranking\")\n\n    def test_vizrank_enabled_no_color_var(self):\n        self.send_signal(self.widget.Inputs.data, self.data[:, :3])\n        self.assertFalse(self.widget.vizrank_button.isEnabled())\n        self.assertEqual(self.widget.vizrank_button.toolTip(),\n                         \"Color variable is not selected\")\n\n    def test_vizrank_enabled_color_var_nans(self):\n        domain = Domain([ContinuousVariable(\"c1\"),\n                         ContinuousVariable(\"c2\"),\n                         ContinuousVariable(\"c3\"),\n                         ContinuousVariable(\"c4\")],\n                        DiscreteVariable(\"cls\", values=(\"a\", \"b\")))\n        table = Table(domain, np.random.random((10, 4)), np.full(10, np.nan))\n        self.send_signal(self.widget.Inputs.data, table)\n        self.assertFalse(self.widget.vizrank_button.isEnabled())\n        self.assertEqual(self.widget.vizrank_button.toolTip(),\n                         \"Color variable has no values\")\n\n    def test_auto_send_selection(self):\n        \"\"\"\n        Scatter Plot automatically sends selection only when the checkbox Send automatically\n        is checked.\n        GH-2649\n        GH-2646\n        \"\"\"\n        data = Table(\"iris\")\n        self.send_signal(self.widget.Inputs.data, data)\n        self.widget.controls.auto_commit.setChecked(False)\n        self.assertFalse(self.widget.controls.auto_commit.isChecked())\n        self._select_data()\n        self.assertIsNone(self.get_output(self.widget.Outputs.selected_data))\n        self.widget.controls.auto_commit.setChecked(True)\n        output = self.get_output(self.widget.Outputs.selected_data)\n        self.assertIsInstance(output, Table)\n\n    def test_color_is_optional(self):\n        zoo = Table(\"zoo\")\n        backbone, breathes, airborne, type = \\\n            [zoo.domain[x] for x in [\"backbone\", \"breathes\", \"airborne\", \"type\"]]\n        default_x, default_y, default_color = \\\n            zoo.domain[0], zoo.domain[1], zoo.domain.class_var\n        attr_x = self.widget.controls.attr_x\n        attr_y = self.widget.controls.attr_y\n        attr_color = self.widget.controls.attr_color\n\n        # Send dataset, ensure defaults are what we expect them to be\n        self.send_signal(self.widget.Inputs.data, zoo)\n        self.assertEqual(attr_x.currentText(), default_x.name)\n        self.assertEqual(attr_y.currentText(), default_y.name)\n        self.assertEqual(attr_color.currentText(), default_color.name)\n        # Select different values\n        simulate.combobox_activate_item(attr_x, backbone.name)\n        simulate.combobox_activate_item(attr_y, breathes.name)\n        simulate.combobox_activate_item(attr_color, airborne.name)\n\n        # Send compatible dataset, values should not change\n        zoo2 = zoo[:, (backbone, breathes, airborne, type)]\n        self.send_signal(self.widget.Inputs.data, zoo2)\n        self.assertEqual(attr_x.currentText(), backbone.name)\n        self.assertEqual(attr_y.currentText(), breathes.name)\n        self.assertEqual(attr_color.currentText(), airborne.name)\n\n        # Send dataset without color variable\n        # x and y should remain, color reset to default\n        zoo3 = zoo[:, (backbone, breathes, type)]\n        self.send_signal(self.widget.Inputs.data, zoo3)\n        self.assertEqual(attr_x.currentText(), backbone.name)\n        self.assertEqual(attr_y.currentText(), breathes.name)\n        self.assertEqual(attr_color.currentText(), default_color.name)\n\n        # Send dataset without x\n        # y and color should be the same as with zoo\n        zoo4 = zoo[:, (default_x, default_y, breathes, airborne, type)]\n        self.send_signal(self.widget.Inputs.data, zoo4)\n        self.assertEqual(attr_x.currentText(), default_x.name)\n        self.assertEqual(attr_y.currentText(), default_y.name)\n        self.assertEqual(attr_color.currentText(), default_color.name)\n\n        # Send dataset compatible with zoo2 and zoo3\n        # Color should reset to one in zoo3, as it was used more\n        # recently\n        zoo5 = zoo[:, (default_x, backbone, breathes, airborne, type)]\n        self.send_signal(self.widget.Inputs.data, zoo5)\n        self.assertEqual(attr_x.currentText(), backbone.name)\n        self.assertEqual(attr_y.currentText(), breathes.name)\n        self.assertEqual(attr_color.currentText(), type.name)\n\n    def test_handle_metas(self):\n        \"\"\"\n        Scatter Plot Graph can handle metas\n        GH-2699\n        \"\"\"\n        w = self.widget\n        data = Table(\"iris\")\n        domain = Domain(\n            attributes=data.domain.attributes[:2],\n            class_vars=data.domain.class_vars,\n            metas=data.domain.attributes[2:]\n        )\n        data = data.transform(domain).copy()\n        # Sometimes floats in metas are saved as objects\n        with data.unlocked():\n            data.metas = data.metas.astype(object)\n        self.send_signal(w.Inputs.data, data)\n        simulate.combobox_activate_item(w.cb_attr_x, data.domain.metas[1].name)\n        simulate.combobox_activate_item(w.controls.attr_color, data.domain.metas[0].name)\n        w.graph.reset_graph()\n\n    def test_subset_data(self):\n        \"\"\"\n        Scatter Plot subset data is sent to Scatter Plot Graph\n        GH-2773\n        \"\"\"\n        data = Table(\"iris\")\n        w = self.widget\n        self.send_signal(w.Inputs.data, data)\n        self.send_signal(w.Inputs.data_subset, data[::30])\n        self.assertEqual(len(w.subset_indices), 5)\n\n    def test_opacity_warning(self):\n        data = Table(\"iris\")\n        w = self.widget\n        self.send_signal(w.Inputs.data, data)\n        w.graph.controls.alpha_value.setSliderPosition(10)\n        self.assertFalse(w.Warning.transparent_subset.is_shown())\n        self.send_signal(w.Inputs.data_subset, data[::30])\n        self.assertTrue(w.Warning.transparent_subset.is_shown())\n        w.graph.controls.alpha_value.setSliderPosition(200)\n        self.assertFalse(w.Warning.transparent_subset.is_shown())\n        w.graph.controls.alpha_value.setSliderPosition(10)\n        self.assertTrue(w.Warning.transparent_subset.is_shown())\n        self.send_signal(w.Inputs.data_subset, None)\n        self.assertFalse(w.Warning.transparent_subset.is_shown())\n\n    def test_jittering(self):\n        self.send_signal(self.widget.Inputs.data, self.data)\n        self.widget.graph.controls.jitter_continuous.setChecked(True)\n        self.widget.graph.controls.jitter_size.setValue(1)\n\n    def test_metas_zero_column(self):\n        \"\"\"\n        Prevent crash when metas column is zero.\n        GH-2775\n        \"\"\"\n        data = Table(\"iris\")\n        domain = data.domain\n        domain = Domain(domain.attributes[:3], domain.class_vars, domain.attributes[3:])\n        data = data.transform(domain).copy()\n        with data.unlocked():\n            data.metas[:, 0] = 0\n        w = self.widget\n        self.send_signal(w.Inputs.data, data)\n        simulate.combobox_activate_item(w.controls.attr_x, domain.metas[0].name)\n\n    def test_tooltip(self):\n        # The test tests presence of some data,\n        # but avoids checking the exact format\n        data = Table(\"heart_disease\")\n        self.send_signal(self.widget.Inputs.data, data)\n        widget = self.widget\n        graph = widget.graph\n        scatterplot_item = graph.scatterplot_item\n\n        widget.controls.attr_x = data.domain[\"chest pain\"]\n        widget.controls.attr_y = data.domain[\"cholesterol\"]\n        all_points = scatterplot_item.points()\n\n        event = MagicMock()\n        with patch.object(scatterplot_item, \"mapFromScene\"), \\\n                patch.object(QToolTip, \"showText\") as show_text:\n\n            # Single point hovered\n            with patch.object(scatterplot_item, \"pointsAt\",\n                              return_value=[all_points[42]]):\n                # Show just x and y attribute\n                widget.tooltip_shows_all = False\n                self.assertTrue(graph.help_event(event))\n                (_, text), _ = show_text.call_args\n                self.assertIn(\"age = {}\".format(data[42, \"age\"]), text)\n                self.assertIn(\"gender = {}\".format(data[42, \"gender\"]), text)\n                self.assertNotIn(\"max HR = {}\".format(data[42, \"max HR\"]), text)\n                self.assertNotIn(\"others\", text)\n\n                # Show all attributes\n                widget.tooltip_shows_all = True\n                self.assertTrue(graph.help_event(event))\n                (_, text), _ = show_text.call_args\n                self.assertIn(\"age = {}\".format(data[42, \"age\"]), text)\n                self.assertIn(\"gender = {}\".format(data[42, \"gender\"]), text)\n                self.assertIn(\"max HR = {}\".format(data[42, \"max HR\"]), text)\n                self.assertIn(\"... and 4 others\", text)\n\n            # Two points hovered\n            with patch.object(scatterplot_item, \"pointsAt\",\n                              return_value=[all_points[42], all_points[100]]):\n                self.assertTrue(graph.help_event(event))\n                (_, text), _ = show_text.call_args\n                self.assertIn(\"age = {}\".format(data[42, \"age\"]), text)\n                self.assertIn(\"gender = {}\".format(data[42, \"gender\"]), text)\n                self.assertIn(\"age = {}\".format(data[100, \"age\"]), text)\n                self.assertIn(\"gender = {}\".format(data[100, \"gender\"]), text)\n\n            # No points hovered\n            with patch.object(scatterplot_item, \"pointsAt\",\n                              return_value=[]):\n                show_text.reset_mock()\n                self.assertFalse(graph.help_event(event))\n                self.assertEqual(show_text.call_count, 0)\n\n    def test_many_discrete_values(self):\n        \"\"\"\n        Do not show all discrete values if there are too many.\n        Also test for values with a nan.\n        GH-2804\n        \"\"\"\n        def prepare_data():\n            data = Table(\"iris\")\n            values = list(range(15))\n            class_var = DiscreteVariable(\"iris5\", values=[str(v) for v in values])\n            data = data.transform(\n                Domain(attributes=data.domain.attributes,\n                       class_vars=[class_var])).copy()\n            with data.unlocked():\n                data.Y = np.array(values * 10, dtype=float)\n            return data\n\n        def assert_equal(data, max):\n            self.send_signal(self.widget.Inputs.data, data)\n            pen_data, brush_data = self.widget.graph.get_colors()\n            self.assertEqual(max, len(np.unique([id(p) for p in pen_data])), )\n\n        assert_equal(prepare_data(), MAX_COLORS)\n        # data with nan value\n        data = prepare_data()\n        with data.unlocked():\n            data.Y[42] = np.nan\n        assert_equal(data, MAX_COLORS + 1)\n\n    def test_invalidated_same_features(self):\n        self.widget.setup_plot = Mock()\n        # send data and set default features\n        self.send_signal(self.widget.Inputs.data, self.data)\n        self.widget.setup_plot.assert_called_once()\n        self.assertListEqual(self.widget.effective_variables,\n                             list(self.data.domain.attributes[:2]))\n\n        # send the same features as already set\n        self.widget.setup_plot.reset_mock()\n        self.send_signal(self.widget.Inputs.features,\n                         AttributeList(self.data.domain.attributes[:2]))\n        self.widget.setup_plot.assert_not_called()\n        self.assertListEqual(self.widget.effective_variables,\n                             list(self.data.domain.attributes[:2]))\n\n    def test_invalidated_same_time(self):\n        self.widget.setup_plot = Mock()\n        # send data and features at the same time (data first)\n        features = self.data.domain.attributes[:2]\n        signals = [(self.widget.Inputs.data, self.data),\n                   (self.widget.Inputs.features, AttributeList(features))]\n        self.send_signals(signals)\n        self.widget.setup_plot.assert_called_once()\n        self.assertListEqual(self.widget.effective_variables, list(features))\n\n    def test_invalidated_features_first(self):\n        self.widget.setup_plot = Mock()\n        # send features (same as default ones)\n        self.send_signal(self.widget.Inputs.features,\n                         AttributeList(self.data.domain.attributes[:2]))\n        self.assertListEqual(self.widget.effective_variables, [])\n        self.widget.setup_plot.assert_called_once()\n\n        # send data\n        self.widget.setup_plot.reset_mock()\n        self.send_signal(self.widget.Inputs.data, self.data)\n        self.widget.setup_plot.assert_called()\n        self.assertListEqual(self.widget.effective_variables,\n                             list(self.data.domain.attributes[:2]))\n\n    def test_invalidated_same_time_features_first(self):\n        self.widget.setup_plot = Mock()\n        # send features and data at the same time (features first)\n        features = self.data.domain.attributes[:2]\n        signals = [(self.widget.Inputs.features, AttributeList(features)),\n                   (self.widget.Inputs.data, self.data)]\n        self.send_signals(signals)\n        self.widget.setup_plot.assert_called_once()\n        self.assertListEqual(self.widget.effective_variables, list(features))\n\n        self.widget.setup_plot.reset_mock()\n        features = self.data.domain.attributes[2:]\n        signals = [(self.widget.Inputs.features, AttributeList(features)),\n                   (self.widget.Inputs.data, self.data)]\n        self.send_signals(signals)\n        self.widget.setup_plot.assert_called_once()\n\n    def test_invalidated_diff_features(self):\n        self.widget.setup_plot = Mock()\n        # send data and set default features\n        self.send_signal(self.widget.Inputs.data, self.data)\n        self.widget.setup_plot.assert_called_once()\n        self.assertListEqual(self.widget.effective_variables,\n                             list(self.data.domain.attributes[:2]))\n\n        # send different features\n        self.widget.setup_plot.reset_mock()\n        self.send_signal(self.widget.Inputs.features,\n                         AttributeList(self.data.domain.attributes[2:4]))\n        self.widget.setup_plot.assert_called_once()\n        self.assertListEqual(self.widget.effective_variables,\n                             list(self.data.domain.attributes[2:4]))\n\n    def test_invalidated_diff_features_same_time(self):\n        self.widget.setup_plot = Mock()\n        # send data and different features at the same time (data first)\n        features = self.data.domain.attributes[2:4]\n        signals = [(self.widget.Inputs.data, self.data),\n                   (self.widget.Inputs.features, AttributeList(features))]\n        self.send_signals(signals)\n        self.widget.setup_plot.assert_called_once()\n        self.assertListEqual(self.widget.effective_variables, list(features))\n\n    def test_invalidated_diff_features_features_first(self):\n        self.widget.setup_plot = Mock()\n        # send features (not the same as defaults)\n        self.send_signal(self.widget.Inputs.features,\n                         AttributeList(self.data.domain.attributes[2:4]))\n        self.assertListEqual(self.widget.effective_variables, [])\n        self.widget.setup_plot.assert_called_once()\n\n        # send data\n        self.widget.setup_plot.reset_mock()\n        self.send_signal(self.widget.Inputs.data, self.data)\n        self.widget.setup_plot.assert_called_once()\n        self.assertListEqual(self.widget.effective_variables,\n                             list(self.data.domain.attributes[2:4]))\n\n    def test_invalidated_diff_features_same_time_features_first(self):\n        self.widget.setup_plot = Mock()\n        # send data and different features at the same time (features first)\n        features = self.data.domain.attributes[2:4]\n        signals = [(self.widget.Inputs.features, AttributeList(features)),\n                   (self.widget.Inputs.data, self.data)]\n        self.send_signals(signals)\n        self.widget.setup_plot.assert_called_once()\n        self.assertListEqual(self.widget.effective_variables, list(features))\n\n    @patch('Orange.widgets.visualize.owscatterplot.ScatterPlotVizRank.'\n           'on_manual_change')\n    def test_vizrank_receives_manual_change(self, on_manual_change):\n        # Recreate the widget so the patch kicks in\n        self.widget = self.create_widget(OWScatterPlot)\n        data = Table(\"iris.tab\")\n        self.send_signal(self.widget.Inputs.data, data)\n        model = self.widget.controls.attr_x.model()\n        self.widget.attr_x = model[0]\n        self.widget.attr_y = model[1]\n        simulate.combobox_activate_index(self.widget.controls.attr_x, 2)\n        self.assertIs(self.widget.attr_x, model[2])\n        on_manual_change.assert_called_with(model[2], model[1])\n\n    def test_on_manual_change(self):\n        data = Table(\"iris.tab\")\n        self.send_signal(self.widget.Inputs.data, data)\n        vizrank = self.widget.vizrank\n        vizrank.toggle()\n        self.process_events(until=lambda: not vizrank.keep_running)\n\n        model = vizrank.rank_model\n        attrs = model.data(model.index(3, 0), vizrank._AttrRole)\n        vizrank.on_manual_change(*attrs)\n        selection = vizrank.rank_table.selectedIndexes()\n        self.assertEqual(len(selection), 1)\n        self.assertEqual(selection[0].row(), 3)\n\n        vizrank.on_manual_change(*attrs[::-1])\n        selection = vizrank.rank_table.selectedIndexes()\n        self.assertEqual(len(selection), 0)\n\n    def test_regression_lines_appear(self):\n        self.widget.graph.controls.show_reg_line.setChecked(True)\n        self.assertEqual(len(self.widget.graph.reg_line_items), 0)\n        self.send_signal(self.widget.Inputs.data, self.data)\n        self.assertEqual(len(self.widget.graph.reg_line_items), 4)\n        simulate.combobox_activate_index(self.widget.controls.attr_color, 0)\n        self.assertEqual(len(self.widget.graph.reg_line_items), 1)\n        data = self.data.copy()\n        with data.unlocked():\n            data[:, 0] = np.nan\n        self.send_signal(self.widget.Inputs.data, data)\n        self.assertEqual(len(self.widget.graph.reg_line_items), 0)\n\n    def test_regression_line_coeffs(self):\n        widget = self.widget\n        graph = widget.graph\n        xy = np.array([[0, 0], [1, 0], [1, 2], [2, 2],\n                       [0, 1], [1, 3], [2, 5]], dtype=float)\n        colors = np.array([0, 0, 0, 0, 1, 1, 1], dtype=float)\n        widget.get_coordinates_data = lambda: xy.T\n        widget.can_draw_regresssion_line = lambda: True\n        widget.get_color_data = lambda: colors\n        widget.is_continuous_color = lambda: False\n        graph.palette = DefaultRGBColors\n        graph.controls.show_reg_line.setChecked(True)\n\n        graph.update_regression_line()\n\n        line1 = graph.reg_line_items[1]\n        self.assertEqual(line1.pos().x(), 0)\n        self.assertEqual(line1.pos().y(), 0)\n        self.assertEqual(line1.angle, 45)\n        self.assertEqual(line1.pen.color().hue(), graph.palette[0].hue())\n\n        line2 = graph.reg_line_items[2]\n        self.assertEqual(line2.pos().x(), 0)\n        self.assertEqual(line2.pos().y(), 1)\n        self.assertAlmostEqual(line2.angle, np.degrees(np.arctan2(2, 1)))\n        self.assertEqual(line2.pen.color().hue(), graph.palette[1].hue())\n\n        graph.orthonormal_regression = True\n        graph.update_regression_line()\n\n        line1 = graph.reg_line_items[1]\n        self.assertEqual(line1.pos().x(), 0)\n        self.assertAlmostEqual(line1.pos().y(), -0.6180339887498949)\n        self.assertAlmostEqual(line1.angle, 58.28252558853899)\n        self.assertEqual(line1.pen.color().hue(), graph.palette[0].hue())\n\n        line2 = graph.reg_line_items[2]\n        self.assertEqual(line2.pos().x(), 0)\n        self.assertEqual(line2.pos().y(), 1)\n        self.assertAlmostEqual(line2.angle, np.degrees(np.arctan2(2, 1)))\n        self.assertEqual(line2.pen.color().hue(), graph.palette[1].hue())\n\n    def test_orthonormal_line(self):\n        color = QColor(1, 2, 3)\n        width = 42\n        # Normal line\n        line = OWScatterPlotGraph._orthonormal_line(\n            np.array([0, 1, 1, 2]), np.array([0, 0, 2, 2]), color, width)\n        self.assertEqual(line.pos().x(), 0)\n        self.assertAlmostEqual(line.pos().y(), -0.6180339887498949)\n        self.assertAlmostEqual(line.angle, 58.28252558853899)\n        self.assertEqual(line.pen.color(), color)\n        self.assertEqual(line.pen.width(), width)\n\n        # Normal line, negative slope\n        line = OWScatterPlotGraph._orthonormal_line(\n            np.array([1, 2, 3]), np.array([3, 2, 1]), color, width)\n        self.assertEqual(line.pos().x(), 1)\n        self.assertEqual(line.pos().y(), 3)\n        self.assertEqual(line.angle % 360, 315)\n\n        # Horizontal line\n        line = OWScatterPlotGraph._orthonormal_line(\n            np.array([10, 11, 12]), np.array([42, 42, 42]), color, width)\n        self.assertEqual(line.pos().x(), 10)\n        self.assertEqual(line.pos().y(), 42)\n        self.assertEqual(line.angle, 0)\n\n        # Vertical line\n        line = OWScatterPlotGraph._orthonormal_line(\n            np.array([42, 42, 42]), np.array([10, 11, 12]), color, width)\n        self.assertEqual(line.pos().x(), 42)\n        self.assertEqual(line.pos().y(), 10)\n        self.assertEqual(line.angle, 90)\n\n        # No line because all points coincide\n        line = OWScatterPlotGraph._orthonormal_line(\n            np.array([1, 1, 1]), np.array([42, 42, 42]), color, width)\n        self.assertIsNone(line)\n\n        # No line because the group is symmetric\n        line = OWScatterPlotGraph._orthonormal_line(\n            np.array([1, 1, 2, 2]), np.array([42, 5, 5, 42]), color, width)\n        self.assertIsNone(line)\n\n    def test_regression_line(self):\n        color = QColor(1, 2, 3)\n        width = 42\n        # Normal line\n        line = OWScatterPlotGraph._regression_line(\n            np.array([0, 1, 1, 2]), np.array([0, 0, 2, 2]), color, width)\n        self.assertEqual(line.pos().x(), 0)\n        self.assertAlmostEqual(line.pos().y(), 0)\n        self.assertEqual(line.angle, 45)\n        self.assertEqual(line.pen.color(), color)\n        self.assertEqual(line.pen.width(), width)\n\n        # Normal line, negative slope\n        line = OWScatterPlotGraph._regression_line(\n            np.array([1, 2, 3]), np.array([3, 2, 1]), color, width)\n        self.assertEqual(line.pos().x(), 1)\n        self.assertEqual(line.pos().y(), 3)\n        self.assertEqual(line.angle % 360, 315)\n\n        # Horizontal line\n        line = OWScatterPlotGraph._regression_line(\n            np.array([10, 11, 12]), np.array([42, 42, 42]), color, width)\n        self.assertEqual(line.pos().x(), 10)\n        self.assertEqual(line.pos().y(), 42)\n        self.assertEqual(line.angle, 0)\n\n        # Vertical line\n        line = OWScatterPlotGraph._regression_line(\n            np.array([42, 42, 42]), np.array([10, 11, 12]), color, width)\n        self.assertIsNone(line)\n\n        # No line because all points coincide\n        line = OWScatterPlotGraph._regression_line(\n            np.array([1, 1, 1]), np.array([42, 42, 42]), color, width)\n        self.assertIsNone(line)\n\n    def test_add_line_calls_proper_regressor(self):\n        graph = self.widget.graph\n        graph._orthonormal_line = Mock(return_value=None)\n        graph._regression_line = Mock(return_value=None)\n        x, y, c = Mock(), Mock(), Mock()\n\n        graph.orthonormal_regression = True\n        graph._add_line(x, y, c)\n        graph._orthonormal_line.assert_called_once_with(x, y, c, 3, 1)\n        graph._regression_line.assert_not_called()\n        graph._orthonormal_line.reset_mock()\n\n        graph.orthonormal_regression = False\n        graph._add_line(x, y, c)\n        graph._regression_line.assert_called_with(x, y, c, 3, 1)\n        graph._orthonormal_line.assert_not_called()\n\n    def test_no_regression_line(self):\n        graph = self.widget.graph\n        graph._orthonormal_line = lambda *_: None\n        graph.orthonormal_regression = True\n\n        graph.plot_widget.addItem = Mock()\n\n        x, y, c = Mock(), Mock(), Mock()\n        graph._add_line(x, y, c)\n        graph.plot_widget.addItem.assert_not_called()\n        self.assertEqual(graph.reg_line_items, [])\n\n    def test_update_regression_line_calls_add_line(self):\n        widget = self.widget\n        graph = widget.graph\n        x, y = np.array([[0, 0], [1, 0], [1, 2], [2, 2],\n                         [0, 1], [1, 3], [2, 5]], dtype=float).T\n        colors = np.array([0, 0, 0, 0, 1, 1, 1], dtype=float)\n        widget.get_coordinates_data = lambda: (x, y)\n        widget.can_draw_regresssion_line = lambda: True\n        widget.get_color_data = lambda: colors\n        widget.is_continuous_color = lambda: False\n        graph.palette = DefaultRGBColors\n        graph.controls.show_reg_line.setChecked(True)\n\n        graph._add_line = Mock()\n\n        graph.update_regression_line()\n        (args1, _), (args2, _), (args3, _) = graph._add_line.call_args_list\n        np.testing.assert_equal(args1[0], x)\n        np.testing.assert_equal(args1[1], y)\n        self.assertEqual(args1[2], QColor(\"#505050\"))\n\n        np.testing.assert_equal(args2[0], x[:4])\n        np.testing.assert_equal(args2[1], y[:4])\n        self.assertEqual(args2[2].hue(), graph.palette[0].hue())\n\n        np.testing.assert_equal(args3[0], x[4:])\n        np.testing.assert_equal(args3[1], y[4:])\n        self.assertEqual(args3[2].hue(), graph.palette[1].hue())\n        graph._add_line.reset_mock()\n\n        # Continuous color - just a single line\n        widget.is_continuous_color = lambda: True\n        graph.update_regression_line()\n        graph._add_line.assert_called_once()\n        args1, _ = graph._add_line.call_args_list[0]\n        np.testing.assert_equal(args1[0], x)\n        np.testing.assert_equal(args1[1], y)\n        self.assertEqual(args1[2].hue(), QColor(\"#505050\").hue())\n        graph._add_line.reset_mock()\n        widget.is_continuous_color = lambda: False\n\n        # No palette - just a single line\n        graph.palette = None\n        graph.update_regression_line()\n        graph._add_line.assert_called_once()\n        graph._add_line.reset_mock()\n        graph.palette = DefaultRGBColors\n\n        # Regression line is disabled\n        graph.show_reg_line = False\n        graph.update_regression_line()\n        graph._add_line.assert_not_called()\n        graph.show_reg_line = True\n\n        # No colors - just one line\n        widget.get_color_data = lambda: None\n        graph.update_regression_line()\n        graph._add_line.assert_called_once()\n        graph._add_line.reset_mock()\n\n        # No data\n        widget.get_coordinates_data = lambda: (None, None)\n        graph.update_regression_line()\n        graph._add_line.assert_not_called()\n        graph.show_reg_line = True\n        widget.get_coordinates_data = lambda: (x, y)\n\n        # One color group contains just one point - skip that line\n        widget.get_color_data = lambda: np.array([0] + [1] * (len(x) - 1))\n\n        graph.update_regression_line()\n        (args1, _), (args2, _) = graph._add_line.call_args_list\n        np.testing.assert_equal(args1[0], x)\n        np.testing.assert_equal(args1[1], y)\n        self.assertEqual(args1[2].hue(), QColor(\"#505050\").hue())\n\n        np.testing.assert_equal(args2[0], x[1:])\n        np.testing.assert_equal(args2[1], y[1:])\n        self.assertEqual(args2[2].hue(), graph.palette[1].hue())\n\n    def test_update_regression_line_is_called(self):\n        widget = self.widget\n        graph = widget.graph\n        urline = graph.update_regression_line = Mock()\n\n        self.send_signal(widget.Inputs.data, self.data)\n        urline.assert_called_once()\n        urline.reset_mock()\n\n        self.send_signal(widget.Inputs.data, None)\n        urline.assert_called_once()\n        urline.reset_mock()\n\n        self.send_signal(widget.Inputs.data, self.data)\n        urline.assert_called_once()\n        urline.reset_mock()\n\n        simulate.combobox_activate_index(self.widget.controls.attr_color, 0)\n        urline.assert_called_once()\n        urline.reset_mock()\n\n        simulate.combobox_activate_index(self.widget.controls.attr_color, 2)\n        urline.assert_called_once()\n        urline.reset_mock()\n\n        simulate.combobox_activate_index(self.widget.controls.attr_x, 3)\n        urline.assert_called_once()\n        urline.reset_mock()\n\n    def test_time_axis(self):\n        a = np.array([[1581953776, 1], [1581963776, 2], [1582953776, 3]])\n        d1 = Domain([ContinuousVariable(\"time\"), ContinuousVariable(\"value\")])\n        data = Table.from_numpy(d1, a)\n        d2 = Domain([TimeVariable(\"time\"), ContinuousVariable(\"value\")])\n        data_time = Table.from_numpy(d2, a)\n\n        x_axis = self.widget.graph.plot_widget.plotItem.getAxis(\"bottom\")\n\n        self.send_signal(self.widget.Inputs.data, data)\n        self.assertFalse(x_axis._use_time)\n        _ticks = x_axis.tickValues(1581953776, 1582953776, 1000)\n        ticks = x_axis.tickStrings(_ticks[0][1], 1, _ticks[0][0])\n        try:\n            float(ticks[0])\n        except ValueError:\n            self.fail(\"axis should display floats\")\n\n        self.send_signal(self.widget.Inputs.data, data_time)\n        self.assertTrue(x_axis._use_time)\n        _ticks = x_axis.tickValues(1581953776, 1582953776, 1000)\n        ticks = x_axis.tickStrings(_ticks[0][1], 1, _ticks[0][0])\n        with self.assertRaises(ValueError):\n            float(ticks[0])\n\n    def test_visual_settings(self):\n        super().test_visual_settings()\n\n        graph = self.widget.graph\n        font = QFont()\n        font.setItalic(True)\n        font.setFamily(\"Helvetica\")\n\n        key, value = ('Fonts', 'Axis title', 'Font size'), 16\n        self.widget.set_visual_settings(key, value)\n        key, value = ('Fonts', 'Axis title', 'Italic'), True\n        self.widget.set_visual_settings(key, value)\n        font.setPointSize(16)\n        for item in graph.parameter_setter.axis_items:\n            self.assertFontEqual(item.label.font(), font)\n\n        key, value = ('Fonts', 'Axis ticks', 'Font size'), 15\n        self.widget.set_visual_settings(key, value)\n        key, value = ('Fonts', 'Axis ticks', 'Italic'), True\n        self.widget.set_visual_settings(key, value)\n        font.setPointSize(15)\n        for item in graph.parameter_setter.axis_items:\n            self.assertFontEqual(item.style[\"tickFont\"], font)\n\n        self.widget.graph.controls.show_reg_line.setChecked(True)\n        self.assertGreater(len(graph.parameter_setter.reg_line_label_items), 0)\n\n        key, value = ('Fonts', 'Line label', 'Font size'), 16\n        self.widget.set_visual_settings(key, value)\n        key, value = ('Fonts', 'Line label', 'Italic'), True\n        self.widget.set_visual_settings(key, value)\n        font.setPointSize(16)\n        for label in graph.parameter_setter.reg_line_label_items:\n            self.assertFontEqual(label.textItem.font(), font)\n\n        key, value = ('Figure', 'Lines', 'Width'), 10\n        self.widget.set_visual_settings(key, value)\n        for item in graph.reg_line_items:\n            self.assertEqual(item.pen.width(), 10)",
                            "ast_type_str": "ClassDef",
                            "start_lineno": 736,
                            "end_lineno": 751,
                            "parent": {
                                "file_name": "Orange/widgets/visualize/tests/test_owscatterplot.py",
                                "type": "class",
                                "name": "TestOWScatterPlot",
                                "content": "class TestOWScatterPlot(WidgetTest, ProjectionWidgetTestMixin,\n                        WidgetOutputsTestMixin):\n    @classmethod\n    def setUpClass(cls):\n        super().setUpClass()\n        WidgetOutputsTestMixin.init(cls)\n        cls.same_input_output_domain = False\n\n        cls.signal_name = \"Data\"\n        cls.signal_data = cls.data\n\n    def setUp(self):\n        self.widget = self.create_widget(OWScatterPlot)\n\n    def test_set_data(self):\n        # Connect iris to scatter plot\n        self.send_signal(self.widget.Inputs.data, self.data)\n\n        # First two attribute should be selected as x an y\n        self.assertEqual(self.widget.attr_x, self.data.domain[0])\n        self.assertEqual(self.widget.attr_y, self.data.domain[1])\n\n        # Class var should be selected as color\n        self.assertIs(self.widget.attr_color, self.data.domain.class_var)\n\n        # Change which attributes are displayed\n        self.widget.attr_x = self.data.domain[2]\n        self.widget.attr_y = self.data.domain[3]\n\n        # Disconnect the data\n        self.send_signal(self.widget.Inputs.data, None)\n\n        # removing data should have cleared attributes\n        self.assertIsNone(self.widget.attr_x)\n        self.assertIsNone(self.widget.attr_y)\n        self.assertIsNone(self.widget.attr_color)\n\n        # and remove the legend\n        self.assertEqual(len(self.widget.graph.color_legend.items), 0)\n\n        # Connect iris again\n        # same attributes that were used last time should be selected\n        self.send_signal(self.widget.Inputs.data, self.data)\n\n        self.assertIs(self.widget.attr_x, self.data.domain[2])\n        self.assertIs(self.widget.attr_y, self.data.domain[3])\n\n    def test_score_heuristics(self):\n        domain = Domain([ContinuousVariable(c) for c in \"abcd\"],\n                        DiscreteVariable(\"e\", values=\"ab\"))\n        a = np.arange(10).reshape((10, 1))\n        data = Table(domain, np.hstack([a, a, a, a]), a >= 5)\n        self.send_signal(self.widget.Inputs.data, data)\n        vizrank = ScatterPlotVizRank(self.widget)\n        self.assertEqual([x.name for x in vizrank.score_heuristic()],\n                         list(\"abcd\"))\n\n    def test_optional_combos(self):\n        domain = self.data.domain\n        d1 = Domain(domain.attributes[:2], domain.class_var,\n                    [domain.attributes[2]])\n        t1 = self.data.transform(d1)\n        self.send_signal(self.widget.Inputs.data, t1)\n        self.widget.graph.attr_size = domain.attributes[2]\n\n        d2 = Domain(domain.attributes[:2], domain.class_var,\n                    [domain.attributes[3]])\n        t2 = self.data.transform(d2)\n        self.send_signal(self.widget.Inputs.data, t2)\n\n    def test_error_message(self):\n        \"\"\"Check if error message appears and then disappears when\n        data is removed from input\"\"\"\n        data = self.data.copy()\n        with data.unlocked():\n            data.X[:, 0] = np.nan\n        self.send_signal(self.widget.Inputs.data, data)\n        self.assertTrue(self.widget.Warning.missing_coords.is_shown())\n        self.send_signal(self.widget.Inputs.data, None)\n        self.assertFalse(self.widget.Warning.missing_coords.is_shown())\n\n    def test_report_on_empty(self):\n        self.widget.report_plot = MagicMock()\n        self.widget.report_caption = MagicMock()\n        self.widget.report_items = MagicMock()\n        self.widget.send_report()  # Essentially, don't crash\n        self.widget.report_plot.assert_not_called()\n        self.widget.report_caption.assert_not_called()\n        self.widget.report_items.assert_not_called()\n\n    def test_data_column_nans(self):\n        \"\"\"\n        ValueError cannot convert float NaN to integer.\n        In case when all column values are NaN then it throws that error.\n        GH-2061\n        \"\"\"\n        table = datasets.data_one_column_nans()\n        self.send_signal(self.widget.Inputs.data, table)\n        cb_attr_color = self.widget.controls.attr_color\n        simulate.combobox_activate_item(cb_attr_color, \"b\")\n        simulate.combobox_activate_item(self.widget.cb_attr_x, \"a\")\n        simulate.combobox_activate_item(self.widget.cb_attr_y, \"a\")\n\n        #self.widget.update_graph()\n        self.widget.graph.reset_graph()\n\n    def test_data_column_infs(self):\n        \"\"\"\n        Scatter Plot should not crash on data with infinity values\n        GH-2707\n        GH-2684\n        \"\"\"\n        table = datasets.data_one_column_infs()\n        self.send_signal(self.widget.Inputs.data, table)\n        attr_x = self.widget.controls.attr_x\n        simulate.combobox_activate_item(attr_x, \"b\")\n\n    def test_regression_line(self):\n        \"\"\"It is possible to draw the line only for pair of continuous attrs\"\"\"\n        self.send_signal(self.widget.Inputs.data, self.data)\n        self.assertTrue(self.widget.cb_reg_line.isEnabled())\n        self.assertIsNone(self.widget.graph.reg_line_item)\n        self.widget.cb_reg_line.setChecked(True)\n        self.assertIsNotNone(self.widget.graph.reg_line_item)\n        self.widget.cb_attr_y.activated.emit(4)\n        self.widget.cb_attr_y.setCurrentIndex(4)\n        self.assertFalse(self.widget.cb_reg_line.isEnabled())\n        self.assertIsNone(self.widget.graph.reg_line_item)\n\n    def test_points_combo_boxes(self):\n        \"\"\"Check Point box combo models and values\"\"\"\n        self.send_signal(self.widget.Inputs.data, self.data)\n        controls = self.widget.controls\n\n        # color and label should contain all variables\n        # size should contain only continuous variables\n        # shape should contain only discrete variables\n        for var in self.data.domain.variables + self.data.domain.metas:\n            self.assertIn(var, controls.attr_color.model())\n            self.assertIn(var, controls.attr_label.model())\n            if var.is_continuous:\n                self.assertIn(var, controls.attr_size.model())\n                self.assertNotIn(var, controls.attr_shape.model())\n            if var.is_discrete:\n                self.assertNotIn(var, controls.attr_size.model())\n                self.assertIn(var, controls.attr_shape.model())\n\n        widget = self.create_widget(OWScatterPlot)\n        self.send_signal(self.widget.Inputs.data, self.data, widget=widget)\n        self.assertEqual(controls.attr_color.currentText(),\n                         self.data.domain.class_var.name)\n\n    def test_group_selections(self):\n        self.send_signal(self.widget.Inputs.data, self.data)\n        graph = self.widget.graph\n        points = graph.scatterplot_item.points()\n        sel_column = np.zeros((len(self.data), 1))\n\n        x = self.data.X\n\n        def selectedx():\n            return self.get_output(self.widget.Outputs.selected_data).X\n\n        def selected_groups():\n            return self.get_output(self.widget.Outputs.selected_data).metas[:, 0]\n\n        def annotated():\n            return self.get_output(self.widget.Outputs.annotated_data).metas\n\n        def annotations():\n            return self.get_output(self.widget.Outputs.annotated_data).domain.metas[0].values\n\n        # Select 0:5\n        graph.select(points[:5])\n        np.testing.assert_equal(selectedx(), x[:5])\n        np.testing.assert_equal(selected_groups(), np.zeros(5))\n        sel_column[:5] = 1\n        np.testing.assert_equal(annotated(), sel_column)\n        self.assertEqual(annotations(), (\"No\", \"Yes\", ))\n\n        # Shift-select 5:10; now we have groups 0:5 and 5:10\n        with self.modifiers(Qt.ShiftModifier):\n            graph.select(points[5:10])\n        np.testing.assert_equal(selectedx(), x[:10])\n        np.testing.assert_equal(selected_groups(), np.array([0] * 5 + [1] * 5))\n        sel_column[:5] = 0\n        sel_column[5:10] = 1\n        sel_column[10:] = 2\n        np.testing.assert_equal(annotated(), sel_column)\n        self.assertEqual(len(annotations()), 3)\n\n        # Select: 15:20; we have 15:20\n        graph.select(points[15:20])\n        sel_column = np.zeros((len(self.data), 1))\n        sel_column[15:20] = 1\n        np.testing.assert_equal(selectedx(), x[15:20])\n        np.testing.assert_equal(selected_groups(), np.zeros(5))\n        self.assertEqual(annotations(), (\"No\", \"Yes\"))\n\n        # Alt-select (remove) 10:17; we have 17:20\n        with self.modifiers(Qt.AltModifier):\n            graph.select(points[10:17])\n        np.testing.assert_equal(selectedx(), x[17:20])\n        np.testing.assert_equal(selected_groups(), np.zeros(3))\n        sel_column[15:17] = 0\n        np.testing.assert_equal(annotated(), sel_column)\n        self.assertEqual(annotations(), (\"No\", \"Yes\"))\n\n        # Ctrl-Shift-select (add-to-last) 10:17; we have 17:25\n        with self.modifiers(Qt.ShiftModifier | Qt.ControlModifier):\n            graph.select(points[20:25])\n        np.testing.assert_equal(selectedx(), x[17:25])\n        np.testing.assert_equal(selected_groups(), np.zeros(8))\n        sel_column[20:25] = 1\n        np.testing.assert_equal(annotated(), sel_column)\n        self.assertEqual(annotations(), (\"No\", \"Yes\"))\n\n        # Shift-select (add) 30:35; we have 17:25, 30:35\n        with self.modifiers(Qt.ShiftModifier):\n            graph.select(points[30:35])\n        # ... then Ctrl-Shift-select (add-to-last) 10:17; we have 17:25, 30:40\n        with self.modifiers(Qt.ShiftModifier | Qt.ControlModifier):\n            graph.select(points[35:40])\n        sel_column[:] = 2\n        sel_column[17:25] = 0\n        sel_column[30:40] = 1\n        np.testing.assert_equal(selected_groups(), np.array([0] * 8 + [1] * 10))\n        np.testing.assert_equal(annotated(), sel_column)\n        self.assertEqual(len(annotations()), 3)\n\n    def test_saving_selection(self):\n        self.send_signal(self.widget.Inputs.data, self.data)  # iris\n        self.widget.graph.select_by_rectangle(QRectF(4, 3, 3, 1))\n        selected_inds = np.flatnonzero(self.widget.graph.selection)\n        settings = self.widget.settingsHandler.pack_data(self.widget)\n        np.testing.assert_equal(selected_inds,\n                                [i for i, g in settings[\"selection\"]])\n\n    def test_points_selection(self):\n        # Opening widget with saved selection should restore it\n        self.widget = self.create_widget(\n            OWScatterPlot, stored_settings={\n                \"selection_group\": [(i, 1) for i in range(50)]}\n        )\n        self.send_signal(self.widget.Inputs.data, self.data)  # iris\n        selected_data = self.get_output(self.widget.Outputs.selected_data)\n        self.assertEqual(len(selected_data), 50)\n\n        # Changing the dataset should clear selection\n        titanic = Table(\"titanic\")\n        self.send_signal(self.widget.Inputs.data, titanic)\n        selected_data = self.get_output(self.widget.Outputs.selected_data)\n        self.assertIsNone(selected_data)\n\n    def test_migrate_selection(self):\n        settings = dict(selection=list(range(2)))\n        OWScatterPlot.migrate_settings(settings, 0)\n        self.assertEqual(settings[\"selection_group\"], [(0, 1), (1, 1)])\n\n    def test_invalid_points_selection(self):\n        # if selection contains rows that are not present in the current\n        # dataset, widget should select what can be selected.\n        self.widget = self.create_widget(\n            OWScatterPlot, stored_settings={\n                \"selection_group\": [(i, 1) for i in range(50)]}\n        )\n        data = self.data[:11].copy()\n        with data.unlocked():\n            data[0, 0] = np.nan\n        self.send_signal(self.widget.Inputs.data, data)\n        self.assertIsNone(self.get_output(self.widget.Outputs.selected_data))\n\n    def test_set_strings_settings(self):\n        \"\"\"\n        Test if settings can be loaded as strings and successfully put\n        in new owplotgui combos.\n        \"\"\"\n        self.send_signal(self.widget.Inputs.data, self.data)\n        settings = self.widget.settingsHandler.pack_data(self.widget)\n        plot_settings = settings[\"context_settings\"][0].values\n        plot_settings[\"attr_label\"] = (\"sepal length\", -2)\n        plot_settings[\"attr_color\"] = (\"sepal width\", -2)\n        plot_settings[\"attr_shape\"] = (\"iris\", -2)\n        plot_settings[\"attr_size\"] = (\"petal width\", -2)\n        w = self.create_widget(OWScatterPlot, stored_settings=settings)\n        self.send_signal(self.widget.Inputs.data, self.data, widget=w)\n        self.assertEqual(w.attr_label.name, \"sepal length\")\n        self.assertEqual(w.attr_color.name, \"sepal width\")\n        self.assertEqual(w.attr_shape.name, \"iris\")\n        self.assertEqual(w.attr_size.name, \"petal width\")\n\n    def test_features_and_no_data(self):\n        \"\"\"\n        Prevent crashing when features are sent but no data.\n        \"\"\"\n        domain = Table(\"iris\").domain\n        self.send_signal(self.widget.Inputs.features,\n                         AttributeList(domain.variables))\n        self.send_signal(self.widget.Inputs.features, None)\n\n    def test_features_and_data(self):\n        self.assertTrue(self.widget.attr_box.isEnabled())\n        self.send_signal(self.widget.Inputs.data, self.data)\n        x, y = self.widget.graph.scatterplot_item.getData()\n        np.testing.assert_array_equal(x, self.data.X[:, 0])\n        np.testing.assert_array_equal(y, self.data.X[:, 1])\n        self.send_signal(self.widget.Inputs.features,\n                         AttributeList(self.data.domain[2:]))\n        self.assertIs(self.widget.attr_x, self.data.domain[2])\n        self.assertIs(self.widget.attr_y, self.data.domain[3])\n        self.assertFalse(self.widget.attr_box.isEnabled())\n        self.assertFalse(self.widget.vizrank.isEnabled())\n        x, y = self.widget.graph.scatterplot_item.getData()\n        np.testing.assert_array_equal(x, self.data.X[:, 2])\n        np.testing.assert_array_equal(y, self.data.X[:, 3])\n\n        self.send_signal(self.widget.Inputs.data, None)\n        self.send_signal(self.widget.Inputs.data, self.data)\n        self.assertIs(self.widget.attr_x, self.data.domain[2])\n        self.assertIs(self.widget.attr_y, self.data.domain[3])\n        self.assertFalse(self.widget.attr_box.isEnabled())\n        self.assertFalse(self.widget.vizrank.isEnabled())\n\n        self.send_signal(self.widget.Inputs.features, None)\n        self.assertTrue(self.widget.attr_box.isEnabled())\n        self.assertTrue(self.widget.vizrank.isEnabled())\n\n    def test_output_features(self):\n        data = Table(\"iris\")\n        self.send_signal(self.widget.Inputs.data, data)\n\n        # This doesn't work because combo's callbacks are connected to signal\n        # `activated`, which is only triggered by user interaction, and not to\n        # `currentIndexChanged`\n        # combo_y = self.widget.controls.attr_y\n        # combo_y.setCurrentIndex(combo_y.model().indexOf(data.domain[3]))\n        # This is a workaround\n        self.widget.attr_y = data.domain[3]\n        self.widget.attr_changed()\n\n        features = self.get_output(self.widget.Outputs.features)\n        self.assertEqual(features, [data.domain[0], data.domain[3]])\n\n    def test_vizrank(self):\n        data = Table(\"iris\")\n        self.send_signal(self.widget.Inputs.data, data)\n        vizrank = ScatterPlotVizRank(self.widget)\n        n_states = len(data.domain.attributes)\n        n_states = n_states * (n_states - 1) / 2\n        states = [state for state in vizrank.iterate_states(None)]\n        self.assertEqual(len(states), n_states)\n        self.assertEqual(len(set(states)), n_states)\n        self.assertIsNotNone(vizrank.compute_score(states[0]))\n        self.send_signal(self.widget.Inputs.data, data[:9])\n        self.assertIsNone(vizrank.compute_score(states[0]))\n\n        data = Table(\"housing\")[::10]\n        self.send_signal(self.widget.Inputs.data, data)\n        vizrank = ScatterPlotVizRank(self.widget)\n        states = [state for state in vizrank.iterate_states(None)]\n        self.assertIsNotNone(vizrank.compute_score(states[0]))\n\n    def test_vizrank_class_nan(self):\n        \"\"\"\n        When class values are nan, vizrank should be disabled. It should behave like\n        the class column is missing.\n        GH-2757\n        \"\"\"\n        def assert_vizrank_enabled(data, is_enabled):\n            self.send_signal(self.widget.Inputs.data, data)\n            self.assertEqual(is_enabled, self.widget.vizrank_button.isEnabled())\n\n        data1 = Table(\"iris\")[::30]\n        data2 = Table(\"iris\")[::30].copy()\n        with data2.unlocked():\n            data2.Y[:] = np.nan\n        domain = Domain(\n            attributes=data2.domain.attributes[:4], class_vars=DiscreteVariable(\"iris\", values=()))\n        data2 = Table(domain, data2.X, Y=data2.Y)\n        data3 = Table(\"iris\")[::30].copy()\n        with data3.unlocked():\n            data3.Y[:] = np.nan\n\n        for data, is_enabled in zip([data1, data2, data1, data3, data1],\n                                    [True, False, True, False, True]):\n            assert_vizrank_enabled(data, is_enabled)\n\n    def test_vizrank_nonprimitives(self):\n        \"\"\"VizRank does not try to include non primitive attributes\"\"\"\n        data = Table(\"zoo\")\n        self.send_signal(self.widget.Inputs.data, data)\n        with patch(\"Orange.widgets.visualize.owscatterplot.ReliefF\",\n                   new=lambda *_1, **_2: lambda data: np.arange(len(data))):\n            self.widget.vizrank.score_heuristic()\n\n    def test_vizrank_enabled(self):\n        self.send_signal(self.widget.Inputs.data, self.data)\n        self.assertTrue(self.widget.vizrank_button.isEnabled())\n        self.assertEqual(self.widget.vizrank_button.toolTip(), \"\")\n        self.assertTrue(self.widget.vizrank.button.isEnabled())\n        self.widget.vizrank.button.click()\n\n    def test_vizrank_enabled_no_data(self):\n        self.send_signal(self.widget.Inputs.data, None)\n        self.assertFalse(self.widget.vizrank_button.isEnabled())\n        self.assertEqual(self.widget.vizrank_button.toolTip(), \"No data on input\")\n\n    def test_vizrank_enabled_sparse_data(self):\n        self.send_signal(self.widget.Inputs.data, self.data.to_sparse())\n        self.assertFalse(self.widget.vizrank_button.isEnabled())\n        self.assertEqual(self.widget.vizrank_button.toolTip(), \"Data is sparse\")\n\n    def test_vizrank_enabled_constant_data(self):\n        domain = Domain([ContinuousVariable(\"c1\"),\n                         ContinuousVariable(\"c2\"),\n                         ContinuousVariable(\"c3\"),\n                         ContinuousVariable(\"c4\")],\n                        DiscreteVariable(\"cls\", values=(\"a\", \"b\")))\n        X = np.zeros((10, 4))\n        table = Table(domain, X, np.random.randint(2, size=10))\n        self.send_signal(self.widget.Inputs.data, table)\n        self.assertEqual(self.widget.vizrank_button.toolTip(), \"\")\n        self.assertTrue(self.widget.vizrank_button.isEnabled())\n        self.assertTrue(self.widget.vizrank.button.isEnabled())\n        self.widget.vizrank.button.click()\n\n    def test_vizrank_enabled_two_features(self):\n        self.send_signal(self.widget.Inputs.data, self.data[:, :2])\n        self.assertFalse(self.widget.vizrank_button.isEnabled())\n        self.assertEqual(self.widget.vizrank_button.toolTip(),\n                         \"Not enough features for ranking\")\n\n    def test_vizrank_enabled_no_color_var(self):\n        self.send_signal(self.widget.Inputs.data, self.data[:, :3])\n        self.assertFalse(self.widget.vizrank_button.isEnabled())\n        self.assertEqual(self.widget.vizrank_button.toolTip(),\n                         \"Color variable is not selected\")\n\n    def test_vizrank_enabled_color_var_nans(self):\n        domain = Domain([ContinuousVariable(\"c1\"),\n                         ContinuousVariable(\"c2\"),\n                         ContinuousVariable(\"c3\"),\n                         ContinuousVariable(\"c4\")],\n                        DiscreteVariable(\"cls\", values=(\"a\", \"b\")))\n        table = Table(domain, np.random.random((10, 4)), np.full(10, np.nan))\n        self.send_signal(self.widget.Inputs.data, table)\n        self.assertFalse(self.widget.vizrank_button.isEnabled())\n        self.assertEqual(self.widget.vizrank_button.toolTip(),\n                         \"Color variable has no values\")\n\n    def test_auto_send_selection(self):\n        \"\"\"\n        Scatter Plot automatically sends selection only when the checkbox Send automatically\n        is checked.\n        GH-2649\n        GH-2646\n        \"\"\"\n        data = Table(\"iris\")\n        self.send_signal(self.widget.Inputs.data, data)\n        self.widget.controls.auto_commit.setChecked(False)\n        self.assertFalse(self.widget.controls.auto_commit.isChecked())\n        self._select_data()\n        self.assertIsNone(self.get_output(self.widget.Outputs.selected_data))\n        self.widget.controls.auto_commit.setChecked(True)\n        output = self.get_output(self.widget.Outputs.selected_data)\n        self.assertIsInstance(output, Table)\n\n    def test_color_is_optional(self):\n        zoo = Table(\"zoo\")\n        backbone, breathes, airborne, type = \\\n            [zoo.domain[x] for x in [\"backbone\", \"breathes\", \"airborne\", \"type\"]]\n        default_x, default_y, default_color = \\\n            zoo.domain[0], zoo.domain[1], zoo.domain.class_var\n        attr_x = self.widget.controls.attr_x\n        attr_y = self.widget.controls.attr_y\n        attr_color = self.widget.controls.attr_color\n\n        # Send dataset, ensure defaults are what we expect them to be\n        self.send_signal(self.widget.Inputs.data, zoo)\n        self.assertEqual(attr_x.currentText(), default_x.name)\n        self.assertEqual(attr_y.currentText(), default_y.name)\n        self.assertEqual(attr_color.currentText(), default_color.name)\n        # Select different values\n        simulate.combobox_activate_item(attr_x, backbone.name)\n        simulate.combobox_activate_item(attr_y, breathes.name)\n        simulate.combobox_activate_item(attr_color, airborne.name)\n\n        # Send compatible dataset, values should not change\n        zoo2 = zoo[:, (backbone, breathes, airborne, type)]\n        self.send_signal(self.widget.Inputs.data, zoo2)\n        self.assertEqual(attr_x.currentText(), backbone.name)\n        self.assertEqual(attr_y.currentText(), breathes.name)\n        self.assertEqual(attr_color.currentText(), airborne.name)\n\n        # Send dataset without color variable\n        # x and y should remain, color reset to default\n        zoo3 = zoo[:, (backbone, breathes, type)]\n        self.send_signal(self.widget.Inputs.data, zoo3)\n        self.assertEqual(attr_x.currentText(), backbone.name)\n        self.assertEqual(attr_y.currentText(), breathes.name)\n        self.assertEqual(attr_color.currentText(), default_color.name)\n\n        # Send dataset without x\n        # y and color should be the same as with zoo\n        zoo4 = zoo[:, (default_x, default_y, breathes, airborne, type)]\n        self.send_signal(self.widget.Inputs.data, zoo4)\n        self.assertEqual(attr_x.currentText(), default_x.name)\n        self.assertEqual(attr_y.currentText(), default_y.name)\n        self.assertEqual(attr_color.currentText(), default_color.name)\n\n        # Send dataset compatible with zoo2 and zoo3\n        # Color should reset to one in zoo3, as it was used more\n        # recently\n        zoo5 = zoo[:, (default_x, backbone, breathes, airborne, type)]\n        self.send_signal(self.widget.Inputs.data, zoo5)\n        self.assertEqual(attr_x.currentText(), backbone.name)\n        self.assertEqual(attr_y.currentText(), breathes.name)\n        self.assertEqual(attr_color.currentText(), type.name)\n\n    def test_handle_metas(self):\n        \"\"\"\n        Scatter Plot Graph can handle metas\n        GH-2699\n        \"\"\"\n        w = self.widget\n        data = Table(\"iris\")\n        domain = Domain(\n            attributes=data.domain.attributes[:2],\n            class_vars=data.domain.class_vars,\n            metas=data.domain.attributes[2:]\n        )\n        data = data.transform(domain).copy()\n        # Sometimes floats in metas are saved as objects\n        with data.unlocked():\n            data.metas = data.metas.astype(object)\n        self.send_signal(w.Inputs.data, data)\n        simulate.combobox_activate_item(w.cb_attr_x, data.domain.metas[1].name)\n        simulate.combobox_activate_item(w.controls.attr_color, data.domain.metas[0].name)\n        w.graph.reset_graph()\n\n    def test_subset_data(self):\n        \"\"\"\n        Scatter Plot subset data is sent to Scatter Plot Graph\n        GH-2773\n        \"\"\"\n        data = Table(\"iris\")\n        w = self.widget\n        self.send_signal(w.Inputs.data, data)\n        self.send_signal(w.Inputs.data_subset, data[::30])\n        self.assertEqual(len(w.subset_indices), 5)\n\n    def test_opacity_warning(self):\n        data = Table(\"iris\")\n        w = self.widget\n        self.send_signal(w.Inputs.data, data)\n        w.graph.controls.alpha_value.setSliderPosition(10)\n        self.assertFalse(w.Warning.transparent_subset.is_shown())\n        self.send_signal(w.Inputs.data_subset, data[::30])\n        self.assertTrue(w.Warning.transparent_subset.is_shown())\n        w.graph.controls.alpha_value.setSliderPosition(200)\n        self.assertFalse(w.Warning.transparent_subset.is_shown())\n        w.graph.controls.alpha_value.setSliderPosition(10)\n        self.assertTrue(w.Warning.transparent_subset.is_shown())\n        self.send_signal(w.Inputs.data_subset, None)\n        self.assertFalse(w.Warning.transparent_subset.is_shown())\n\n    def test_jittering(self):\n        self.send_signal(self.widget.Inputs.data, self.data)\n        self.widget.graph.controls.jitter_continuous.setChecked(True)\n        self.widget.graph.controls.jitter_size.setValue(1)\n\n    def test_metas_zero_column(self):\n        \"\"\"\n        Prevent crash when metas column is zero.\n        GH-2775\n        \"\"\"\n        data = Table(\"iris\")\n        domain = data.domain\n        domain = Domain(domain.attributes[:3], domain.class_vars, domain.attributes[3:])\n        data = data.transform(domain).copy()\n        with data.unlocked():\n            data.metas[:, 0] = 0\n        w = self.widget\n        self.send_signal(w.Inputs.data, data)\n        simulate.combobox_activate_item(w.controls.attr_x, domain.metas[0].name)\n\n    def test_tooltip(self):\n        # The test tests presence of some data,\n        # but avoids checking the exact format\n        data = Table(\"heart_disease\")\n        self.send_signal(self.widget.Inputs.data, data)\n        widget = self.widget\n        graph = widget.graph\n        scatterplot_item = graph.scatterplot_item\n\n        widget.controls.attr_x = data.domain[\"chest pain\"]\n        widget.controls.attr_y = data.domain[\"cholesterol\"]\n        all_points = scatterplot_item.points()\n\n        event = MagicMock()\n        with patch.object(scatterplot_item, \"mapFromScene\"), \\\n                patch.object(QToolTip, \"showText\") as show_text:\n\n            # Single point hovered\n            with patch.object(scatterplot_item, \"pointsAt\",\n                              return_value=[all_points[42]]):\n                # Show just x and y attribute\n                widget.tooltip_shows_all = False\n                self.assertTrue(graph.help_event(event))\n                (_, text), _ = show_text.call_args\n                self.assertIn(\"age = {}\".format(data[42, \"age\"]), text)\n                self.assertIn(\"gender = {}\".format(data[42, \"gender\"]), text)\n                self.assertNotIn(\"max HR = {}\".format(data[42, \"max HR\"]), text)\n                self.assertNotIn(\"others\", text)\n\n                # Show all attributes\n                widget.tooltip_shows_all = True\n                self.assertTrue(graph.help_event(event))\n                (_, text), _ = show_text.call_args\n                self.assertIn(\"age = {}\".format(data[42, \"age\"]), text)\n                self.assertIn(\"gender = {}\".format(data[42, \"gender\"]), text)\n                self.assertIn(\"max HR = {}\".format(data[42, \"max HR\"]), text)\n                self.assertIn(\"... and 4 others\", text)\n\n            # Two points hovered\n            with patch.object(scatterplot_item, \"pointsAt\",\n                              return_value=[all_points[42], all_points[100]]):\n                self.assertTrue(graph.help_event(event))\n                (_, text), _ = show_text.call_args\n                self.assertIn(\"age = {}\".format(data[42, \"age\"]), text)\n                self.assertIn(\"gender = {}\".format(data[42, \"gender\"]), text)\n                self.assertIn(\"age = {}\".format(data[100, \"age\"]), text)\n                self.assertIn(\"gender = {}\".format(data[100, \"gender\"]), text)\n\n            # No points hovered\n            with patch.object(scatterplot_item, \"pointsAt\",\n                              return_value=[]):\n                show_text.reset_mock()\n                self.assertFalse(graph.help_event(event))\n                self.assertEqual(show_text.call_count, 0)\n\n    def test_many_discrete_values(self):\n        \"\"\"\n        Do not show all discrete values if there are too many.\n        Also test for values with a nan.\n        GH-2804\n        \"\"\"\n        def prepare_data():\n            data = Table(\"iris\")\n            values = list(range(15))\n            class_var = DiscreteVariable(\"iris5\", values=[str(v) for v in values])\n            data = data.transform(\n                Domain(attributes=data.domain.attributes,\n                       class_vars=[class_var])).copy()\n            with data.unlocked():\n                data.Y = np.array(values * 10, dtype=float)\n            return data\n\n        def assert_equal(data, max):\n            self.send_signal(self.widget.Inputs.data, data)\n            pen_data, brush_data = self.widget.graph.get_colors()\n            self.assertEqual(max, len(np.unique([id(p) for p in pen_data])), )\n\n        assert_equal(prepare_data(), MAX_COLORS)\n        # data with nan value\n        data = prepare_data()\n        with data.unlocked():\n            data.Y[42] = np.nan\n        assert_equal(data, MAX_COLORS + 1)\n\n    def test_invalidated_same_features(self):\n        self.widget.setup_plot = Mock()\n        # send data and set default features\n        self.send_signal(self.widget.Inputs.data, self.data)\n        self.widget.setup_plot.assert_called_once()\n        self.assertListEqual(self.widget.effective_variables,\n                             list(self.data.domain.attributes[:2]))\n\n        # send the same features as already set\n        self.widget.setup_plot.reset_mock()\n        self.send_signal(self.widget.Inputs.features,\n                         AttributeList(self.data.domain.attributes[:2]))\n        self.widget.setup_plot.assert_not_called()\n        self.assertListEqual(self.widget.effective_variables,\n                             list(self.data.domain.attributes[:2]))\n\n    def test_invalidated_same_time(self):\n        self.widget.setup_plot = Mock()\n        # send data and features at the same time (data first)\n        features = self.data.domain.attributes[:2]\n        signals = [(self.widget.Inputs.data, self.data),\n                   (self.widget.Inputs.features, AttributeList(features))]\n        self.send_signals(signals)\n        self.widget.setup_plot.assert_called_once()\n        self.assertListEqual(self.widget.effective_variables, list(features))\n\n    def test_invalidated_features_first(self):\n        self.widget.setup_plot = Mock()\n        # send features (same as default ones)\n        self.send_signal(self.widget.Inputs.features,\n                         AttributeList(self.data.domain.attributes[:2]))\n        self.assertListEqual(self.widget.effective_variables, [])\n        self.widget.setup_plot.assert_called_once()\n\n        # send data\n        self.widget.setup_plot.reset_mock()\n        self.send_signal(self.widget.Inputs.data, self.data)\n        self.widget.setup_plot.assert_called()\n        self.assertListEqual(self.widget.effective_variables,\n                             list(self.data.domain.attributes[:2]))\n\n    def test_invalidated_same_time_features_first(self):\n        self.widget.setup_plot = Mock()\n        # send features and data at the same time (features first)\n        features = self.data.domain.attributes[:2]\n        signals = [(self.widget.Inputs.features, AttributeList(features)),\n                   (self.widget.Inputs.data, self.data)]\n        self.send_signals(signals)\n        self.widget.setup_plot.assert_called_once()\n        self.assertListEqual(self.widget.effective_variables, list(features))\n\n        self.widget.setup_plot.reset_mock()\n        features = self.data.domain.attributes[2:]\n        signals = [(self.widget.Inputs.features, AttributeList(features)),\n                   (self.widget.Inputs.data, self.data)]\n        self.send_signals(signals)\n        self.widget.setup_plot.assert_called_once()\n\n    def test_invalidated_diff_features(self):\n        self.widget.setup_plot = Mock()\n        # send data and set default features\n        self.send_signal(self.widget.Inputs.data, self.data)\n        self.widget.setup_plot.assert_called_once()\n        self.assertListEqual(self.widget.effective_variables,\n                             list(self.data.domain.attributes[:2]))\n\n        # send different features\n        self.widget.setup_plot.reset_mock()\n        self.send_signal(self.widget.Inputs.features,\n                         AttributeList(self.data.domain.attributes[2:4]))\n        self.widget.setup_plot.assert_called_once()\n        self.assertListEqual(self.widget.effective_variables,\n                             list(self.data.domain.attributes[2:4]))\n\n    def test_invalidated_diff_features_same_time(self):\n        self.widget.setup_plot = Mock()\n        # send data and different features at the same time (data first)\n        features = self.data.domain.attributes[2:4]\n        signals = [(self.widget.Inputs.data, self.data),\n                   (self.widget.Inputs.features, AttributeList(features))]\n        self.send_signals(signals)\n        self.widget.setup_plot.assert_called_once()\n        self.assertListEqual(self.widget.effective_variables, list(features))\n\n    def test_invalidated_diff_features_features_first(self):\n        self.widget.setup_plot = Mock()\n        # send features (not the same as defaults)\n        self.send_signal(self.widget.Inputs.features,\n                         AttributeList(self.data.domain.attributes[2:4]))\n        self.assertListEqual(self.widget.effective_variables, [])\n        self.widget.setup_plot.assert_called_once()\n\n        # send data\n        self.widget.setup_plot.reset_mock()\n        self.send_signal(self.widget.Inputs.data, self.data)\n        self.widget.setup_plot.assert_called_once()\n        self.assertListEqual(self.widget.effective_variables,\n                             list(self.data.domain.attributes[2:4]))\n\n    def test_invalidated_diff_features_same_time_features_first(self):\n        self.widget.setup_plot = Mock()\n        # send data and different features at the same time (features first)\n        features = self.data.domain.attributes[2:4]\n        signals = [(self.widget.Inputs.features, AttributeList(features)),\n                   (self.widget.Inputs.data, self.data)]\n        self.send_signals(signals)\n        self.widget.setup_plot.assert_called_once()\n        self.assertListEqual(self.widget.effective_variables, list(features))\n\n    @patch('Orange.widgets.visualize.owscatterplot.ScatterPlotVizRank.'\n           'on_manual_change')\n    def test_vizrank_receives_manual_change(self, on_manual_change):\n        # Recreate the widget so the patch kicks in\n        self.widget = self.create_widget(OWScatterPlot)\n        data = Table(\"iris.tab\")\n        self.send_signal(self.widget.Inputs.data, data)\n        model = self.widget.controls.attr_x.model()\n        self.widget.attr_x = model[0]\n        self.widget.attr_y = model[1]\n        simulate.combobox_activate_index(self.widget.controls.attr_x, 2)\n        self.assertIs(self.widget.attr_x, model[2])\n        on_manual_change.assert_called_with(model[2], model[1])\n\n    def test_on_manual_change(self):\n        data = Table(\"iris.tab\")\n        self.send_signal(self.widget.Inputs.data, data)\n        vizrank = self.widget.vizrank\n        vizrank.toggle()\n        self.process_events(until=lambda: not vizrank.keep_running)\n\n        model = vizrank.rank_model\n        attrs = model.data(model.index(3, 0), vizrank._AttrRole)\n        vizrank.on_manual_change(*attrs)\n        selection = vizrank.rank_table.selectedIndexes()\n        self.assertEqual(len(selection), 1)\n        self.assertEqual(selection[0].row(), 3)\n\n        vizrank.on_manual_change(*attrs[::-1])\n        selection = vizrank.rank_table.selectedIndexes()\n        self.assertEqual(len(selection), 0)\n\n    def test_regression_lines_appear(self):\n        self.widget.graph.controls.show_reg_line.setChecked(True)\n        self.assertEqual(len(self.widget.graph.reg_line_items), 0)\n        self.send_signal(self.widget.Inputs.data, self.data)\n        self.assertEqual(len(self.widget.graph.reg_line_items), 4)\n        simulate.combobox_activate_index(self.widget.controls.attr_color, 0)\n        self.assertEqual(len(self.widget.graph.reg_line_items), 1)\n        data = self.data.copy()\n        with data.unlocked():\n            data[:, 0] = np.nan\n        self.send_signal(self.widget.Inputs.data, data)\n        self.assertEqual(len(self.widget.graph.reg_line_items), 0)\n\n    def test_regression_line_coeffs(self):\n        widget = self.widget\n        graph = widget.graph\n        xy = np.array([[0, 0], [1, 0], [1, 2], [2, 2],\n                       [0, 1], [1, 3], [2, 5]], dtype=float)\n        colors = np.array([0, 0, 0, 0, 1, 1, 1], dtype=float)\n        widget.get_coordinates_data = lambda: xy.T\n        widget.can_draw_regresssion_line = lambda: True\n        widget.get_color_data = lambda: colors\n        widget.is_continuous_color = lambda: False\n        graph.palette = DefaultRGBColors\n        graph.controls.show_reg_line.setChecked(True)\n\n        graph.update_regression_line()\n\n        line1 = graph.reg_line_items[1]\n        self.assertEqual(line1.pos().x(), 0)\n        self.assertEqual(line1.pos().y(), 0)\n        self.assertEqual(line1.angle, 45)\n        self.assertEqual(line1.pen.color().hue(), graph.palette[0].hue())\n\n        line2 = graph.reg_line_items[2]\n        self.assertEqual(line2.pos().x(), 0)\n        self.assertEqual(line2.pos().y(), 1)\n        self.assertAlmostEqual(line2.angle, np.degrees(np.arctan2(2, 1)))\n        self.assertEqual(line2.pen.color().hue(), graph.palette[1].hue())\n\n        graph.orthonormal_regression = True\n        graph.update_regression_line()\n\n        line1 = graph.reg_line_items[1]\n        self.assertEqual(line1.pos().x(), 0)\n        self.assertAlmostEqual(line1.pos().y(), -0.6180339887498949)\n        self.assertAlmostEqual(line1.angle, 58.28252558853899)\n        self.assertEqual(line1.pen.color().hue(), graph.palette[0].hue())\n\n        line2 = graph.reg_line_items[2]\n        self.assertEqual(line2.pos().x(), 0)\n        self.assertEqual(line2.pos().y(), 1)\n        self.assertAlmostEqual(line2.angle, np.degrees(np.arctan2(2, 1)))\n        self.assertEqual(line2.pen.color().hue(), graph.palette[1].hue())\n\n    def test_orthonormal_line(self):\n        color = QColor(1, 2, 3)\n        width = 42\n        # Normal line\n        line = OWScatterPlotGraph._orthonormal_line(\n            np.array([0, 1, 1, 2]), np.array([0, 0, 2, 2]), color, width)\n        self.assertEqual(line.pos().x(), 0)\n        self.assertAlmostEqual(line.pos().y(), -0.6180339887498949)\n        self.assertAlmostEqual(line.angle, 58.28252558853899)\n        self.assertEqual(line.pen.color(), color)\n        self.assertEqual(line.pen.width(), width)\n\n        # Normal line, negative slope\n        line = OWScatterPlotGraph._orthonormal_line(\n            np.array([1, 2, 3]), np.array([3, 2, 1]), color, width)\n        self.assertEqual(line.pos().x(), 1)\n        self.assertEqual(line.pos().y(), 3)\n        self.assertEqual(line.angle % 360, 315)\n\n        # Horizontal line\n        line = OWScatterPlotGraph._orthonormal_line(\n            np.array([10, 11, 12]), np.array([42, 42, 42]), color, width)\n        self.assertEqual(line.pos().x(), 10)\n        self.assertEqual(line.pos().y(), 42)\n        self.assertEqual(line.angle, 0)\n\n        # Vertical line\n        line = OWScatterPlotGraph._orthonormal_line(\n            np.array([42, 42, 42]), np.array([10, 11, 12]), color, width)\n        self.assertEqual(line.pos().x(), 42)\n        self.assertEqual(line.pos().y(), 10)\n        self.assertEqual(line.angle, 90)\n\n        # No line because all points coincide\n        line = OWScatterPlotGraph._orthonormal_line(\n            np.array([1, 1, 1]), np.array([42, 42, 42]), color, width)\n        self.assertIsNone(line)\n\n        # No line because the group is symmetric\n        line = OWScatterPlotGraph._orthonormal_line(\n            np.array([1, 1, 2, 2]), np.array([42, 5, 5, 42]), color, width)\n        self.assertIsNone(line)\n\n    def test_regression_line(self):\n        color = QColor(1, 2, 3)\n        width = 42\n        # Normal line\n        line = OWScatterPlotGraph._regression_line(\n            np.array([0, 1, 1, 2]), np.array([0, 0, 2, 2]), color, width)\n        self.assertEqual(line.pos().x(), 0)\n        self.assertAlmostEqual(line.pos().y(), 0)\n        self.assertEqual(line.angle, 45)\n        self.assertEqual(line.pen.color(), color)\n        self.assertEqual(line.pen.width(), width)\n\n        # Normal line, negative slope\n        line = OWScatterPlotGraph._regression_line(\n            np.array([1, 2, 3]), np.array([3, 2, 1]), color, width)\n        self.assertEqual(line.pos().x(), 1)\n        self.assertEqual(line.pos().y(), 3)\n        self.assertEqual(line.angle % 360, 315)\n\n        # Horizontal line\n        line = OWScatterPlotGraph._regression_line(\n            np.array([10, 11, 12]), np.array([42, 42, 42]), color, width)\n        self.assertEqual(line.pos().x(), 10)\n        self.assertEqual(line.pos().y(), 42)\n        self.assertEqual(line.angle, 0)\n\n        # Vertical line\n        line = OWScatterPlotGraph._regression_line(\n            np.array([42, 42, 42]), np.array([10, 11, 12]), color, width)\n        self.assertIsNone(line)\n\n        # No line because all points coincide\n        line = OWScatterPlotGraph._regression_line(\n            np.array([1, 1, 1]), np.array([42, 42, 42]), color, width)\n        self.assertIsNone(line)\n\n    def test_add_line_calls_proper_regressor(self):\n        graph = self.widget.graph\n        graph._orthonormal_line = Mock(return_value=None)\n        graph._regression_line = Mock(return_value=None)\n        x, y, c = Mock(), Mock(), Mock()\n\n        graph.orthonormal_regression = True\n        graph._add_line(x, y, c)\n        graph._orthonormal_line.assert_called_once_with(x, y, c, 3, 1)\n        graph._regression_line.assert_not_called()\n        graph._orthonormal_line.reset_mock()\n\n        graph.orthonormal_regression = False\n        graph._add_line(x, y, c)\n        graph._regression_line.assert_called_with(x, y, c, 3, 1)\n        graph._orthonormal_line.assert_not_called()\n\n    def test_no_regression_line(self):\n        graph = self.widget.graph\n        graph._orthonormal_line = lambda *_: None\n        graph.orthonormal_regression = True\n\n        graph.plot_widget.addItem = Mock()\n\n        x, y, c = Mock(), Mock(), Mock()\n        graph._add_line(x, y, c)\n        graph.plot_widget.addItem.assert_not_called()\n        self.assertEqual(graph.reg_line_items, [])\n\n    def test_update_regression_line_calls_add_line(self):\n        widget = self.widget\n        graph = widget.graph\n        x, y = np.array([[0, 0], [1, 0], [1, 2], [2, 2],\n                         [0, 1], [1, 3], [2, 5]], dtype=float).T\n        colors = np.array([0, 0, 0, 0, 1, 1, 1], dtype=float)\n        widget.get_coordinates_data = lambda: (x, y)\n        widget.can_draw_regresssion_line = lambda: True\n        widget.get_color_data = lambda: colors\n        widget.is_continuous_color = lambda: False\n        graph.palette = DefaultRGBColors\n        graph.controls.show_reg_line.setChecked(True)\n\n        graph._add_line = Mock()\n\n        graph.update_regression_line()\n        (args1, _), (args2, _), (args3, _) = graph._add_line.call_args_list\n        np.testing.assert_equal(args1[0], x)\n        np.testing.assert_equal(args1[1], y)\n        self.assertEqual(args1[2], QColor(\"#505050\"))\n\n        np.testing.assert_equal(args2[0], x[:4])\n        np.testing.assert_equal(args2[1], y[:4])\n        self.assertEqual(args2[2].hue(), graph.palette[0].hue())\n\n        np.testing.assert_equal(args3[0], x[4:])\n        np.testing.assert_equal(args3[1], y[4:])\n        self.assertEqual(args3[2].hue(), graph.palette[1].hue())\n        graph._add_line.reset_mock()\n\n        # Continuous color - just a single line\n        widget.is_continuous_color = lambda: True\n        graph.update_regression_line()\n        graph._add_line.assert_called_once()\n        args1, _ = graph._add_line.call_args_list[0]\n        np.testing.assert_equal(args1[0], x)\n        np.testing.assert_equal(args1[1], y)\n        self.assertEqual(args1[2].hue(), QColor(\"#505050\").hue())\n        graph._add_line.reset_mock()\n        widget.is_continuous_color = lambda: False\n\n        # No palette - just a single line\n        graph.palette = None\n        graph.update_regression_line()\n        graph._add_line.assert_called_once()\n        graph._add_line.reset_mock()\n        graph.palette = DefaultRGBColors\n\n        # Regression line is disabled\n        graph.show_reg_line = False\n        graph.update_regression_line()\n        graph._add_line.assert_not_called()\n        graph.show_reg_line = True\n\n        # No colors - just one line\n        widget.get_color_data = lambda: None\n        graph.update_regression_line()\n        graph._add_line.assert_called_once()\n        graph._add_line.reset_mock()\n\n        # No data\n        widget.get_coordinates_data = lambda: (None, None)\n        graph.update_regression_line()\n        graph._add_line.assert_not_called()\n        graph.show_reg_line = True\n        widget.get_coordinates_data = lambda: (x, y)\n\n        # One color group contains just one point - skip that line\n        widget.get_color_data = lambda: np.array([0] + [1] * (len(x) - 1))\n\n        graph.update_regression_line()\n        (args1, _), (args2, _) = graph._add_line.call_args_list\n        np.testing.assert_equal(args1[0], x)\n        np.testing.assert_equal(args1[1], y)\n        self.assertEqual(args1[2].hue(), QColor(\"#505050\").hue())\n\n        np.testing.assert_equal(args2[0], x[1:])\n        np.testing.assert_equal(args2[1], y[1:])\n        self.assertEqual(args2[2].hue(), graph.palette[1].hue())\n\n    def test_update_regression_line_is_called(self):\n        widget = self.widget\n        graph = widget.graph\n        urline = graph.update_regression_line = Mock()\n\n        self.send_signal(widget.Inputs.data, self.data)\n        urline.assert_called_once()\n        urline.reset_mock()\n\n        self.send_signal(widget.Inputs.data, None)\n        urline.assert_called_once()\n        urline.reset_mock()\n\n        self.send_signal(widget.Inputs.data, self.data)\n        urline.assert_called_once()\n        urline.reset_mock()\n\n        simulate.combobox_activate_index(self.widget.controls.attr_color, 0)\n        urline.assert_called_once()\n        urline.reset_mock()\n\n        simulate.combobox_activate_index(self.widget.controls.attr_color, 2)\n        urline.assert_called_once()\n        urline.reset_mock()\n\n        simulate.combobox_activate_index(self.widget.controls.attr_x, 3)\n        urline.assert_called_once()\n        urline.reset_mock()\n\n    def test_time_axis(self):\n        a = np.array([[1581953776, 1], [1581963776, 2], [1582953776, 3]])\n        d1 = Domain([ContinuousVariable(\"time\"), ContinuousVariable(\"value\")])\n        data = Table.from_numpy(d1, a)\n        d2 = Domain([TimeVariable(\"time\"), ContinuousVariable(\"value\")])\n        data_time = Table.from_numpy(d2, a)\n\n        x_axis = self.widget.graph.plot_widget.plotItem.getAxis(\"bottom\")\n\n        self.send_signal(self.widget.Inputs.data, data)\n        self.assertFalse(x_axis._use_time)\n        _ticks = x_axis.tickValues(1581953776, 1582953776, 1000)\n        ticks = x_axis.tickStrings(_ticks[0][1], 1, _ticks[0][0])\n        try:\n            float(ticks[0])\n        except ValueError:\n            self.fail(\"axis should display floats\")\n\n        self.send_signal(self.widget.Inputs.data, data_time)\n        self.assertTrue(x_axis._use_time)\n        _ticks = x_axis.tickValues(1581953776, 1582953776, 1000)\n        ticks = x_axis.tickStrings(_ticks[0][1], 1, _ticks[0][0])\n        with self.assertRaises(ValueError):\n            float(ticks[0])\n\n    def test_visual_settings(self):\n        super().test_visual_settings()\n\n        graph = self.widget.graph\n        font = QFont()\n        font.setItalic(True)\n        font.setFamily(\"Helvetica\")\n\n        key, value = ('Fonts', 'Axis title', 'Font size'), 16\n        self.widget.set_visual_settings(key, value)\n        key, value = ('Fonts', 'Axis title', 'Italic'), True\n        self.widget.set_visual_settings(key, value)\n        font.setPointSize(16)\n        for item in graph.parameter_setter.axis_items:\n            self.assertFontEqual(item.label.font(), font)\n\n        key, value = ('Fonts', 'Axis ticks', 'Font size'), 15\n        self.widget.set_visual_settings(key, value)\n        key, value = ('Fonts', 'Axis ticks', 'Italic'), True\n        self.widget.set_visual_settings(key, value)\n        font.setPointSize(15)\n        for item in graph.parameter_setter.axis_items:\n            self.assertFontEqual(item.style[\"tickFont\"], font)\n\n        self.widget.graph.controls.show_reg_line.setChecked(True)\n        self.assertGreater(len(graph.parameter_setter.reg_line_label_items), 0)\n\n        key, value = ('Fonts', 'Line label', 'Font size'), 16\n        self.widget.set_visual_settings(key, value)\n        key, value = ('Fonts', 'Line label', 'Italic'), True\n        self.widget.set_visual_settings(key, value)\n        font.setPointSize(16)\n        for label in graph.parameter_setter.reg_line_label_items:\n            self.assertFontEqual(label.textItem.font(), font)\n\n        key, value = ('Figure', 'Lines', 'Width'), 10\n        self.widget.set_visual_settings(key, value)\n        for item in graph.reg_line_items:\n            self.assertEqual(item.pen.width(), 10)",
                                "ast_type_str": "ClassDef",
                                "start_lineno": 25,
                                "end_lineno": 1171,
                                "parent": null
                            }
                        }
                    ],
                    "added_entities": [],
                    "deleted_entities": []
                }
            ]
        }
    ],
    "old_commit_hash": "589345fa6bff5b4626acd816a6344081cba53474^",
    "new_commit_hash": "589345fa6bff5b4626acd816a6344081cba53474",
    "commit_message": "Scatter Plot: Replot when input attrs change",
    "commit_date": "2022-02-23T09:37:53+01:00",
    "metadata": {}
}
{
    "repo_name": "orange3",
    "new_commit_hash": "589345fa6bff5b4626acd816a6344081cba53474",
    "test_file_codes": [
        "# Test methods with long descriptive names can omit docstrings\n# pylint: disable=missing-docstring,too-many-public-methods,protected-access\n# pylint: disable=too-many-lines\nfrom unittest.mock import MagicMock, patch, Mock\nimport numpy as np\n\nfrom AnyQt.QtCore import QRectF, Qt\nfrom AnyQt.QtWidgets import QToolTip\nfrom AnyQt.QtGui import QColor, QFont\n\nfrom Orange.data import (\n    Table, Domain, ContinuousVariable, DiscreteVariable, TimeVariable\n)\nfrom Orange.widgets.tests.base import (\n    WidgetTest, WidgetOutputsTestMixin, datasets, ProjectionWidgetTestMixin\n)\nfrom Orange.widgets.tests.utils import simulate\nfrom Orange.widgets.utils.colorpalettes import DefaultRGBColors\nfrom Orange.widgets.visualize.owscatterplot import (\n    OWScatterPlot, ScatterPlotVizRank, OWScatterPlotGraph)\nfrom Orange.widgets.visualize.utils.widget import MAX_COLORS\nfrom Orange.widgets.widget import AttributeList\n\n\nclass TestOWScatterPlot(WidgetTest, ProjectionWidgetTestMixin,\n                        WidgetOutputsTestMixin):\n    @classmethod\n    def setUpClass(cls):\n        super().setUpClass()\n        WidgetOutputsTestMixin.init(cls)\n        cls.same_input_output_domain = False\n\n        cls.signal_name = \"Data\"\n        cls.signal_data = cls.data\n\n    def setUp(self):\n        self.widget = self.create_widget(OWScatterPlot)\n\n    def test_set_data(self):\n        # Connect iris to scatter plot\n        self.send_signal(self.widget.Inputs.data, self.data)\n\n        # First two attribute should be selected as x an y\n        self.assertEqual(self.widget.attr_x, self.data.domain[0])\n        self.assertEqual(self.widget.attr_y, self.data.domain[1])\n\n        # Class var should be selected as color\n        self.assertIs(self.widget.attr_color, self.data.domain.class_var)\n\n        # Change which attributes are displayed\n        self.widget.attr_x = self.data.domain[2]\n        self.widget.attr_y = self.data.domain[3]\n\n        # Disconnect the data\n        self.send_signal(self.widget.Inputs.data, None)\n\n        # removing data should have cleared attributes\n        self.assertIsNone(self.widget.attr_x)\n        self.assertIsNone(self.widget.attr_y)\n        self.assertIsNone(self.widget.attr_color)\n\n        # and remove the legend\n        self.assertEqual(len(self.widget.graph.color_legend.items), 0)\n\n        # Connect iris again\n        # same attributes that were used last time should be selected\n        self.send_signal(self.widget.Inputs.data, self.data)\n\n        self.assertIs(self.widget.attr_x, self.data.domain[2])\n        self.assertIs(self.widget.attr_y, self.data.domain[3])\n\n    def test_score_heuristics(self):\n        domain = Domain([ContinuousVariable(c) for c in \"abcd\"],\n                        DiscreteVariable(\"e\", values=\"ab\"))\n        a = np.arange(10).reshape((10, 1))\n        data = Table(domain, np.hstack([a, a, a, a]), a >= 5)\n        self.send_signal(self.widget.Inputs.data, data)\n        vizrank = ScatterPlotVizRank(self.widget)\n        self.assertEqual([x.name for x in vizrank.score_heuristic()],\n                         list(\"abcd\"))\n\n    def test_optional_combos(self):\n        domain = self.data.domain\n        d1 = Domain(domain.attributes[:2], domain.class_var,\n                    [domain.attributes[2]])\n        t1 = self.data.transform(d1)\n        self.send_signal(self.widget.Inputs.data, t1)\n        self.widget.graph.attr_size = domain.attributes[2]\n\n        d2 = Domain(domain.attributes[:2], domain.class_var,\n                    [domain.attributes[3]])\n        t2 = self.data.transform(d2)\n        self.send_signal(self.widget.Inputs.data, t2)\n\n    def test_error_message(self):\n        \"\"\"Check if error message appears and then disappears when\n        data is removed from input\"\"\"\n        data = self.data.copy()\n        with data.unlocked():\n            data.X[:, 0] = np.nan\n        self.send_signal(self.widget.Inputs.data, data)\n        self.assertTrue(self.widget.Warning.missing_coords.is_shown())\n        self.send_signal(self.widget.Inputs.data, None)\n        self.assertFalse(self.widget.Warning.missing_coords.is_shown())\n\n    def test_report_on_empty(self):\n        self.widget.report_plot = MagicMock()\n        self.widget.report_caption = MagicMock()\n        self.widget.report_items = MagicMock()\n        self.widget.send_report()  # Essentially, don't crash\n        self.widget.report_plot.assert_not_called()\n        self.widget.report_caption.assert_not_called()\n        self.widget.report_items.assert_not_called()\n\n    def test_data_column_nans(self):\n        \"\"\"\n        ValueError cannot convert float NaN to integer.\n        In case when all column values are NaN then it throws that error.\n        GH-2061\n        \"\"\"\n        table = datasets.data_one_column_nans()\n        self.send_signal(self.widget.Inputs.data, table)\n        cb_attr_color = self.widget.controls.attr_color\n        simulate.combobox_activate_item(cb_attr_color, \"b\")\n        simulate.combobox_activate_item(self.widget.cb_attr_x, \"a\")\n        simulate.combobox_activate_item(self.widget.cb_attr_y, \"a\")\n\n        #self.widget.update_graph()\n        self.widget.graph.reset_graph()\n\n    def test_data_column_infs(self):\n        \"\"\"\n        Scatter Plot should not crash on data with infinity values\n        GH-2707\n        GH-2684\n        \"\"\"\n        table = datasets.data_one_column_infs()\n        self.send_signal(self.widget.Inputs.data, table)\n        attr_x = self.widget.controls.attr_x\n        simulate.combobox_activate_item(attr_x, \"b\")\n\n    def test_regression_line(self):\n        \"\"\"It is possible to draw the line only for pair of continuous attrs\"\"\"\n        self.send_signal(self.widget.Inputs.data, self.data)\n        self.assertTrue(self.widget.cb_reg_line.isEnabled())\n        self.assertIsNone(self.widget.graph.reg_line_item)\n        self.widget.cb_reg_line.setChecked(True)\n        self.assertIsNotNone(self.widget.graph.reg_line_item)\n        self.widget.cb_attr_y.activated.emit(4)\n        self.widget.cb_attr_y.setCurrentIndex(4)\n        self.assertFalse(self.widget.cb_reg_line.isEnabled())\n        self.assertIsNone(self.widget.graph.reg_line_item)\n\n    def test_points_combo_boxes(self):\n        \"\"\"Check Point box combo models and values\"\"\"\n        self.send_signal(self.widget.Inputs.data, self.data)\n        controls = self.widget.controls\n\n        # color and label should contain all variables\n        # size should contain only continuous variables\n        # shape should contain only discrete variables\n        for var in self.data.domain.variables + self.data.domain.metas:\n            self.assertIn(var, controls.attr_color.model())\n            self.assertIn(var, controls.attr_label.model())\n            if var.is_continuous:\n                self.assertIn(var, controls.attr_size.model())\n                self.assertNotIn(var, controls.attr_shape.model())\n            if var.is_discrete:\n                self.assertNotIn(var, controls.attr_size.model())\n                self.assertIn(var, controls.attr_shape.model())\n\n        widget = self.create_widget(OWScatterPlot)\n        self.send_signal(self.widget.Inputs.data, self.data, widget=widget)\n        self.assertEqual(controls.attr_color.currentText(),\n                         self.data.domain.class_var.name)\n\n    def test_group_selections(self):\n        self.send_signal(self.widget.Inputs.data, self.data)\n        graph = self.widget.graph\n        points = graph.scatterplot_item.points()\n        sel_column = np.zeros((len(self.data), 1))\n\n        x = self.data.X\n\n        def selectedx():\n            return self.get_output(self.widget.Outputs.selected_data).X\n\n        def selected_groups():\n            return self.get_output(self.widget.Outputs.selected_data).metas[:, 0]\n\n        def annotated():\n            return self.get_output(self.widget.Outputs.annotated_data).metas\n\n        def annotations():\n            return self.get_output(self.widget.Outputs.annotated_data).domain.metas[0].values\n\n        # Select 0:5\n        graph.select(points[:5])\n        np.testing.assert_equal(selectedx(), x[:5])\n        np.testing.assert_equal(selected_groups(), np.zeros(5))\n        sel_column[:5] = 1\n        np.testing.assert_equal(annotated(), sel_column)\n        self.assertEqual(annotations(), (\"No\", \"Yes\", ))\n\n        # Shift-select 5:10; now we have groups 0:5 and 5:10\n        with self.modifiers(Qt.ShiftModifier):\n            graph.select(points[5:10])\n        np.testing.assert_equal(selectedx(), x[:10])\n        np.testing.assert_equal(selected_groups(), np.array([0] * 5 + [1] * 5))\n        sel_column[:5] = 0\n        sel_column[5:10] = 1\n        sel_column[10:] = 2\n        np.testing.assert_equal(annotated(), sel_column)\n        self.assertEqual(len(annotations()), 3)\n\n        # Select: 15:20; we have 15:20\n        graph.select(points[15:20])\n        sel_column = np.zeros((len(self.data), 1))\n        sel_column[15:20] = 1\n        np.testing.assert_equal(selectedx(), x[15:20])\n        np.testing.assert_equal(selected_groups(), np.zeros(5))\n        self.assertEqual(annotations(), (\"No\", \"Yes\"))\n\n        # Alt-select (remove) 10:17; we have 17:20\n        with self.modifiers(Qt.AltModifier):\n            graph.select(points[10:17])\n        np.testing.assert_equal(selectedx(), x[17:20])\n        np.testing.assert_equal(selected_groups(), np.zeros(3))\n        sel_column[15:17] = 0\n        np.testing.assert_equal(annotated(), sel_column)\n        self.assertEqual(annotations(), (\"No\", \"Yes\"))\n\n        # Ctrl-Shift-select (add-to-last) 10:17; we have 17:25\n        with self.modifiers(Qt.ShiftModifier | Qt.ControlModifier):\n            graph.select(points[20:25])\n        np.testing.assert_equal(selectedx(), x[17:25])\n        np.testing.assert_equal(selected_groups(), np.zeros(8))\n        sel_column[20:25] = 1\n        np.testing.assert_equal(annotated(), sel_column)\n        self.assertEqual(annotations(), (\"No\", \"Yes\"))\n\n        # Shift-select (add) 30:35; we have 17:25, 30:35\n        with self.modifiers(Qt.ShiftModifier):\n            graph.select(points[30:35])\n        # ... then Ctrl-Shift-select (add-to-last) 10:17; we have 17:25, 30:40\n        with self.modifiers(Qt.ShiftModifier | Qt.ControlModifier):\n            graph.select(points[35:40])\n        sel_column[:] = 2\n        sel_column[17:25] = 0\n        sel_column[30:40] = 1\n        np.testing.assert_equal(selected_groups(), np.array([0] * 8 + [1] * 10))\n        np.testing.assert_equal(annotated(), sel_column)\n        self.assertEqual(len(annotations()), 3)\n\n    def test_saving_selection(self):\n        self.send_signal(self.widget.Inputs.data, self.data)  # iris\n        self.widget.graph.select_by_rectangle(QRectF(4, 3, 3, 1))\n        selected_inds = np.flatnonzero(self.widget.graph.selection)\n        settings = self.widget.settingsHandler.pack_data(self.widget)\n        np.testing.assert_equal(selected_inds,\n                                [i for i, g in settings[\"selection\"]])\n\n    def test_points_selection(self):\n        # Opening widget with saved selection should restore it\n        self.widget = self.create_widget(\n            OWScatterPlot, stored_settings={\n                \"selection_group\": [(i, 1) for i in range(50)]}\n        )\n        self.send_signal(self.widget.Inputs.data, self.data)  # iris\n        selected_data = self.get_output(self.widget.Outputs.selected_data)\n        self.assertEqual(len(selected_data), 50)\n\n        # Changing the dataset should clear selection\n        titanic = Table(\"titanic\")\n        self.send_signal(self.widget.Inputs.data, titanic)\n        selected_data = self.get_output(self.widget.Outputs.selected_data)\n        self.assertIsNone(selected_data)\n\n    def test_migrate_selection(self):\n        settings = dict(selection=list(range(2)))\n        OWScatterPlot.migrate_settings(settings, 0)\n        self.assertEqual(settings[\"selection_group\"], [(0, 1), (1, 1)])\n\n    def test_invalid_points_selection(self):\n        # if selection contains rows that are not present in the current\n        # dataset, widget should select what can be selected.\n        self.widget = self.create_widget(\n            OWScatterPlot, stored_settings={\n                \"selection_group\": [(i, 1) for i in range(50)]}\n        )\n        data = self.data[:11].copy()\n        with data.unlocked():\n            data[0, 0] = np.nan\n        self.send_signal(self.widget.Inputs.data, data)\n        self.assertIsNone(self.get_output(self.widget.Outputs.selected_data))\n\n    def test_set_strings_settings(self):\n        \"\"\"\n        Test if settings can be loaded as strings and successfully put\n        in new owplotgui combos.\n        \"\"\"\n        self.send_signal(self.widget.Inputs.data, self.data)\n        settings = self.widget.settingsHandler.pack_data(self.widget)\n        plot_settings = settings[\"context_settings\"][0].values\n        plot_settings[\"attr_label\"] = (\"sepal length\", -2)\n        plot_settings[\"attr_color\"] = (\"sepal width\", -2)\n        plot_settings[\"attr_shape\"] = (\"iris\", -2)\n        plot_settings[\"attr_size\"] = (\"petal width\", -2)\n        w = self.create_widget(OWScatterPlot, stored_settings=settings)\n        self.send_signal(self.widget.Inputs.data, self.data, widget=w)\n        self.assertEqual(w.attr_label.name, \"sepal length\")\n        self.assertEqual(w.attr_color.name, \"sepal width\")\n        self.assertEqual(w.attr_shape.name, \"iris\")\n        self.assertEqual(w.attr_size.name, \"petal width\")\n\n    def test_features_and_no_data(self):\n        \"\"\"\n        Prevent crashing when features are sent but no data.\n        \"\"\"\n        domain = Table(\"iris\").domain\n        self.send_signal(self.widget.Inputs.features,\n                         AttributeList(domain.variables))\n        self.send_signal(self.widget.Inputs.features, None)\n\n    def test_features_and_data(self):\n        self.assertTrue(self.widget.attr_box.isEnabled())\n        self.send_signal(self.widget.Inputs.data, self.data)\n        x, y = self.widget.graph.scatterplot_item.getData()\n        np.testing.assert_array_equal(x, self.data.X[:, 0])\n        np.testing.assert_array_equal(y, self.data.X[:, 1])\n        self.send_signal(self.widget.Inputs.features,\n                         AttributeList(self.data.domain[2:]))\n        self.assertIs(self.widget.attr_x, self.data.domain[2])\n        self.assertIs(self.widget.attr_y, self.data.domain[3])\n        self.assertFalse(self.widget.attr_box.isEnabled())\n        self.assertFalse(self.widget.vizrank.isEnabled())\n        x, y = self.widget.graph.scatterplot_item.getData()\n        np.testing.assert_array_equal(x, self.data.X[:, 2])\n        np.testing.assert_array_equal(y, self.data.X[:, 3])\n\n        self.send_signal(self.widget.Inputs.data, None)\n        self.send_signal(self.widget.Inputs.data, self.data)\n        self.assertIs(self.widget.attr_x, self.data.domain[2])\n        self.assertIs(self.widget.attr_y, self.data.domain[3])\n        self.assertFalse(self.widget.attr_box.isEnabled())\n        self.assertFalse(self.widget.vizrank.isEnabled())\n\n        self.send_signal(self.widget.Inputs.features, None)\n        self.assertTrue(self.widget.attr_box.isEnabled())\n        self.assertTrue(self.widget.vizrank.isEnabled())\n\n    def test_output_features(self):\n        data = Table(\"iris\")\n        self.send_signal(self.widget.Inputs.data, data)\n\n        # This doesn't work because combo's callbacks are connected to signal\n        # `activated`, which is only triggered by user interaction, and not to\n        # `currentIndexChanged`\n        # combo_y = self.widget.controls.attr_y\n        # combo_y.setCurrentIndex(combo_y.model().indexOf(data.domain[3]))\n        # This is a workaround\n        self.widget.attr_y = data.domain[3]\n        self.widget.attr_changed()\n\n        features = self.get_output(self.widget.Outputs.features)\n        self.assertEqual(features, [data.domain[0], data.domain[3]])\n\n    def test_vizrank(self):\n        data = Table(\"iris\")\n        self.send_signal(self.widget.Inputs.data, data)\n        vizrank = ScatterPlotVizRank(self.widget)\n        n_states = len(data.domain.attributes)\n        n_states = n_states * (n_states - 1) / 2\n        states = [state for state in vizrank.iterate_states(None)]\n        self.assertEqual(len(states), n_states)\n        self.assertEqual(len(set(states)), n_states)\n        self.assertIsNotNone(vizrank.compute_score(states[0]))\n        self.send_signal(self.widget.Inputs.data, data[:9])\n        self.assertIsNone(vizrank.compute_score(states[0]))\n\n        data = Table(\"housing\")[::10]\n        self.send_signal(self.widget.Inputs.data, data)\n        vizrank = ScatterPlotVizRank(self.widget)\n        states = [state for state in vizrank.iterate_states(None)]\n        self.assertIsNotNone(vizrank.compute_score(states[0]))\n\n    def test_vizrank_class_nan(self):\n        \"\"\"\n        When class values are nan, vizrank should be disabled. It should behave like\n        the class column is missing.\n        GH-2757\n        \"\"\"\n        def assert_vizrank_enabled(data, is_enabled):\n            self.send_signal(self.widget.Inputs.data, data)\n            self.assertEqual(is_enabled, self.widget.vizrank_button.isEnabled())\n\n        data1 = Table(\"iris\")[::30]\n        data2 = Table(\"iris\")[::30].copy()\n        with data2.unlocked():\n            data2.Y[:] = np.nan\n        domain = Domain(\n            attributes=data2.domain.attributes[:4], class_vars=DiscreteVariable(\"iris\", values=()))\n        data2 = Table(domain, data2.X, Y=data2.Y)\n        data3 = Table(\"iris\")[::30].copy()\n        with data3.unlocked():\n            data3.Y[:] = np.nan\n\n        for data, is_enabled in zip([data1, data2, data1, data3, data1],\n                                    [True, False, True, False, True]):\n            assert_vizrank_enabled(data, is_enabled)\n\n    def test_vizrank_nonprimitives(self):\n        \"\"\"VizRank does not try to include non primitive attributes\"\"\"\n        data = Table(\"zoo\")\n        self.send_signal(self.widget.Inputs.data, data)\n        with patch(\"Orange.widgets.visualize.owscatterplot.ReliefF\",\n                   new=lambda *_1, **_2: lambda data: np.arange(len(data))):\n            self.widget.vizrank.score_heuristic()\n\n    def test_vizrank_enabled(self):\n        self.send_signal(self.widget.Inputs.data, self.data)\n        self.assertTrue(self.widget.vizrank_button.isEnabled())\n        self.assertEqual(self.widget.vizrank_button.toolTip(), \"\")\n        self.assertTrue(self.widget.vizrank.button.isEnabled())\n        self.widget.vizrank.button.click()\n\n    def test_vizrank_enabled_no_data(self):\n        self.send_signal(self.widget.Inputs.data, None)\n        self.assertFalse(self.widget.vizrank_button.isEnabled())\n        self.assertEqual(self.widget.vizrank_button.toolTip(), \"No data on input\")\n\n    def test_vizrank_enabled_sparse_data(self):\n        self.send_signal(self.widget.Inputs.data, self.data.to_sparse())\n        self.assertFalse(self.widget.vizrank_button.isEnabled())\n        self.assertEqual(self.widget.vizrank_button.toolTip(), \"Data is sparse\")\n\n    def test_vizrank_enabled_constant_data(self):\n        domain = Domain([ContinuousVariable(\"c1\"),\n                         ContinuousVariable(\"c2\"),\n                         ContinuousVariable(\"c3\"),\n                         ContinuousVariable(\"c4\")],\n                        DiscreteVariable(\"cls\", values=(\"a\", \"b\")))\n        X = np.zeros((10, 4))\n        table = Table(domain, X, np.random.randint(2, size=10))\n        self.send_signal(self.widget.Inputs.data, table)\n        self.assertEqual(self.widget.vizrank_button.toolTip(), \"\")\n        self.assertTrue(self.widget.vizrank_button.isEnabled())\n        self.assertTrue(self.widget.vizrank.button.isEnabled())\n        self.widget.vizrank.button.click()\n\n    def test_vizrank_enabled_two_features(self):\n        self.send_signal(self.widget.Inputs.data, self.data[:, :2])\n        self.assertFalse(self.widget.vizrank_button.isEnabled())\n        self.assertEqual(self.widget.vizrank_button.toolTip(),\n                         \"Not enough features for ranking\")\n\n    def test_vizrank_enabled_no_color_var(self):\n        self.send_signal(self.widget.Inputs.data, self.data[:, :3])\n        self.assertFalse(self.widget.vizrank_button.isEnabled())\n        self.assertEqual(self.widget.vizrank_button.toolTip(),\n                         \"Color variable is not selected\")\n\n    def test_vizrank_enabled_color_var_nans(self):\n        domain = Domain([ContinuousVariable(\"c1\"),\n                         ContinuousVariable(\"c2\"),\n                         ContinuousVariable(\"c3\"),\n                         ContinuousVariable(\"c4\")],\n                        DiscreteVariable(\"cls\", values=(\"a\", \"b\")))\n        table = Table(domain, np.random.random((10, 4)), np.full(10, np.nan))\n        self.send_signal(self.widget.Inputs.data, table)\n        self.assertFalse(self.widget.vizrank_button.isEnabled())\n        self.assertEqual(self.widget.vizrank_button.toolTip(),\n                         \"Color variable has no values\")\n\n    def test_auto_send_selection(self):\n        \"\"\"\n        Scatter Plot automatically sends selection only when the checkbox Send automatically\n        is checked.\n        GH-2649\n        GH-2646\n        \"\"\"\n        data = Table(\"iris\")\n        self.send_signal(self.widget.Inputs.data, data)\n        self.widget.controls.auto_commit.setChecked(False)\n        self.assertFalse(self.widget.controls.auto_commit.isChecked())\n        self._select_data()\n        self.assertIsNone(self.get_output(self.widget.Outputs.selected_data))\n        self.widget.controls.auto_commit.setChecked(True)\n        output = self.get_output(self.widget.Outputs.selected_data)\n        self.assertIsInstance(output, Table)\n\n    def test_color_is_optional(self):\n        zoo = Table(\"zoo\")\n        backbone, breathes, airborne, type = \\\n            [zoo.domain[x] for x in [\"backbone\", \"breathes\", \"airborne\", \"type\"]]\n        default_x, default_y, default_color = \\\n            zoo.domain[0], zoo.domain[1], zoo.domain.class_var\n        attr_x = self.widget.controls.attr_x\n        attr_y = self.widget.controls.attr_y\n        attr_color = self.widget.controls.attr_color\n\n        # Send dataset, ensure defaults are what we expect them to be\n        self.send_signal(self.widget.Inputs.data, zoo)\n        self.assertEqual(attr_x.currentText(), default_x.name)\n        self.assertEqual(attr_y.currentText(), default_y.name)\n        self.assertEqual(attr_color.currentText(), default_color.name)\n        # Select different values\n        simulate.combobox_activate_item(attr_x, backbone.name)\n        simulate.combobox_activate_item(attr_y, breathes.name)\n        simulate.combobox_activate_item(attr_color, airborne.name)\n\n        # Send compatible dataset, values should not change\n        zoo2 = zoo[:, (backbone, breathes, airborne, type)]\n        self.send_signal(self.widget.Inputs.data, zoo2)\n        self.assertEqual(attr_x.currentText(), backbone.name)\n        self.assertEqual(attr_y.currentText(), breathes.name)\n        self.assertEqual(attr_color.currentText(), airborne.name)\n\n        # Send dataset without color variable\n        # x and y should remain, color reset to default\n        zoo3 = zoo[:, (backbone, breathes, type)]\n        self.send_signal(self.widget.Inputs.data, zoo3)\n        self.assertEqual(attr_x.currentText(), backbone.name)\n        self.assertEqual(attr_y.currentText(), breathes.name)\n        self.assertEqual(attr_color.currentText(), default_color.name)\n\n        # Send dataset without x\n        # y and color should be the same as with zoo\n        zoo4 = zoo[:, (default_x, default_y, breathes, airborne, type)]\n        self.send_signal(self.widget.Inputs.data, zoo4)\n        self.assertEqual(attr_x.currentText(), default_x.name)\n        self.assertEqual(attr_y.currentText(), default_y.name)\n        self.assertEqual(attr_color.currentText(), default_color.name)\n\n        # Send dataset compatible with zoo2 and zoo3\n        # Color should reset to one in zoo3, as it was used more\n        # recently\n        zoo5 = zoo[:, (default_x, backbone, breathes, airborne, type)]\n        self.send_signal(self.widget.Inputs.data, zoo5)\n        self.assertEqual(attr_x.currentText(), backbone.name)\n        self.assertEqual(attr_y.currentText(), breathes.name)\n        self.assertEqual(attr_color.currentText(), type.name)\n\n    def test_handle_metas(self):\n        \"\"\"\n        Scatter Plot Graph can handle metas\n        GH-2699\n        \"\"\"\n        w = self.widget\n        data = Table(\"iris\")\n        domain = Domain(\n            attributes=data.domain.attributes[:2],\n            class_vars=data.domain.class_vars,\n            metas=data.domain.attributes[2:]\n        )\n        data = data.transform(domain).copy()\n        # Sometimes floats in metas are saved as objects\n        with data.unlocked():\n            data.metas = data.metas.astype(object)\n        self.send_signal(w.Inputs.data, data)\n        simulate.combobox_activate_item(w.cb_attr_x, data.domain.metas[1].name)\n        simulate.combobox_activate_item(w.controls.attr_color, data.domain.metas[0].name)\n        w.graph.reset_graph()\n\n    def test_subset_data(self):\n        \"\"\"\n        Scatter Plot subset data is sent to Scatter Plot Graph\n        GH-2773\n        \"\"\"\n        data = Table(\"iris\")\n        w = self.widget\n        self.send_signal(w.Inputs.data, data)\n        self.send_signal(w.Inputs.data_subset, data[::30])\n        self.assertEqual(len(w.subset_indices), 5)\n\n    def test_opacity_warning(self):\n        data = Table(\"iris\")\n        w = self.widget\n        self.send_signal(w.Inputs.data, data)\n        w.graph.controls.alpha_value.setSliderPosition(10)\n        self.assertFalse(w.Warning.transparent_subset.is_shown())\n        self.send_signal(w.Inputs.data_subset, data[::30])\n        self.assertTrue(w.Warning.transparent_subset.is_shown())\n        w.graph.controls.alpha_value.setSliderPosition(200)\n        self.assertFalse(w.Warning.transparent_subset.is_shown())\n        w.graph.controls.alpha_value.setSliderPosition(10)\n        self.assertTrue(w.Warning.transparent_subset.is_shown())\n        self.send_signal(w.Inputs.data_subset, None)\n        self.assertFalse(w.Warning.transparent_subset.is_shown())\n\n    def test_jittering(self):\n        self.send_signal(self.widget.Inputs.data, self.data)\n        self.widget.graph.controls.jitter_continuous.setChecked(True)\n        self.widget.graph.controls.jitter_size.setValue(1)\n\n    def test_metas_zero_column(self):\n        \"\"\"\n        Prevent crash when metas column is zero.\n        GH-2775\n        \"\"\"\n        data = Table(\"iris\")\n        domain = data.domain\n        domain = Domain(domain.attributes[:3], domain.class_vars, domain.attributes[3:])\n        data = data.transform(domain).copy()\n        with data.unlocked():\n            data.metas[:, 0] = 0\n        w = self.widget\n        self.send_signal(w.Inputs.data, data)\n        simulate.combobox_activate_item(w.controls.attr_x, domain.metas[0].name)\n\n    def test_tooltip(self):\n        # The test tests presence of some data,\n        # but avoids checking the exact format\n        data = Table(\"heart_disease\")\n        self.send_signal(self.widget.Inputs.data, data)\n        widget = self.widget\n        graph = widget.graph\n        scatterplot_item = graph.scatterplot_item\n\n        widget.controls.attr_x = data.domain[\"chest pain\"]\n        widget.controls.attr_y = data.domain[\"cholesterol\"]\n        all_points = scatterplot_item.points()\n\n        event = MagicMock()\n        with patch.object(scatterplot_item, \"mapFromScene\"), \\\n                patch.object(QToolTip, \"showText\") as show_text:\n\n            # Single point hovered\n            with patch.object(scatterplot_item, \"pointsAt\",\n                              return_value=[all_points[42]]):\n                # Show just x and y attribute\n                widget.tooltip_shows_all = False\n                self.assertTrue(graph.help_event(event))\n                (_, text), _ = show_text.call_args\n                self.assertIn(\"age = {}\".format(data[42, \"age\"]), text)\n                self.assertIn(\"gender = {}\".format(data[42, \"gender\"]), text)\n                self.assertNotIn(\"max HR = {}\".format(data[42, \"max HR\"]), text)\n                self.assertNotIn(\"others\", text)\n\n                # Show all attributes\n                widget.tooltip_shows_all = True\n                self.assertTrue(graph.help_event(event))\n                (_, text), _ = show_text.call_args\n                self.assertIn(\"age = {}\".format(data[42, \"age\"]), text)\n                self.assertIn(\"gender = {}\".format(data[42, \"gender\"]), text)\n                self.assertIn(\"max HR = {}\".format(data[42, \"max HR\"]), text)\n                self.assertIn(\"... and 4 others\", text)\n\n            # Two points hovered\n            with patch.object(scatterplot_item, \"pointsAt\",\n                              return_value=[all_points[42], all_points[100]]):\n                self.assertTrue(graph.help_event(event))\n                (_, text), _ = show_text.call_args\n                self.assertIn(\"age = {}\".format(data[42, \"age\"]), text)\n                self.assertIn(\"gender = {}\".format(data[42, \"gender\"]), text)\n                self.assertIn(\"age = {}\".format(data[100, \"age\"]), text)\n                self.assertIn(\"gender = {}\".format(data[100, \"gender\"]), text)\n\n            # No points hovered\n            with patch.object(scatterplot_item, \"pointsAt\",\n                              return_value=[]):\n                show_text.reset_mock()\n                self.assertFalse(graph.help_event(event))\n                self.assertEqual(show_text.call_count, 0)\n\n    def test_many_discrete_values(self):\n        \"\"\"\n        Do not show all discrete values if there are too many.\n        Also test for values with a nan.\n        GH-2804\n        \"\"\"\n        def prepare_data():\n            data = Table(\"iris\")\n            values = list(range(15))\n            class_var = DiscreteVariable(\"iris5\", values=[str(v) for v in values])\n            data = data.transform(\n                Domain(attributes=data.domain.attributes,\n                       class_vars=[class_var])).copy()\n            with data.unlocked():\n                data.Y = np.array(values * 10, dtype=float)\n            return data\n\n        def assert_equal(data, max):\n            self.send_signal(self.widget.Inputs.data, data)\n            pen_data, brush_data = self.widget.graph.get_colors()\n            self.assertEqual(max, len(np.unique([id(p) for p in pen_data])), )\n\n        assert_equal(prepare_data(), MAX_COLORS)\n        # data with nan value\n        data = prepare_data()\n        with data.unlocked():\n            data.Y[42] = np.nan\n        assert_equal(data, MAX_COLORS + 1)\n\n    def test_invalidated_same_features(self):\n        self.widget.setup_plot = Mock()\n        # send data and set default features\n        self.send_signal(self.widget.Inputs.data, self.data)\n        self.widget.setup_plot.assert_called_once()\n        self.assertListEqual(self.widget.effective_variables,\n                             list(self.data.domain.attributes[:2]))\n\n        # send the same features as already set\n        self.widget.setup_plot.reset_mock()\n        self.send_signal(self.widget.Inputs.features,\n                         AttributeList(self.data.domain.attributes[:2]))\n        self.widget.setup_plot.assert_not_called()\n        self.assertListEqual(self.widget.effective_variables,\n                             list(self.data.domain.attributes[:2]))\n\n    def test_invalidated_same_time(self):\n        self.widget.setup_plot = Mock()\n        # send data and features at the same time (data first)\n        features = self.data.domain.attributes[:2]\n        signals = [(self.widget.Inputs.data, self.data),\n                   (self.widget.Inputs.features, AttributeList(features))]\n        self.send_signals(signals)\n        self.widget.setup_plot.assert_called_once()\n        self.assertListEqual(self.widget.effective_variables, list(features))\n\n    def test_invalidated_features_first(self):\n        self.widget.setup_plot = Mock()\n        # send features (same as default ones)\n        self.send_signal(self.widget.Inputs.features,\n                         AttributeList(self.data.domain.attributes[:2]))\n        self.assertListEqual(self.widget.effective_variables, [])\n        self.widget.setup_plot.assert_called_once()\n\n        # send data\n        self.widget.setup_plot.reset_mock()\n        self.send_signal(self.widget.Inputs.data, self.data)\n        self.widget.setup_plot.assert_called()\n        self.assertListEqual(self.widget.effective_variables,\n                             list(self.data.domain.attributes[:2]))\n\n    def test_invalidated_same_time_features_first(self):\n        self.widget.setup_plot = Mock()\n        # send features and data at the same time (features first)\n        features = self.data.domain.attributes[:2]\n        signals = [(self.widget.Inputs.features, AttributeList(features)),\n                   (self.widget.Inputs.data, self.data)]\n        self.send_signals(signals)\n        self.widget.setup_plot.assert_called_once()\n        self.assertListEqual(self.widget.effective_variables, list(features))\n\n        self.widget.setup_plot.reset_mock()\n        features = self.data.domain.attributes[2:]\n        signals = [(self.widget.Inputs.features, AttributeList(features)),\n                   (self.widget.Inputs.data, self.data)]\n        self.send_signals(signals)\n        self.widget.setup_plot.assert_called_once()\n\n    def test_invalidated_diff_features(self):\n        self.widget.setup_plot = Mock()\n        # send data and set default features\n        self.send_signal(self.widget.Inputs.data, self.data)\n        self.widget.setup_plot.assert_called_once()\n        self.assertListEqual(self.widget.effective_variables,\n                             list(self.data.domain.attributes[:2]))\n\n        # send different features\n        self.widget.setup_plot.reset_mock()\n        self.send_signal(self.widget.Inputs.features,\n                         AttributeList(self.data.domain.attributes[2:4]))\n        self.widget.setup_plot.assert_called_once()\n        self.assertListEqual(self.widget.effective_variables,\n                             list(self.data.domain.attributes[2:4]))\n\n    def test_invalidated_diff_features_same_time(self):\n        self.widget.setup_plot = Mock()\n        # send data and different features at the same time (data first)\n        features = self.data.domain.attributes[2:4]\n        signals = [(self.widget.Inputs.data, self.data),\n                   (self.widget.Inputs.features, AttributeList(features))]\n        self.send_signals(signals)\n        self.widget.setup_plot.assert_called_once()\n        self.assertListEqual(self.widget.effective_variables, list(features))\n\n    def test_invalidated_diff_features_features_first(self):\n        self.widget.setup_plot = Mock()\n        # send features (not the same as defaults)\n        self.send_signal(self.widget.Inputs.features,\n                         AttributeList(self.data.domain.attributes[2:4]))\n        self.assertListEqual(self.widget.effective_variables, [])\n        self.widget.setup_plot.assert_called_once()\n\n        # send data\n        self.widget.setup_plot.reset_mock()\n        self.send_signal(self.widget.Inputs.data, self.data)\n        self.widget.setup_plot.assert_called_once()\n        self.assertListEqual(self.widget.effective_variables,\n                             list(self.data.domain.attributes[2:4]))\n\n    def test_invalidated_diff_features_same_time_features_first(self):\n        self.widget.setup_plot = Mock()\n        # send data and different features at the same time (features first)\n        features = self.data.domain.attributes[2:4]\n        signals = [(self.widget.Inputs.features, AttributeList(features)),\n                   (self.widget.Inputs.data, self.data)]\n        self.send_signals(signals)\n        self.widget.setup_plot.assert_called_once()\n        self.assertListEqual(self.widget.effective_variables, list(features))\n\n    @patch('Orange.widgets.visualize.owscatterplot.ScatterPlotVizRank.'\n           'on_manual_change')\n    def test_vizrank_receives_manual_change(self, on_manual_change):\n        # Recreate the widget so the patch kicks in\n        self.widget = self.create_widget(OWScatterPlot)\n        data = Table(\"iris.tab\")\n        self.send_signal(self.widget.Inputs.data, data)\n        model = self.widget.controls.attr_x.model()\n        self.widget.attr_x = model[0]\n        self.widget.attr_y = model[1]\n        simulate.combobox_activate_index(self.widget.controls.attr_x, 2)\n        self.assertIs(self.widget.attr_x, model[2])\n        on_manual_change.assert_called_with(model[2], model[1])\n\n    def test_on_manual_change(self):\n        data = Table(\"iris.tab\")\n        self.send_signal(self.widget.Inputs.data, data)\n        vizrank = self.widget.vizrank\n        vizrank.toggle()\n        self.process_events(until=lambda: not vizrank.keep_running)\n\n        model = vizrank.rank_model\n        attrs = model.data(model.index(3, 0), vizrank._AttrRole)\n        vizrank.on_manual_change(*attrs)\n        selection = vizrank.rank_table.selectedIndexes()\n        self.assertEqual(len(selection), 1)\n        self.assertEqual(selection[0].row(), 3)\n\n        vizrank.on_manual_change(*attrs[::-1])\n        selection = vizrank.rank_table.selectedIndexes()\n        self.assertEqual(len(selection), 0)\n\n    def test_regression_lines_appear(self):\n        self.widget.graph.controls.show_reg_line.setChecked(True)\n        self.assertEqual(len(self.widget.graph.reg_line_items), 0)\n        self.send_signal(self.widget.Inputs.data, self.data)\n        self.assertEqual(len(self.widget.graph.reg_line_items), 4)\n        simulate.combobox_activate_index(self.widget.controls.attr_color, 0)\n        self.assertEqual(len(self.widget.graph.reg_line_items), 1)\n        data = self.data.copy()\n        with data.unlocked():\n            data[:, 0] = np.nan\n        self.send_signal(self.widget.Inputs.data, data)\n        self.assertEqual(len(self.widget.graph.reg_line_items), 0)\n\n    def test_regression_line_coeffs(self):\n        widget = self.widget\n        graph = widget.graph\n        xy = np.array([[0, 0], [1, 0], [1, 2], [2, 2],\n                       [0, 1], [1, 3], [2, 5]], dtype=float)\n        colors = np.array([0, 0, 0, 0, 1, 1, 1], dtype=float)\n        widget.get_coordinates_data = lambda: xy.T\n        widget.can_draw_regresssion_line = lambda: True\n        widget.get_color_data = lambda: colors\n        widget.is_continuous_color = lambda: False\n        graph.palette = DefaultRGBColors\n        graph.controls.show_reg_line.setChecked(True)\n\n        graph.update_regression_line()\n\n        line1 = graph.reg_line_items[1]\n        self.assertEqual(line1.pos().x(), 0)\n        self.assertEqual(line1.pos().y(), 0)\n        self.assertEqual(line1.angle, 45)\n        self.assertEqual(line1.pen.color().hue(), graph.palette[0].hue())\n\n        line2 = graph.reg_line_items[2]\n        self.assertEqual(line2.pos().x(), 0)\n        self.assertEqual(line2.pos().y(), 1)\n        self.assertAlmostEqual(line2.angle, np.degrees(np.arctan2(2, 1)))\n        self.assertEqual(line2.pen.color().hue(), graph.palette[1].hue())\n\n        graph.orthonormal_regression = True\n        graph.update_regression_line()\n\n        line1 = graph.reg_line_items[1]\n        self.assertEqual(line1.pos().x(), 0)\n        self.assertAlmostEqual(line1.pos().y(), -0.6180339887498949)\n        self.assertAlmostEqual(line1.angle, 58.28252558853899)\n        self.assertEqual(line1.pen.color().hue(), graph.palette[0].hue())\n\n        line2 = graph.reg_line_items[2]\n        self.assertEqual(line2.pos().x(), 0)\n        self.assertEqual(line2.pos().y(), 1)\n        self.assertAlmostEqual(line2.angle, np.degrees(np.arctan2(2, 1)))\n        self.assertEqual(line2.pen.color().hue(), graph.palette[1].hue())\n\n    def test_orthonormal_line(self):\n        color = QColor(1, 2, 3)\n        width = 42\n        # Normal line\n        line = OWScatterPlotGraph._orthonormal_line(\n            np.array([0, 1, 1, 2]), np.array([0, 0, 2, 2]), color, width)\n        self.assertEqual(line.pos().x(), 0)\n        self.assertAlmostEqual(line.pos().y(), -0.6180339887498949)\n        self.assertAlmostEqual(line.angle, 58.28252558853899)\n        self.assertEqual(line.pen.color(), color)\n        self.assertEqual(line.pen.width(), width)\n\n        # Normal line, negative slope\n        line = OWScatterPlotGraph._orthonormal_line(\n            np.array([1, 2, 3]), np.array([3, 2, 1]), color, width)\n        self.assertEqual(line.pos().x(), 1)\n        self.assertEqual(line.pos().y(), 3)\n        self.assertEqual(line.angle % 360, 315)\n\n        # Horizontal line\n        line = OWScatterPlotGraph._orthonormal_line(\n            np.array([10, 11, 12]), np.array([42, 42, 42]), color, width)\n        self.assertEqual(line.pos().x(), 10)\n        self.assertEqual(line.pos().y(), 42)\n        self.assertEqual(line.angle, 0)\n\n        # Vertical line\n        line = OWScatterPlotGraph._orthonormal_line(\n            np.array([42, 42, 42]), np.array([10, 11, 12]), color, width)\n        self.assertEqual(line.pos().x(), 42)\n        self.assertEqual(line.pos().y(), 10)\n        self.assertEqual(line.angle, 90)\n\n        # No line because all points coincide\n        line = OWScatterPlotGraph._orthonormal_line(\n            np.array([1, 1, 1]), np.array([42, 42, 42]), color, width)\n        self.assertIsNone(line)\n\n        # No line because the group is symmetric\n        line = OWScatterPlotGraph._orthonormal_line(\n            np.array([1, 1, 2, 2]), np.array([42, 5, 5, 42]), color, width)\n        self.assertIsNone(line)\n\n    def test_regression_line(self):\n        color = QColor(1, 2, 3)\n        width = 42\n        # Normal line\n        line = OWScatterPlotGraph._regression_line(\n            np.array([0, 1, 1, 2]), np.array([0, 0, 2, 2]), color, width)\n        self.assertEqual(line.pos().x(), 0)\n        self.assertAlmostEqual(line.pos().y(), 0)\n        self.assertEqual(line.angle, 45)\n        self.assertEqual(line.pen.color(), color)\n        self.assertEqual(line.pen.width(), width)\n\n        # Normal line, negative slope\n        line = OWScatterPlotGraph._regression_line(\n            np.array([1, 2, 3]), np.array([3, 2, 1]), color, width)\n        self.assertEqual(line.pos().x(), 1)\n        self.assertEqual(line.pos().y(), 3)\n        self.assertEqual(line.angle % 360, 315)\n\n        # Horizontal line\n        line = OWScatterPlotGraph._regression_line(\n            np.array([10, 11, 12]), np.array([42, 42, 42]), color, width)\n        self.assertEqual(line.pos().x(), 10)\n        self.assertEqual(line.pos().y(), 42)\n        self.assertEqual(line.angle, 0)\n\n        # Vertical line\n        line = OWScatterPlotGraph._regression_line(\n            np.array([42, 42, 42]), np.array([10, 11, 12]), color, width)\n        self.assertIsNone(line)\n\n        # No line because all points coincide\n        line = OWScatterPlotGraph._regression_line(\n            np.array([1, 1, 1]), np.array([42, 42, 42]), color, width)\n        self.assertIsNone(line)\n\n    def test_add_line_calls_proper_regressor(self):\n        graph = self.widget.graph\n        graph._orthonormal_line = Mock(return_value=None)\n        graph._regression_line = Mock(return_value=None)\n        x, y, c = Mock(), Mock(), Mock()\n\n        graph.orthonormal_regression = True\n        graph._add_line(x, y, c)\n        graph._orthonormal_line.assert_called_once_with(x, y, c, 3, 1)\n        graph._regression_line.assert_not_called()\n        graph._orthonormal_line.reset_mock()\n\n        graph.orthonormal_regression = False\n        graph._add_line(x, y, c)\n        graph._regression_line.assert_called_with(x, y, c, 3, 1)\n        graph._orthonormal_line.assert_not_called()\n\n    def test_no_regression_line(self):\n        graph = self.widget.graph\n        graph._orthonormal_line = lambda *_: None\n        graph.orthonormal_regression = True\n\n        graph.plot_widget.addItem = Mock()\n\n        x, y, c = Mock(), Mock(), Mock()\n        graph._add_line(x, y, c)\n        graph.plot_widget.addItem.assert_not_called()\n        self.assertEqual(graph.reg_line_items, [])\n\n    def test_update_regression_line_calls_add_line(self):\n        widget = self.widget\n        graph = widget.graph\n        x, y = np.array([[0, 0], [1, 0], [1, 2], [2, 2],\n                         [0, 1], [1, 3], [2, 5]], dtype=float).T\n        colors = np.array([0, 0, 0, 0, 1, 1, 1], dtype=float)\n        widget.get_coordinates_data = lambda: (x, y)\n        widget.can_draw_regresssion_line = lambda: True\n        widget.get_color_data = lambda: colors\n        widget.is_continuous_color = lambda: False\n        graph.palette = DefaultRGBColors\n        graph.controls.show_reg_line.setChecked(True)\n\n        graph._add_line = Mock()\n\n        graph.update_regression_line()\n        (args1, _), (args2, _), (args3, _) = graph._add_line.call_args_list\n        np.testing.assert_equal(args1[0], x)\n        np.testing.assert_equal(args1[1], y)\n        self.assertEqual(args1[2], QColor(\"#505050\"))\n\n        np.testing.assert_equal(args2[0], x[:4])\n        np.testing.assert_equal(args2[1], y[:4])\n        self.assertEqual(args2[2].hue(), graph.palette[0].hue())\n\n        np.testing.assert_equal(args3[0], x[4:])\n        np.testing.assert_equal(args3[1], y[4:])\n        self.assertEqual(args3[2].hue(), graph.palette[1].hue())\n        graph._add_line.reset_mock()\n\n        # Continuous color - just a single line\n        widget.is_continuous_color = lambda: True\n        graph.update_regression_line()\n        graph._add_line.assert_called_once()\n        args1, _ = graph._add_line.call_args_list[0]\n        np.testing.assert_equal(args1[0], x)\n        np.testing.assert_equal(args1[1], y)\n        self.assertEqual(args1[2].hue(), QColor(\"#505050\").hue())\n        graph._add_line.reset_mock()\n        widget.is_continuous_color = lambda: False\n\n        # No palette - just a single line\n        graph.palette = None\n        graph.update_regression_line()\n        graph._add_line.assert_called_once()\n        graph._add_line.reset_mock()\n        graph.palette = DefaultRGBColors\n\n        # Regression line is disabled\n        graph.show_reg_line = False\n        graph.update_regression_line()\n        graph._add_line.assert_not_called()\n        graph.show_reg_line = True\n\n        # No colors - just one line\n        widget.get_color_data = lambda: None\n        graph.update_regression_line()\n        graph._add_line.assert_called_once()\n        graph._add_line.reset_mock()\n\n        # No data\n        widget.get_coordinates_data = lambda: (None, None)\n        graph.update_regression_line()\n        graph._add_line.assert_not_called()\n        graph.show_reg_line = True\n        widget.get_coordinates_data = lambda: (x, y)\n\n        # One color group contains just one point - skip that line\n        widget.get_color_data = lambda: np.array([0] + [1] * (len(x) - 1))\n\n        graph.update_regression_line()\n        (args1, _), (args2, _) = graph._add_line.call_args_list\n        np.testing.assert_equal(args1[0], x)\n        np.testing.assert_equal(args1[1], y)\n        self.assertEqual(args1[2].hue(), QColor(\"#505050\").hue())\n\n        np.testing.assert_equal(args2[0], x[1:])\n        np.testing.assert_equal(args2[1], y[1:])\n        self.assertEqual(args2[2].hue(), graph.palette[1].hue())\n\n    def test_update_regression_line_is_called(self):\n        widget = self.widget\n        graph = widget.graph\n        urline = graph.update_regression_line = Mock()\n\n        self.send_signal(widget.Inputs.data, self.data)\n        urline.assert_called_once()\n        urline.reset_mock()\n\n        self.send_signal(widget.Inputs.data, None)\n        urline.assert_called_once()\n        urline.reset_mock()\n\n        self.send_signal(widget.Inputs.data, self.data)\n        urline.assert_called_once()\n        urline.reset_mock()\n\n        simulate.combobox_activate_index(self.widget.controls.attr_color, 0)\n        urline.assert_called_once()\n        urline.reset_mock()\n\n        simulate.combobox_activate_index(self.widget.controls.attr_color, 2)\n        urline.assert_called_once()\n        urline.reset_mock()\n\n        simulate.combobox_activate_index(self.widget.controls.attr_x, 3)\n        urline.assert_called_once()\n        urline.reset_mock()\n\n    def test_time_axis(self):\n        a = np.array([[1581953776, 1], [1581963776, 2], [1582953776, 3]])\n        d1 = Domain([ContinuousVariable(\"time\"), ContinuousVariable(\"value\")])\n        data = Table.from_numpy(d1, a)\n        d2 = Domain([TimeVariable(\"time\"), ContinuousVariable(\"value\")])\n        data_time = Table.from_numpy(d2, a)\n\n        x_axis = self.widget.graph.plot_widget.plotItem.getAxis(\"bottom\")\n\n        self.send_signal(self.widget.Inputs.data, data)\n        self.assertFalse(x_axis._use_time)\n        _ticks = x_axis.tickValues(1581953776, 1582953776, 1000)\n        ticks = x_axis.tickStrings(_ticks[0][1], 1, _ticks[0][0])\n        try:\n            float(ticks[0])\n        except ValueError:\n            self.fail(\"axis should display floats\")\n\n        self.send_signal(self.widget.Inputs.data, data_time)\n        self.assertTrue(x_axis._use_time)\n        _ticks = x_axis.tickValues(1581953776, 1582953776, 1000)\n        ticks = x_axis.tickStrings(_ticks[0][1], 1, _ticks[0][0])\n        with self.assertRaises(ValueError):\n            float(ticks[0])\n\n    def test_visual_settings(self):\n        super().test_visual_settings()\n\n        graph = self.widget.graph\n        font = QFont()\n        font.setItalic(True)\n        font.setFamily(\"Helvetica\")\n\n        key, value = ('Fonts', 'Axis title', 'Font size'), 16\n        self.widget.set_visual_settings(key, value)\n        key, value = ('Fonts', 'Axis title', 'Italic'), True\n        self.widget.set_visual_settings(key, value)\n        font.setPointSize(16)\n        for item in graph.parameter_setter.axis_items:\n            self.assertFontEqual(item.label.font(), font)\n\n        key, value = ('Fonts', 'Axis ticks', 'Font size'), 15\n        self.widget.set_visual_settings(key, value)\n        key, value = ('Fonts', 'Axis ticks', 'Italic'), True\n        self.widget.set_visual_settings(key, value)\n        font.setPointSize(15)\n        for item in graph.parameter_setter.axis_items:\n            self.assertFontEqual(item.style[\"tickFont\"], font)\n\n        self.widget.graph.controls.show_reg_line.setChecked(True)\n        self.assertGreater(len(graph.parameter_setter.reg_line_label_items), 0)\n\n        key, value = ('Fonts', 'Line label', 'Font size'), 16\n        self.widget.set_visual_settings(key, value)\n        key, value = ('Fonts', 'Line label', 'Italic'), True\n        self.widget.set_visual_settings(key, value)\n        font.setPointSize(16)\n        for label in graph.parameter_setter.reg_line_label_items:\n            self.assertFontEqual(label.textItem.font(), font)\n\n        key, value = ('Figure', 'Lines', 'Width'), 10\n        self.widget.set_visual_settings(key, value)\n        for item in graph.reg_line_items:\n            self.assertEqual(item.pen.width(), 10)\n\n\nif __name__ == \"__main__\":\n    import unittest\n    unittest.main()\n"
    ],
    "test_file_names": [
        "test_1.py"
    ],
    "setup_res_code": 0,
    "setup_res_stdout": "Starting Orange installation attempts...\nAttempting installation with Python 3.7...\nrunning build_ext\nCompiling Orange/classification/_tree_scorers.pyx because it changed.\nCompiling Orange/data/_contingency.pyx because it changed.\nCompiling Orange/data/_io.pyx because it changed.\nCompiling Orange/data/_valuecount.pyx because it changed.\nCompiling Orange/data/_variable.pyx because it changed.\nCompiling Orange/distance/_distance.pyx because it changed.\nCompiling Orange/preprocess/_discretize.pyx because it changed.\nCompiling Orange/preprocess/_relieff.pyx because it changed.\nCompiling Orange/projection/_som.pyx because it changed.\n[1/9] Cythonizing Orange/classification/_tree_scorers.pyx\n[2/9] Cythonizing Orange/data/_contingency.pyx\n[3/9] Cythonizing Orange/data/_io.pyx\n[4/9] Cythonizing Orange/data/_valuecount.pyx\n[5/9] Cythonizing Orange/data/_variable.pyx\n[6/9] Cythonizing Orange/distance/_distance.pyx\n[7/9] Cythonizing Orange/preprocess/_discretize.pyx\n[8/9] Cythonizing Orange/preprocess/_relieff.pyx\n[9/9] Cythonizing Orange/projection/_som.pyx\nbuilding 'Orange.classification._tree_scorers' extension\ncreating build\ncreating build/temp.linux-x86_64-3.7\ncreating build/temp.linux-x86_64-3.7/Orange\ncreating build/temp.linux-x86_64-3.7/Orange/classification\ncc -pthread -Wno-unused-result -Wsign-compare -Wunreachable-code -DNDEBUG -g -fwrapv -O3 -Wall -fPIC -I/tools/deps/include -I/tools/deps/include/ncursesw -fPIC -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_589345fa6bff5b4626acd816a6344081cba53474/.venv/lib/python3.7/site-packages/numpy/core/include -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_589345fa6bff5b4626acd816a6344081cba53474/.venv/include -I/home/gcpuser/.local/share/uv/python/cpython-3.7.9-linux-x86_64-gnu/include/python3.7m -c Orange/classification/_tree_scorers.c -o build/temp.linux-x86_64-3.7/Orange/classification/_tree_scorers.o\ncreating build/lib.linux-x86_64-3.7\ncreating build/lib.linux-x86_64-3.7/Orange\ncreating build/lib.linux-x86_64-3.7/Orange/classification\ncc -pthread -shared -L/tools/deps/lib build/temp.linux-x86_64-3.7/Orange/classification/_tree_scorers.o -L/home/gcpuser/.local/share/uv/python/cpython-3.7.9-linux-x86_64-gnu/lib -lm -lpython3.7m -o build/lib.linux-x86_64-3.7/Orange/classification/_tree_scorers.cpython-37m-x86_64-linux-gnu.so\nbuilding 'Orange.data._contingency' extension\ncreating build/temp.linux-x86_64-3.7/Orange/data\ncc -pthread -Wno-unused-result -Wsign-compare -Wunreachable-code -DNDEBUG -g -fwrapv -O3 -Wall -fPIC -I/tools/deps/include -I/tools/deps/include/ncursesw -fPIC -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_589345fa6bff5b4626acd816a6344081cba53474/.venv/lib/python3.7/site-packages/numpy/core/include -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_589345fa6bff5b4626acd816a6344081cba53474/.venv/include -I/home/gcpuser/.local/share/uv/python/cpython-3.7.9-linux-x86_64-gnu/include/python3.7m -c Orange/data/_contingency.c -o build/temp.linux-x86_64-3.7/Orange/data/_contingency.o\ncreating build/lib.linux-x86_64-3.7/Orange/data\ncc -pthread -shared -L/tools/deps/lib build/temp.linux-x86_64-3.7/Orange/data/_contingency.o -L/home/gcpuser/.local/share/uv/python/cpython-3.7.9-linux-x86_64-gnu/lib -lm -lpython3.7m -o build/lib.linux-x86_64-3.7/Orange/data/_contingency.cpython-37m-x86_64-linux-gnu.so\nbuilding 'Orange.data._io' extension\ncc -pthread -Wno-unused-result -Wsign-compare -Wunreachable-code -DNDEBUG -g -fwrapv -O3 -Wall -fPIC -I/tools/deps/include -I/tools/deps/include/ncursesw -fPIC -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_589345fa6bff5b4626acd816a6344081cba53474/.venv/lib/python3.7/site-packages/numpy/core/include -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_589345fa6bff5b4626acd816a6344081cba53474/.venv/include -I/home/gcpuser/.local/share/uv/python/cpython-3.7.9-linux-x86_64-gnu/include/python3.7m -c Orange/data/_io.c -o build/temp.linux-x86_64-3.7/Orange/data/_io.o\ncc -pthread -shared -L/tools/deps/lib build/temp.linux-x86_64-3.7/Orange/data/_io.o -L/home/gcpuser/.local/share/uv/python/cpython-3.7.9-linux-x86_64-gnu/lib -lm -lpython3.7m -o build/lib.linux-x86_64-3.7/Orange/data/_io.cpython-37m-x86_64-linux-gnu.so\nbuilding 'Orange.data._valuecount' extension\ncc -pthread -Wno-unused-result -Wsign-compare -Wunreachable-code -DNDEBUG -g -fwrapv -O3 -Wall -fPIC -I/tools/deps/include -I/tools/deps/include/ncursesw -fPIC -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_589345fa6bff5b4626acd816a6344081cba53474/.venv/lib/python3.7/site-packages/numpy/core/include -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_589345fa6bff5b4626acd816a6344081cba53474/.venv/include -I/home/gcpuser/.local/share/uv/python/cpython-3.7.9-linux-x86_64-gnu/include/python3.7m -c Orange/data/_valuecount.c -o build/temp.linux-x86_64-3.7/Orange/data/_valuecount.o\ncc -pthread -shared -L/tools/deps/lib build/temp.linux-x86_64-3.7/Orange/data/_valuecount.o -L/home/gcpuser/.local/share/uv/python/cpython-3.7.9-linux-x86_64-gnu/lib -lm -lpython3.7m -o build/lib.linux-x86_64-3.7/Orange/data/_valuecount.cpython-37m-x86_64-linux-gnu.so\nbuilding 'Orange.data._variable' extension\ncc -pthread -Wno-unused-result -Wsign-compare -Wunreachable-code -DNDEBUG -g -fwrapv -O3 -Wall -fPIC -I/tools/deps/include -I/tools/deps/include/ncursesw -fPIC -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_589345fa6bff5b4626acd816a6344081cba53474/.venv/lib/python3.7/site-packages/numpy/core/include -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_589345fa6bff5b4626acd816a6344081cba53474/.venv/include -I/home/gcpuser/.local/share/uv/python/cpython-3.7.9-linux-x86_64-gnu/include/python3.7m -c Orange/data/_variable.c -o build/temp.linux-x86_64-3.7/Orange/data/_variable.o\ncc -pthread -shared -L/tools/deps/lib build/temp.linux-x86_64-3.7/Orange/data/_variable.o -L/home/gcpuser/.local/share/uv/python/cpython-3.7.9-linux-x86_64-gnu/lib -lm -lpython3.7m -o build/lib.linux-x86_64-3.7/Orange/data/_variable.cpython-37m-x86_64-linux-gnu.so\nbuilding 'Orange.distance._distance' extension\ncreating build/temp.linux-x86_64-3.7/Orange/distance\ncc -pthread -Wno-unused-result -Wsign-compare -Wunreachable-code -DNDEBUG -g -fwrapv -O3 -Wall -fPIC -I/tools/deps/include -I/tools/deps/include/ncursesw -fPIC -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_589345fa6bff5b4626acd816a6344081cba53474/.venv/lib/python3.7/site-packages/numpy/core/include -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_589345fa6bff5b4626acd816a6344081cba53474/.venv/include -I/home/gcpuser/.local/share/uv/python/cpython-3.7.9-linux-x86_64-gnu/include/python3.7m -c Orange/distance/_distance.c -o build/temp.linux-x86_64-3.7/Orange/distance/_distance.o\ncreating build/lib.linux-x86_64-3.7/Orange/distance\ncc -pthread -shared -L/tools/deps/lib build/temp.linux-x86_64-3.7/Orange/distance/_distance.o -L/home/gcpuser/.local/share/uv/python/cpython-3.7.9-linux-x86_64-gnu/lib -lm -lpython3.7m -o build/lib.linux-x86_64-3.7/Orange/distance/_distance.cpython-37m-x86_64-linux-gnu.so\nbuilding 'Orange.preprocess._discretize' extension\ncreating build/temp.linux-x86_64-3.7/Orange/preprocess\ncc -pthread -Wno-unused-result -Wsign-compare -Wunreachable-code -DNDEBUG -g -fwrapv -O3 -Wall -fPIC -I/tools/deps/include -I/tools/deps/include/ncursesw -fPIC -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_589345fa6bff5b4626acd816a6344081cba53474/.venv/lib/python3.7/site-packages/numpy/core/include -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_589345fa6bff5b4626acd816a6344081cba53474/.venv/include -I/home/gcpuser/.local/share/uv/python/cpython-3.7.9-linux-x86_64-gnu/include/python3.7m -c Orange/preprocess/_discretize.c -o build/temp.linux-x86_64-3.7/Orange/preprocess/_discretize.o\ncreating build/lib.linux-x86_64-3.7/Orange/preprocess\ncc -pthread -shared -L/tools/deps/lib build/temp.linux-x86_64-3.7/Orange/preprocess/_discretize.o -L/home/gcpuser/.local/share/uv/python/cpython-3.7.9-linux-x86_64-gnu/lib -lm -lpython3.7m -o build/lib.linux-x86_64-3.7/Orange/preprocess/_discretize.cpython-37m-x86_64-linux-gnu.so\nbuilding 'Orange.preprocess._relieff' extension\ncc -pthread -Wno-unused-result -Wsign-compare -Wunreachable-code -DNDEBUG -g -fwrapv -O3 -Wall -fPIC -I/tools/deps/include -I/tools/deps/include/ncursesw -fPIC -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_589345fa6bff5b4626acd816a6344081cba53474/.venv/lib/python3.7/site-packages/numpy/core/include -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_589345fa6bff5b4626acd816a6344081cba53474/.venv/include -I/home/gcpuser/.local/share/uv/python/cpython-3.7.9-linux-x86_64-gnu/include/python3.7m -c Orange/preprocess/_relieff.cpp -o build/temp.linux-x86_64-3.7/Orange/preprocess/_relieff.o\nc++ -pthread -shared -L/tools/deps/lib build/temp.linux-x86_64-3.7/Orange/preprocess/_relieff.o -L/home/gcpuser/.local/share/uv/python/cpython-3.7.9-linux-x86_64-gnu/lib -lm -lpython3.7m -o build/lib.linux-x86_64-3.7/Orange/preprocess/_relieff.cpython-37m-x86_64-linux-gnu.so\nbuilding 'Orange.projection._som' extension\ncreating build/temp.linux-x86_64-3.7/Orange/projection\ncc -pthread -Wno-unused-result -Wsign-compare -Wunreachable-code -DNDEBUG -g -fwrapv -O3 -Wall -fPIC -I/tools/deps/include -I/tools/deps/include/ncursesw -fPIC -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_589345fa6bff5b4626acd816a6344081cba53474/.venv/lib/python3.7/site-packages/numpy/core/include -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_589345fa6bff5b4626acd816a6344081cba53474/.venv/include -I/home/gcpuser/.local/share/uv/python/cpython-3.7.9-linux-x86_64-gnu/include/python3.7m -c Orange/projection/_som.c -o build/temp.linux-x86_64-3.7/Orange/projection/_som.o\ncreating build/lib.linux-x86_64-3.7/Orange/projection\ncc -pthread -shared -L/tools/deps/lib build/temp.linux-x86_64-3.7/Orange/projection/_som.o -L/home/gcpuser/.local/share/uv/python/cpython-3.7.9-linux-x86_64-gnu/lib -lm -lpython3.7m -o build/lib.linux-x86_64-3.7/Orange/projection/_som.cpython-37m-x86_64-linux-gnu.so\nbuilding 'Orange.classification._simple_tree' extension\ncc -pthread -Wno-unused-result -Wsign-compare -Wunreachable-code -DNDEBUG -g -fwrapv -O3 -Wall -fPIC -I/tools/deps/include -I/tools/deps/include/ncursesw -fPIC -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_589345fa6bff5b4626acd816a6344081cba53474/.venv/lib/python3.7/site-packages/numpy/core/include -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_589345fa6bff5b4626acd816a6344081cba53474/.venv/include -I/home/gcpuser/.local/share/uv/python/cpython-3.7.9-linux-x86_64-gnu/include/python3.7m -c Orange/classification/_simple_tree.c -o build/temp.linux-x86_64-3.7/Orange/classification/_simple_tree.o\ncc -pthread -shared -L/tools/deps/lib build/temp.linux-x86_64-3.7/Orange/classification/_simple_tree.o -L/home/gcpuser/.local/share/uv/python/cpython-3.7.9-linux-x86_64-gnu/lib -lm -lpython3.7m -o build/lib.linux-x86_64-3.7/Orange/classification/_simple_tree.cpython-37m-x86_64-linux-gnu.so\nbuilding 'Orange.widgets.utils._grid_density' extension\ncreating build/temp.linux-x86_64-3.7/Orange/widgets\ncreating build/temp.linux-x86_64-3.7/Orange/widgets/utils\ncc -pthread -Wno-unused-result -Wsign-compare -Wunreachable-code -DNDEBUG -g -fwrapv -O3 -Wall -fPIC -I/tools/deps/include -I/tools/deps/include/ncursesw -fPIC -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_589345fa6bff5b4626acd816a6344081cba53474/.venv/lib/python3.7/site-packages/numpy/core/include -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_589345fa6bff5b4626acd816a6344081cba53474/.venv/include -I/home/gcpuser/.local/share/uv/python/cpython-3.7.9-linux-x86_64-gnu/include/python3.7m -c Orange/widgets/utils/_grid_density.cpp -o build/temp.linux-x86_64-3.7/Orange/widgets/utils/_grid_density.o\ncreating build/lib.linux-x86_64-3.7/Orange/widgets\ncreating build/lib.linux-x86_64-3.7/Orange/widgets/utils\nc++ -pthread -shared -L/tools/deps/lib build/temp.linux-x86_64-3.7/Orange/widgets/utils/_grid_density.o -L/home/gcpuser/.local/share/uv/python/cpython-3.7.9-linux-x86_64-gnu/lib -lm -lpython3.7m -o build/lib.linux-x86_64-3.7/Orange/widgets/utils/_grid_density.cpython-37m-x86_64-linux-gnu.so\ncopying build/lib.linux-x86_64-3.7/Orange/classification/_tree_scorers.cpython-37m-x86_64-linux-gnu.so -> Orange/classification\ncopying build/lib.linux-x86_64-3.7/Orange/data/_contingency.cpython-37m-x86_64-linux-gnu.so -> Orange/data\ncopying build/lib.linux-x86_64-3.7/Orange/data/_io.cpython-37m-x86_64-linux-gnu.so -> Orange/data\ncopying build/lib.linux-x86_64-3.7/Orange/data/_valuecount.cpython-37m-x86_64-linux-gnu.so -> Orange/data\ncopying build/lib.linux-x86_64-3.7/Orange/data/_variable.cpython-37m-x86_64-linux-gnu.so -> Orange/data\ncopying build/lib.linux-x86_64-3.7/Orange/distance/_distance.cpython-37m-x86_64-linux-gnu.so -> Orange/distance\ncopying build/lib.linux-x86_64-3.7/Orange/preprocess/_discretize.cpython-37m-x86_64-linux-gnu.so -> Orange/preprocess\ncopying build/lib.linux-x86_64-3.7/Orange/preprocess/_relieff.cpython-37m-x86_64-linux-gnu.so -> Orange/preprocess\ncopying build/lib.linux-x86_64-3.7/Orange/projection/_som.cpython-37m-x86_64-linux-gnu.so -> Orange/projection\ncopying build/lib.linux-x86_64-3.7/Orange/classification/_simple_tree.cpython-37m-x86_64-linux-gnu.so -> Orange/classification\ncopying build/lib.linux-x86_64-3.7/Orange/widgets/utils/_grid_density.cpython-37m-x86_64-linux-gnu.so -> Orange/widgets/utils\nrunning develop\nrunning egg_info\ncreating Orange3.egg-info\nwriting Orange3.egg-info/PKG-INFO\nwriting dependency_links to Orange3.egg-info/dependency_links.txt\nwriting entry points to Orange3.egg-info/entry_points.txt\nwriting requirements to Orange3.egg-info/requires.txt\nwriting top-level names to Orange3.egg-info/top_level.txt\nwriting manifest file 'Orange3.egg-info/SOURCES.txt'\nreading manifest file 'Orange3.egg-info/SOURCES.txt'\nreading manifest template 'MANIFEST.in'\nadding license file 'LICENSE'\nwriting manifest file 'Orange3.egg-info/SOURCES.txt'\nrunning build_ext\ncopying build/lib.linux-x86_64-3.7/Orange/classification/_tree_scorers.cpython-37m-x86_64-linux-gnu.so -> Orange/classification\ncopying build/lib.linux-x86_64-3.7/Orange/data/_contingency.cpython-37m-x86_64-linux-gnu.so -> Orange/data\ncopying build/lib.linux-x86_64-3.7/Orange/data/_io.cpython-37m-x86_64-linux-gnu.so -> Orange/data\ncopying build/lib.linux-x86_64-3.7/Orange/data/_valuecount.cpython-37m-x86_64-linux-gnu.so -> Orange/data\ncopying build/lib.linux-x86_64-3.7/Orange/data/_variable.cpython-37m-x86_64-linux-gnu.so -> Orange/data\ncopying build/lib.linux-x86_64-3.7/Orange/distance/_distance.cpython-37m-x86_64-linux-gnu.so -> Orange/distance\ncopying build/lib.linux-x86_64-3.7/Orange/preprocess/_discretize.cpython-37m-x86_64-linux-gnu.so -> Orange/preprocess\ncopying build/lib.linux-x86_64-3.7/Orange/preprocess/_relieff.cpython-37m-x86_64-linux-gnu.so -> Orange/preprocess\ncopying build/lib.linux-x86_64-3.7/Orange/projection/_som.cpython-37m-x86_64-linux-gnu.so -> Orange/projection\ncopying build/lib.linux-x86_64-3.7/Orange/classification/_simple_tree.cpython-37m-x86_64-linux-gnu.so -> Orange/classification\ncopying build/lib.linux-x86_64-3.7/Orange/widgets/utils/_grid_density.cpython-37m-x86_64-linux-gnu.so -> Orange/widgets/utils\nCreating /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_589345fa6bff5b4626acd816a6344081cba53474/.venv/lib/python3.7/site-packages/Orange3.egg-link (link to .)\nAdding Orange3 3.32.0.dev0+589345f to easy-install.pth file\nInstalling orange-canvas script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_589345fa6bff5b4626acd816a6344081cba53474/.venv/bin\n\nInstalled /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_589345fa6bff5b4626acd816a6344081cba53474\nProcessing dependencies for Orange3==3.32.0.dev0+589345f\nSearching for XlsxWriter==3.2.0\nBest match: XlsxWriter 3.2.0\nAdding XlsxWriter 3.2.0 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_589345fa6bff5b4626acd816a6344081cba53474/.venv/lib/python3.7/site-packages\nSearching for xlrd==2.0.1\nBest match: xlrd 2.0.1\nAdding xlrd 2.0.1 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_589345fa6bff5b4626acd816a6344081cba53474/.venv/lib/python3.7/site-packages\nSearching for setuptools==59.8.0\nBest match: setuptools 59.8.0\nAdding setuptools 59.8.0 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_589345fa6bff5b4626acd816a6344081cba53474/.venv/lib/python3.7/site-packages\nSearching for serverfiles==0.3.1\nBest match: serverfiles 0.3.1\nAdding serverfiles 0.3.1 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_589345fa6bff5b4626acd816a6344081cba53474/.venv/lib/python3.7/site-packages\nSearching for scipy==1.7.3\nBest match: scipy 1.7.3\nAdding scipy 1.7.3 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_589345fa6bff5b4626acd816a6344081cba53474/.venv/lib/python3.7/site-packages\nSearching for scikit-learn==1.0.2\nBest match: scikit-learn 1.0.2\nAdding scikit-learn 1.0.2 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_589345fa6bff5b4626acd816a6344081cba53474/.venv/lib/python3.7/site-packages\nSearching for requests==2.31.0\nBest match: requests 2.31.0\nAdding requests 2.31.0 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_589345fa6bff5b4626acd816a6344081cba53474/.venv/lib/python3.7/site-packages\nSearching for qtconsole==5.4.4\nBest match: qtconsole 5.4.4\nAdding qtconsole 5.4.4 to easy-install.pth file\nInstalling jupyter-qtconsole script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_589345fa6bff5b4626acd816a6344081cba53474/.venv/bin\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_589345fa6bff5b4626acd816a6344081cba53474/.venv/lib/python3.7/site-packages\nSearching for PyYAML==6.0.1\nBest match: PyYAML 6.0.1\nAdding PyYAML 6.0.1 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_589345fa6bff5b4626acd816a6344081cba53474/.venv/lib/python3.7/site-packages\nSearching for python-louvain==0.16\nBest match: python-louvain 0.16\nAdding python-louvain 0.16 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_589345fa6bff5b4626acd816a6344081cba53474/.venv/lib/python3.7/site-packages\nSearching for pyqtgraph==0.12.4\nBest match: pyqtgraph 0.12.4\nAdding pyqtgraph 0.12.4 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_589345fa6bff5b4626acd816a6344081cba53474/.venv/lib/python3.7/site-packages\nSearching for pygments==2.17.2\nBest match: pygments 2.17.2\nAdding pygments 2.17.2 to easy-install.pth file\nInstalling pygmentize script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_589345fa6bff5b4626acd816a6344081cba53474/.venv/bin\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_589345fa6bff5b4626acd816a6344081cba53474/.venv/lib/python3.7/site-packages\nSearching for pip==24.0\nBest match: pip 24.0\nAdding pip 24.0 to easy-install.pth file\nInstalling pip script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_589345fa6bff5b4626acd816a6344081cba53474/.venv/bin\nInstalling pip3 script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_589345fa6bff5b4626acd816a6344081cba53474/.venv/bin\nInstalling pip3.10 script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_589345fa6bff5b4626acd816a6344081cba53474/.venv/bin\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_589345fa6bff5b4626acd816a6344081cba53474/.venv/lib/python3.7/site-packages\nSearching for pandas==1.3.5\nBest match: pandas 1.3.5\nAdding pandas 1.3.5 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_589345fa6bff5b4626acd816a6344081cba53474/.venv/lib/python3.7/site-packages\nSearching for orange-widget-base==4.22.0\nBest match: orange-widget-base 4.22.0\nAdding orange-widget-base 4.22.0 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_589345fa6bff5b4626acd816a6344081cba53474/.venv/lib/python3.7/site-packages\nSearching for orange-canvas-core==0.1.35\nBest match: orange-canvas-core 0.1.35\nAdding orange-canvas-core 0.1.35 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_589345fa6bff5b4626acd816a6344081cba53474/.venv/lib/python3.7/site-packages\nSearching for openpyxl==3.1.3\nBest match: openpyxl 3.1.3\nAdding openpyxl 3.1.3 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_589345fa6bff5b4626acd816a6344081cba53474/.venv/lib/python3.7/site-packages\nSearching for openTSNE==1.0.1\nBest match: openTSNE 1.0.1\nAdding openTSNE 1.0.1 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_589345fa6bff5b4626acd816a6344081cba53474/.venv/lib/python3.7/site-packages\nSearching for numpy==1.17.5\nBest match: numpy 1.17.5\nAdding numpy 1.17.5 to easy-install.pth file\nInstalling f2py script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_589345fa6bff5b4626acd816a6344081cba53474/.venv/bin\nInstalling f2py3 script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_589345fa6bff5b4626acd816a6344081cba53474/.venv/bin\nInstalling f2py3.7 script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_589345fa6bff5b4626acd816a6344081cba53474/.venv/bin\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_589345fa6bff5b4626acd816a6344081cba53474/.venv/lib/python3.7/site-packages\nSearching for networkx==2.6.3\nBest match: networkx 2.6.3\nAdding networkx 2.6.3 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_589345fa6bff5b4626acd816a6344081cba53474/.venv/lib/python3.7/site-packages\nSearching for matplotlib==3.5.3\nBest match: matplotlib 3.5.3\nAdding matplotlib 3.5.3 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_589345fa6bff5b4626acd816a6344081cba53474/.venv/lib/python3.7/site-packages\nSearching for keyrings.alt==4.2.0\nBest match: keyrings.alt 4.2.0\nAdding keyrings.alt 4.2.0 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_589345fa6bff5b4626acd816a6344081cba53474/.venv/lib/python3.7/site-packages\nSearching for keyring==24.1.1\nBest match: keyring 24.1.1\nAdding keyring 24.1.1 to easy-install.pth file\nInstalling keyring script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_589345fa6bff5b4626acd816a6344081cba53474/.venv/bin\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_589345fa6bff5b4626acd816a6344081cba53474/.venv/lib/python3.7/site-packages\nSearching for joblib==1.3.2\nBest match: joblib 1.3.2\nAdding joblib 1.3.2 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_589345fa6bff5b4626acd816a6344081cba53474/.venv/lib/python3.7/site-packages\nSearching for httpx==0.19.0\nBest match: httpx 0.19.0\nAdding httpx 0.19.0 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_589345fa6bff5b4626acd816a6344081cba53474/.venv/lib/python3.7/site-packages\nSearching for chardet==5.2.0\nBest match: chardet 5.2.0\nAdding chardet 5.2.0 to easy-install.pth file\nInstalling chardetect script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_589345fa6bff5b4626acd816a6344081cba53474/.venv/bin\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_589345fa6bff5b4626acd816a6344081cba53474/.venv/lib/python3.7/site-packages\nSearching for Bottleneck==1.4.0\nBest match: Bottleneck 1.4.0\nAdding Bottleneck 1.4.0 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_589345fa6bff5b4626acd816a6344081cba53474/.venv/lib/python3.7/site-packages\nSearching for baycomp==1.0.3\nBest match: baycomp 1.0.3\nAdding baycomp 1.0.3 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_589345fa6bff5b4626acd816a6344081cba53474/.venv/lib/python3.7/site-packages\nSearching for PyQtWebEngine==5.15.6\nBest match: PyQtWebEngine 5.15.6\nAdding PyQtWebEngine 5.15.6 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_589345fa6bff5b4626acd816a6344081cba53474/.venv/lib/python3.7/site-packages\nSearching for PyQt5==5.15.10\nBest match: PyQt5 5.15.10\nAdding PyQt5 5.15.10 to easy-install.pth file\nInstalling pylupdate5 script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_589345fa6bff5b4626acd816a6344081cba53474/.venv/bin\nInstalling pyrcc5 script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_589345fa6bff5b4626acd816a6344081cba53474/.venv/bin\nInstalling pyuic5 script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_589345fa6bff5b4626acd816a6344081cba53474/.venv/bin\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_589345fa6bff5b4626acd816a6344081cba53474/.venv/lib/python3.7/site-packages\nSearching for AnyQt==0.2.0\nBest match: AnyQt 0.2.0\nAdding AnyQt 0.2.0 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_589345fa6bff5b4626acd816a6344081cba53474/.venv/lib/python3.7/site-packages\nSearching for threadpoolctl==3.1.0\nBest match: threadpoolctl 3.1.0\nAdding threadpoolctl 3.1.0 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_589345fa6bff5b4626acd816a6344081cba53474/.venv/lib/python3.7/site-packages\nSearching for charset-normalizer==3.4.1\nBest match: charset-normalizer 3.4.1\nAdding charset-normalizer 3.4.1 to easy-install.pth file\nInstalling normalizer script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_589345fa6bff5b4626acd816a6344081cba53474/.venv/bin\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_589345fa6bff5b4626acd816a6344081cba53474/.venv/lib/python3.7/site-packages\nSearching for certifi==2024.12.14\nBest match: certifi 2024.12.14\nAdding certifi 2024.12.14 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_589345fa6bff5b4626acd816a6344081cba53474/.venv/lib/python3.7/site-packages\nSearching for urllib3==2.0.7\nBest match: urllib3 2.0.7\nAdding urllib3 2.0.7 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_589345fa6bff5b4626acd816a6344081cba53474/.venv/lib/python3.7/site-packages\nSearching for idna==3.10\nBest match: idna 3.10\nAdding idna 3.10 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_589345fa6bff5b4626acd816a6344081cba53474/.venv/lib/python3.7/site-packages\nSearching for pyzmq==26.2.0\nBest match: pyzmq 26.2.0\nAdding pyzmq 26.2.0 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_589345fa6bff5b4626acd816a6344081cba53474/.venv/lib/python3.7/site-packages\nSearching for jupyter-client==7.4.9\nBest match: jupyter-client 7.4.9\nAdding jupyter-client 7.4.9 to easy-install.pth file\nInstalling jupyter-kernel script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_589345fa6bff5b4626acd816a6344081cba53474/.venv/bin\nInstalling jupyter-kernelspec script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_589345fa6bff5b4626acd816a6344081cba53474/.venv/bin\nInstalling jupyter-run script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_589345fa6bff5b4626acd816a6344081cba53474/.venv/bin\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_589345fa6bff5b4626acd816a6344081cba53474/.venv/lib/python3.7/site-packages\nSearching for ipykernel==6.16.2\nBest match: ipykernel 6.16.2\nAdding ipykernel 6.16.2 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_589345fa6bff5b4626acd816a6344081cba53474/.venv/lib/python3.7/site-packages\nSearching for ipython-genutils==0.2.0\nBest match: ipython-genutils 0.2.0\nAdding ipython-genutils 0.2.0 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_589345fa6bff5b4626acd816a6344081cba53474/.venv/lib/python3.7/site-packages\nSearching for jupyter-core==4.12.0\nBest match: jupyter-core 4.12.0\nAdding jupyter-core 4.12.0 to easy-install.pth file\nInstalling jupyter script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_589345fa6bff5b4626acd816a6344081cba53474/.venv/bin\nInstalling jupyter-migrate script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_589345fa6bff5b4626acd816a6344081cba53474/.venv/bin\nInstalling jupyter-troubleshoot script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_589345fa6bff5b4626acd816a6344081cba53474/.venv/bin\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_589345fa6bff5b4626acd816a6344081cba53474/.venv/lib/python3.7/site-packages\nSearching for QtPy==2.4.2\nBest match: QtPy 2.4.2\nAdding QtPy 2.4.2 to easy-install.pth file\nInstalling qtpy script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_589345fa6bff5b4626acd816a6344081cba53474/.venv/bin\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_589345fa6bff5b4626acd816a6344081cba53474/.venv/lib/python3.7/site-packages\nSearching for packaging==24.0\nBest match: packaging 24.0\nAdding packaging 24.0 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_589345fa6bff5b4626acd816a6344081cba53474/.venv/lib/python3.7/site-packages\nSearching for traitlets==5.9.0\nBest match: traitlets 5.9.0\nAdding traitlets 5.9.0 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_589345fa6bff5b4626acd816a6344081cba53474/.venv/lib/python3.7/site-packages\nSearching for pytz==2024.2\nBest match: pytz 2024.2\nAdding pytz 2024.2 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_589345fa6bff5b4626acd816a6344081cba53474/.venv/lib/python3.7/site-packages\nSearching for python-dateutil==2.9.0.post0\nBest match: python-dateutil 2.9.0.post0\nAdding python-dateutil 2.9.0.post0 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_589345fa6bff5b4626acd816a6344081cba53474/.venv/lib/python3.7/site-packages\nSearching for typing-extensions==4.7.1\nBest match: typing-extensions 4.7.1\nAdding typing-extensions 4.7.1 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_589345fa6bff5b4626acd816a6344081cba53474/.venv/lib/python3.7/site-packages\nSearching for dictdiffer==0.9.0\nBest match: dictdiffer 0.9.0\nAdding dictdiffer 0.9.0 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_589345fa6bff5b4626acd816a6344081cba53474/.venv/lib/python3.7/site-packages\nSearching for docutils==0.20.1\nBest match: docutils 0.20.1\nAdding docutils 0.20.1 to easy-install.pth file\nInstalling docutils script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_589345fa6bff5b4626acd816a6344081cba53474/.venv/bin\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_589345fa6bff5b4626acd816a6344081cba53474/.venv/lib/python3.7/site-packages\nSearching for qasync==0.25.0\nBest match: qasync 0.25.0\nAdding qasync 0.25.0 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_589345fa6bff5b4626acd816a6344081cba53474/.venv/lib/python3.7/site-packages\nSearching for requests-cache==1.1.1\nBest match: requests-cache 1.1.1\nAdding requests-cache 1.1.1 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_589345fa6bff5b4626acd816a6344081cba53474/.venv/lib/python3.7/site-packages\nSearching for commonmark==0.9.1\nBest match: commonmark 0.9.1\nAdding commonmark 0.9.1 to easy-install.pth file\nInstalling cmark script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_589345fa6bff5b4626acd816a6344081cba53474/.venv/bin\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_589345fa6bff5b4626acd816a6344081cba53474/.venv/lib/python3.7/site-packages\nSearching for importlib-metadata==6.7.0\nBest match: importlib-metadata 6.7.0\nAdding importlib-metadata 6.7.0 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_589345fa6bff5b4626acd816a6344081cba53474/.venv/lib/python3.7/site-packages\nSearching for et-xmlfile==1.1.0\nBest match: et-xmlfile 1.1.0\nAdding et-xmlfile 1.1.0 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_589345fa6bff5b4626acd816a6344081cba53474/.venv/lib/python3.7/site-packages\nSearching for kiwisolver==1.4.5\nBest match: kiwisolver 1.4.5\nAdding kiwisolver 1.4.5 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_589345fa6bff5b4626acd816a6344081cba53474/.venv/lib/python3.7/site-packages\nSearching for cycler==0.11.0\nBest match: cycler 0.11.0\nAdding cycler 0.11.0 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_589345fa6bff5b4626acd816a6344081cba53474/.venv/lib/python3.7/site-packages\nSearching for Pillow==9.5.0\nBest match: Pillow 9.5.0\nAdding Pillow 9.5.0 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_589345fa6bff5b4626acd816a6344081cba53474/.venv/lib/python3.7/site-packages\nSearching for pyparsing==3.1.4\nBest match: pyparsing 3.1.4\nAdding pyparsing 3.1.4 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_589345fa6bff5b4626acd816a6344081cba53474/.venv/lib/python3.7/site-packages\nSearching for fonttools==4.38.0\nBest match: fonttools 4.38.0\nAdding fonttools 4.38.0 to easy-install.pth file\nInstalling fonttools script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_589345fa6bff5b4626acd816a6344081cba53474/.venv/bin\nInstalling pyftmerge script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_589345fa6bff5b4626acd816a6344081cba53474/.venv/bin\nInstalling pyftsubset script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_589345fa6bff5b4626acd816a6344081cba53474/.venv/bin\nInstalling ttx script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_589345fa6bff5b4626acd816a6344081cba53474/.venv/bin\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_589345fa6bff5b4626acd816a6344081cba53474/.venv/lib/python3.7/site-packages\nSearching for jaraco.classes==3.2.3\nBest match: jaraco.classes 3.2.3\nAdding jaraco.classes 3.2.3 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_589345fa6bff5b4626acd816a6344081cba53474/.venv/lib/python3.7/site-packages\nSearching for SecretStorage==3.3.3\nBest match: SecretStorage 3.3.3\nAdding SecretStorage 3.3.3 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_589345fa6bff5b4626acd816a6344081cba53474/.venv/lib/python3.7/site-packages\nSearching for importlib-resources==5.12.0\nBest match: importlib-resources 5.12.0\nAdding importlib-resources 5.12.0 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_589345fa6bff5b4626acd816a6344081cba53474/.venv/lib/python3.7/site-packages\nSearching for jeepney==0.8.0\nBest match: jeepney 0.8.0\nAdding jeepney 0.8.0 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_589345fa6bff5b4626acd816a6344081cba53474/.venv/lib/python3.7/site-packages\nSearching for httpcore==0.13.7\nBest match: httpcore 0.13.7\nAdding httpcore 0.13.7 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_589345fa6bff5b4626acd816a6344081cba53474/.venv/lib/python3.7/site-packages\nSearching for sniffio==1.3.1\nBest match: sniffio 1.3.1\nAdding sniffio 1.3.1 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_589345fa6bff5b4626acd816a6344081cba53474/.venv/lib/python3.7/site-packages\nSearching for rfc3986==1.5.0\nBest match: rfc3986 1.5.0\nAdding rfc3986 1.5.0 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_589345fa6bff5b4626acd816a6344081cba53474/.venv/lib/python3.7/site-packages\nSearching for PyQtWebEngine-Qt5==5.15.16\nBest match: PyQtWebEngine-Qt5 5.15.16\nAdding PyQtWebEngine-Qt5 5.15.16 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_589345fa6bff5b4626acd816a6344081cba53474/.venv/lib/python3.7/site-packages\nSearching for PyQt5-sip==12.13.0\nBest match: PyQt5-sip 12.13.0\nAdding PyQt5-sip 12.13.0 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_589345fa6bff5b4626acd816a6344081cba53474/.venv/lib/python3.7/site-packages\nSearching for PyQt5-Qt5==5.15.16\nBest match: PyQt5-Qt5 5.15.16\nAdding PyQt5-Qt5 5.15.16 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_589345fa6bff5b4626acd816a6344081cba53474/.venv/lib/python3.7/site-packages\nSearching for entrypoints==0.4\nBest match: entrypoints 0.4\nAdding entrypoints 0.4 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_589345fa6bff5b4626acd816a6344081cba53474/.venv/lib/python3.7/site-packages\nSearching for tornado==6.2\nBest match: tornado 6.2\nAdding tornado 6.2 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_589345fa6bff5b4626acd816a6344081cba53474/.venv/lib/python3.7/site-packages\nSearching for nest-asyncio==1.6.0\nBest match: nest-asyncio 1.6.0\nAdding nest-asyncio 1.6.0 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_589345fa6bff5b4626acd816a6344081cba53474/.venv/lib/python3.7/site-packages\nSearching for matplotlib-inline==0.1.6\nBest match: matplotlib-inline 0.1.6\nAdding matplotlib-inline 0.1.6 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_589345fa6bff5b4626acd816a6344081cba53474/.venv/lib/python3.7/site-packages\nSearching for debugpy==1.7.0\nBest match: debugpy 1.7.0\nAdding debugpy 1.7.0 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_589345fa6bff5b4626acd816a6344081cba53474/.venv/lib/python3.7/site-packages\nSearching for ipython==7.34.0\nBest match: ipython 7.34.0\nAdding ipython 7.34.0 to easy-install.pth file\nInstalling iptest script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_589345fa6bff5b4626acd816a6344081cba53474/.venv/bin\nInstalling iptest3 script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_589345fa6bff5b4626acd816a6344081cba53474/.venv/bin\nInstalling ipython script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_589345fa6bff5b4626acd816a6344081cba53474/.venv/bin\nInstalling ipython3 script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_589345fa6bff5b4626acd816a6344081cba53474/.venv/bin\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_589345fa6bff5b4626acd816a6344081cba53474/.venv/lib/python3.7/site-packages\nSearching for psutil==6.1.1\nBest match: psutil 6.1.1\nAdding psutil 6.1.1 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_589345fa6bff5b4626acd816a6344081cba53474/.venv/lib/python3.7/site-packages\nSearching for six==1.17.0\nBest match: six 1.17.0\nAdding six 1.17.0 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_589345fa6bff5b4626acd816a6344081cba53474/.venv/lib/python3.7/site-packages\nSearching for attrs==24.2.0\nBest match: attrs 24.2.0\nAdding attrs 24.2.0 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_589345fa6bff5b4626acd816a6344081cba53474/.venv/lib/python3.7/site-packages\nSearching for url-normalize==1.4.3\nBest match: url-normalize 1.4.3\nAdding url-normalize 1.4.3 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_589345fa6bff5b4626acd816a6344081cba53474/.venv/lib/python3.7/site-packages\nSearching for platformdirs==4.0.0\nBest match: platformdirs 4.0.0\nAdding platformdirs 4.0.0 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_589345fa6bff5b4626acd816a6344081cba53474/.venv/lib/python3.7/site-packages\nSearching for cattrs==23.1.2\nBest match: cattrs 23.1.2\nAdding cattrs 23.1.2 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_589345fa6bff5b4626acd816a6344081cba53474/.venv/lib/python3.7/site-packages\nSearching for zipp==3.15.0\nBest match: zipp 3.15.0\nAdding zipp 3.15.0 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_589345fa6bff5b4626acd816a6344081cba53474/.venv/lib/python3.7/site-packages\nSearching for more-itertools==9.1.0\nBest match: more-itertools 9.1.0\nAdding more-itertools 9.1.0 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_589345fa6bff5b4626acd816a6344081cba53474/.venv/lib/python3.7/site-packages\nSearching for cryptography==44.0.0\nBest match: cryptography 44.0.0\nAdding cryptography 44.0.0 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_589345fa6bff5b4626acd816a6344081cba53474/.venv/lib/python3.7/site-packages\nSearching for anyio==3.7.1\nBest match: anyio 3.7.1\nAdding anyio 3.7.1 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_589345fa6bff5b4626acd816a6344081cba53474/.venv/lib/python3.7/site-packages\nSearching for h11==0.12.0\nBest match: h11 0.12.0\nAdding h11 0.12.0 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_589345fa6bff5b4626acd816a6344081cba53474/.venv/lib/python3.7/site-packages\nSearching for decorator==5.1.1\nBest match: decorator 5.1.1\nAdding decorator 5.1.1 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_589345fa6bff5b4626acd816a6344081cba53474/.venv/lib/python3.7/site-packages\nSearching for backcall==0.2.0\nBest match: backcall 0.2.0\nAdding backcall 0.2.0 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_589345fa6bff5b4626acd816a6344081cba53474/.venv/lib/python3.7/site-packages\nSearching for prompt-toolkit==3.0.48\nBest match: prompt-toolkit 3.0.48\nAdding prompt-toolkit 3.0.48 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_589345fa6bff5b4626acd816a6344081cba53474/.venv/lib/python3.7/site-packages\nSearching for pickleshare==0.7.5\nBest match: pickleshare 0.7.5\nAdding pickleshare 0.7.5 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_589345fa6bff5b4626acd816a6344081cba53474/.venv/lib/python3.7/site-packages\nSearching for pexpect==4.9.0\nBest match: pexpect 4.9.0\nAdding pexpect 4.9.0 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_589345fa6bff5b4626acd816a6344081cba53474/.venv/lib/python3.7/site-packages\nSearching for jedi==0.19.2\nBest match: jedi 0.19.2\nAdding jedi 0.19.2 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_589345fa6bff5b4626acd816a6344081cba53474/.venv/lib/python3.7/site-packages\nSearching for exceptiongroup==1.2.2\nBest match: exceptiongroup 1.2.2\nAdding exceptiongroup 1.2.2 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_589345fa6bff5b4626acd816a6344081cba53474/.venv/lib/python3.7/site-packages\nSearching for cffi==1.15.1\nBest match: cffi 1.15.1\nAdding cffi 1.15.1 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_589345fa6bff5b4626acd816a6344081cba53474/.venv/lib/python3.7/site-packages\nSearching for wcwidth==0.2.13\nBest match: wcwidth 0.2.13\nAdding wcwidth 0.2.13 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_589345fa6bff5b4626acd816a6344081cba53474/.venv/lib/python3.7/site-packages\nSearching for ptyprocess==0.7.0\nBest match: ptyprocess 0.7.0\nAdding ptyprocess 0.7.0 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_589345fa6bff5b4626acd816a6344081cba53474/.venv/lib/python3.7/site-packages\nSearching for parso==0.8.4\nBest match: parso 0.8.4\nAdding parso 0.8.4 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_589345fa6bff5b4626acd816a6344081cba53474/.venv/lib/python3.7/site-packages\nSearching for pycparser==2.21\nBest match: pycparser 2.21\nAdding pycparser 2.21 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_589345fa6bff5b4626acd816a6344081cba53474/.venv/lib/python3.7/site-packages\nFinished processing dependencies for Orange3==3.32.0.dev0+589345f\nVerifying Orange installation...\n✅ Orange installation successful!\nSuccessfully installed orange using Python 3.7\n",
    "setup_res_stderr": "Using CPython 3.7.9\nCreating virtual environment at: .venv\nActivate with: source .venv/bin/activate\nResolved 18 packages in 109ms\nPrepared 18 packages in 27ms\nInstalled 18 packages in 89ms\n + cython==0.29.37\n + exceptiongroup==1.2.2\n + importlib-metadata==6.7.0\n + iniconfig==2.0.0\n + numpy==1.17.5\n + packaging==24.0\n + pluggy==1.2.0\n + pyqt5==5.15.10\n + pyqt5-qt5==5.15.16\n + pyqt5-sip==12.13.0\n + pyqtwebengine==5.15.6\n + pyqtwebengine-qt5==5.15.16\n + pytest==7.4.4\n + setuptools==59.8.0\n + tomli==2.0.1\n + typing-extensions==4.7.1\n + wheel==0.42.0\n + zipp==3.15.0\nResolved 55 packages in 252ms\nInstalled 48 packages in 410ms\n + anyio==3.7.1\n + baycomp==1.0.3\n + bottleneck==1.4.0\n + certifi==2024.12.14\n + cffi==1.15.1\n + chardet==5.2.0\n + charset-normalizer==3.4.1\n + cryptography==44.0.0\n + cycler==0.11.0\n + et-xmlfile==1.1.0\n + fonttools==4.38.0\n + h11==0.12.0\n + httpcore==0.13.7\n + httpx==0.19.0\n + idna==3.10\n + importlib-resources==5.12.0\n + jaraco-classes==3.2.3\n + jeepney==0.8.0\n + joblib==1.3.2\n + keyring==24.1.1\n + keyrings-alt==4.2.0\n + kiwisolver==1.4.5\n + matplotlib==3.5.3\n + more-itertools==9.1.0\n + networkx==2.6.3\n + openpyxl==3.1.3\n + opentsne==1.0.1\n + pandas==1.3.5\n + pillow==9.5.0\n + pip==24.0\n + pycparser==2.21\n + pyparsing==3.1.4\n + python-dateutil==2.9.0.post0\n + python-louvain==0.16\n + pytz==2024.2\n + pyyaml==6.0.1\n + requests==2.31.0\n + rfc3986==1.5.0\n + scikit-learn==1.0.2\n + scipy==1.7.3\n + secretstorage==3.3.3\n + serverfiles==0.3.1\n + six==1.17.0\n + sniffio==1.3.1\n + threadpoolctl==3.1.0\n + urllib3==2.0.7\n + xlrd==2.0.1\n + xlsxwriter==3.2.0\nResolved 59 packages in 668ms\nInstalled 38 packages in 302ms\n + anyqt==0.2.0\n + attrs==24.2.0\n + backcall==0.2.0\n + cattrs==23.1.2\n + commonmark==0.9.1\n + debugpy==1.7.0\n + decorator==5.1.1\n + dictdiffer==0.9.0\n + docutils==0.20.1\n + entrypoints==0.4\n + ipykernel==6.16.2\n + ipython==7.34.0\n + ipython-genutils==0.2.0\n + jedi==0.19.2\n + jupyter-client==7.4.9\n + jupyter-core==4.12.0\n + matplotlib-inline==0.1.6\n + nest-asyncio==1.6.0\n + orange-canvas-core==0.1.35\n + orange-widget-base==4.22.0\n + parso==0.8.4\n + pexpect==4.9.0\n + pickleshare==0.7.5\n + platformdirs==4.0.0\n + prompt-toolkit==3.0.48\n + psutil==6.1.1\n + ptyprocess==0.7.0\n + pygments==2.17.2\n + pyqtgraph==0.12.4\n + pyzmq==26.2.0\n + qasync==0.25.0\n + qtconsole==5.4.4\n + qtpy==2.4.2\n + requests-cache==1.1.1\n + tornado==6.2\n + traitlets==5.9.0\n + url-normalize==1.4.3\n + wcwidth==0.2.13\nResolved 2 packages in 11ms\nInstalled 2 packages in 53ms\n + psycopg2==2.9.9\n + pymssql==2.3.2\nResolved 19 packages in 56ms\nInstalled 5 packages in 1.03s\n + catboost==1.2.7\n + graphviz==0.20.1\n + plotly==5.18.0\n + tenacity==8.2.3\n + xgboost==1.6.2\nAudited 2 packages in 1ms\nIn file included from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_589345fa6bff5b4626acd816a6344081cba53474/.venv/lib/python3.7/site-packages/numpy/core/include/numpy/ndarraytypes.h:1830,\n                 from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_589345fa6bff5b4626acd816a6344081cba53474/.venv/lib/python3.7/site-packages/numpy/core/include/numpy/ndarrayobject.h:12,\n                 from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_589345fa6bff5b4626acd816a6344081cba53474/.venv/lib/python3.7/site-packages/numpy/core/include/numpy/arrayobject.h:4,\n                 from Orange/classification/_tree_scorers.c:775:\n/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_589345fa6bff5b4626acd816a6344081cba53474/.venv/lib/python3.7/site-packages/numpy/core/include/numpy/npy_1_7_deprecated_api.h:17:2: warning: #warning \"Using deprecated NumPy API, disable it with \" \"#define NPY_NO_DEPRECATED_API NPY_1_7_API_VERSION\" [-Wcpp]\n   17 | #warning \"Using deprecated NumPy API, disable it with \" \\\n      |  ^~~~~~~\nOrange/classification/_tree_scorers.c: In function ‘__pyx_pf_6Orange_14classification_13_tree_scorers_2find_threshold_entropy’:\nOrange/classification/_tree_scorers.c:3427:27: warning: comparison of integer expressions of different signedness: ‘unsigned int’ and ‘int’ [-Wsign-compare]\n 3427 |   __pyx_t_7 = ((__pyx_v_N <= __pyx_v_min_leaf) != 0);\n      |                           ^~\nOrange/classification/_tree_scorers.c: In function ‘__pyx_pf_6Orange_14classification_13_tree_scorers_6find_threshold_MSE’:\nOrange/classification/_tree_scorers.c:4883:27: warning: comparison of integer expressions of different signedness: ‘unsigned int’ and ‘int’ [-Wsign-compare]\n 4883 |   __pyx_t_1 = ((__pyx_v_N <= __pyx_v_min_leaf) != 0);\n      |                           ^~\nOrange/classification/_tree_scorers.c: In function ‘__pyx_pf_6Orange_14classification_13_tree_scorers_8find_binarization_MSE’:\nOrange/classification/_tree_scorers.c:5629:43: warning: comparison of integer expressions of different signedness: ‘unsigned int’ and ‘int’ [-Wsign-compare]\n 5629 |           for (__pyx_t_19 = 0; __pyx_t_19 < __pyx_t_18; __pyx_t_19+=1) {\n      |                                           ^\nOrange/classification/_tree_scorers.c:5750:39: warning: comparison of integer expressions of different signedness: ‘unsigned int’ and ‘int’ [-Wsign-compare]\n 5750 |           __pyx_t_20 = ((__pyx_v_left >= __pyx_v_min_leaf) != 0);\n      |                                       ^~\nOrange/classification/_tree_scorers.c:5756:53: warning: comparison of integer expressions of different signedness: ‘unsigned int’ and ‘int’ [-Wsign-compare]\n 5756 |           __pyx_t_20 = (((__pyx_v_N - __pyx_v_left) >= __pyx_v_min_leaf) != 0);\n      |                                                     ^~\nOrange/classification/_tree_scorers.c: In function ‘__pyx_pf_6Orange_14classification_13_tree_scorers_14compute_predictions_csr’:\nOrange/classification/_tree_scorers.c:7217:41: warning: comparison of integer expressions of different signedness: ‘unsigned int’ and ‘int’ [-Wsign-compare]\n 7217 |         for (__pyx_t_12 = 0; __pyx_t_12 < __pyx_t_11; __pyx_t_12+=1) {\n      |                                         ^\nOrange/classification/_tree_scorers.c: In function ‘__pyx_pf_6Orange_14classification_13_tree_scorers_16compute_predictions_csc’:\nOrange/classification/_tree_scorers.c:7874:41: warning: comparison of integer expressions of different signedness: ‘unsigned int’ and ‘int’ [-Wsign-compare]\n 7874 |         for (__pyx_t_12 = 0; __pyx_t_12 < __pyx_t_11; __pyx_t_12+=1) {\n      |                                         ^\nOrange/classification/_tree_scorers.c:7934:150: warning: comparison of integer expressions of different signedness: ‘__pyx_t_5numpy_int32_t’ {aka ‘int’} and ‘unsigned int’ [-Wsign-compare]\n 7934 |               __pyx_t_16 = (((*((__pyx_t_5numpy_int32_t const  *) ( /* dim=0 */ (__pyx_v_indices.data + __pyx_t_15 * __pyx_v_indices.strides[0]) ))) != __pyx_v_i) != 0);\n      |                                                                                                                                                      ^~\nIn file included from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_589345fa6bff5b4626acd816a6344081cba53474/.venv/lib/python3.7/site-packages/numpy/core/include/numpy/ndarraytypes.h:1830,\n                 from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_589345fa6bff5b4626acd816a6344081cba53474/.venv/lib/python3.7/site-packages/numpy/core/include/numpy/ndarrayobject.h:12,\n                 from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_589345fa6bff5b4626acd816a6344081cba53474/.venv/lib/python3.7/site-packages/numpy/core/include/numpy/arrayobject.h:4,\n                 from Orange/data/_contingency.c:775:\n/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_589345fa6bff5b4626acd816a6344081cba53474/.venv/lib/python3.7/site-packages/numpy/core/include/numpy/npy_1_7_deprecated_api.h:17:2: warning: #warning \"Using deprecated NumPy API, disable it with \" \"#define NPY_NO_DEPRECATED_API NPY_1_7_API_VERSION\" [-Wcpp]\n   17 | #warning \"Using deprecated NumPy API, disable it with \" \\\n      |  ^~~~~~~\nOrange/data/_io.c: In function ‘__pyx_pf_6Orange_4data_3_io_sparse_read_float.constprop’:\nOrange/data/_io.c:4120:43: warning: ‘__pyx_v_decs’ may be used uninitialized in this function [-Wmaybe-uninitialized]\n 4120 |           __pyx_v_value = ((__pyx_v_value * __pyx_v_decs) + (__pyx_v_c & 0xf));\n      |                            ~~~~~~~~~~~~~~~^~~~~~~~~~~~~~~\nOrange/data/_io.c:4318:24: warning: ‘__pyx_v_value’ may be used uninitialized in this function [-Wmaybe-uninitialized]\n 4318 |           __pyx_t_10 = PyFloat_FromDouble(__pyx_v_value); if (unlikely(!__pyx_t_10)) __PYX_ERR(0, 223, __pyx_L5_error)\n      |                        ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\nOrange/data/_io.c:683:40: warning: ‘__pyx_v_attr_index’ may be used uninitialized in this function [-Wmaybe-uninitialized]\n  683 |   #define PyInt_FromLong               PyLong_FromLong\n      |                                        ^~~~~~~~~~~~~~~\nOrange/data/_io.c:1590:7: note: ‘__pyx_v_attr_index’ was declared here\n 1590 |   int __pyx_v_attr_index;\n      |       ^~~~~~~~~~~~~~~~~~\nOrange/data/_io.c:3183:25: warning: ‘__pyx_v_atomp’ may be used uninitialized in this function [-Wmaybe-uninitialized]\n 3183 |           __pyx_v_atomp = (__pyx_v_atomp + 1);\n      |           ~~~~~~~~~~~~~~^~~~~~~~~~~~~~~~~~~~~\nIn file included from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_589345fa6bff5b4626acd816a6344081cba53474/.venv/lib/python3.7/site-packages/numpy/core/include/numpy/ndarraytypes.h:1830,\n                 from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_589345fa6bff5b4626acd816a6344081cba53474/.venv/lib/python3.7/site-packages/numpy/core/include/numpy/ndarrayobject.h:12,\n                 from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_589345fa6bff5b4626acd816a6344081cba53474/.venv/lib/python3.7/site-packages/numpy/core/include/numpy/arrayobject.h:4,\n                 from Orange/data/_valuecount.c:775:\n/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_589345fa6bff5b4626acd816a6344081cba53474/.venv/lib/python3.7/site-packages/numpy/core/include/numpy/npy_1_7_deprecated_api.h:17:2: warning: #warning \"Using deprecated NumPy API, disable it with \" \"#define NPY_NO_DEPRECATED_API NPY_1_7_API_VERSION\" [-Wcpp]\n   17 | #warning \"Using deprecated NumPy API, disable it with \" \\\n      |  ^~~~~~~\nIn file included from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_589345fa6bff5b4626acd816a6344081cba53474/.venv/lib/python3.7/site-packages/numpy/core/include/numpy/ndarraytypes.h:1830,\n                 from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_589345fa6bff5b4626acd816a6344081cba53474/.venv/lib/python3.7/site-packages/numpy/core/include/numpy/ndarrayobject.h:12,\n                 from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_589345fa6bff5b4626acd816a6344081cba53474/.venv/lib/python3.7/site-packages/numpy/core/include/numpy/arrayobject.h:4,\n                 from Orange/distance/_distance.c:775:\n/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_589345fa6bff5b4626acd816a6344081cba53474/.venv/lib/python3.7/site-packages/numpy/core/include/numpy/npy_1_7_deprecated_api.h:17:2: warning: #warning \"Using deprecated NumPy API, disable it with \" \"#define NPY_NO_DEPRECATED_API NPY_1_7_API_VERSION\" [-Wcpp]\n   17 | #warning \"Using deprecated NumPy API, disable it with \" \\\n      |  ^~~~~~~\nIn file included from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_589345fa6bff5b4626acd816a6344081cba53474/.venv/lib/python3.7/site-packages/numpy/core/include/numpy/ndarraytypes.h:1830,\n                 from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_589345fa6bff5b4626acd816a6344081cba53474/.venv/lib/python3.7/site-packages/numpy/core/include/numpy/ndarrayobject.h:12,\n                 from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_589345fa6bff5b4626acd816a6344081cba53474/.venv/lib/python3.7/site-packages/numpy/core/include/numpy/arrayobject.h:4,\n                 from Orange/preprocess/_discretize.c:774:\n/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_589345fa6bff5b4626acd816a6344081cba53474/.venv/lib/python3.7/site-packages/numpy/core/include/numpy/npy_1_7_deprecated_api.h:17:2: warning: #warning \"Using deprecated NumPy API, disable it with \" \"#define NPY_NO_DEPRECATED_API NPY_1_7_API_VERSION\" [-Wcpp]\n   17 | #warning \"Using deprecated NumPy API, disable it with \" \\\n      |  ^~~~~~~\nIn file included from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_589345fa6bff5b4626acd816a6344081cba53474/.venv/lib/python3.7/site-packages/numpy/core/include/numpy/ndarraytypes.h:1830,\n                 from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_589345fa6bff5b4626acd816a6344081cba53474/.venv/lib/python3.7/site-packages/numpy/core/include/numpy/ndarrayobject.h:12,\n                 from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_589345fa6bff5b4626acd816a6344081cba53474/.venv/lib/python3.7/site-packages/numpy/core/include/numpy/arrayobject.h:4,\n                 from Orange/preprocess/_relieff.cpp:790:\n/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_589345fa6bff5b4626acd816a6344081cba53474/.venv/lib/python3.7/site-packages/numpy/core/include/numpy/npy_1_7_deprecated_api.h:17:2: warning: #warning \"Using deprecated NumPy API, disable it with \" \"#define NPY_NO_DEPRECATED_API NPY_1_7_API_VERSION\" [-Wcpp]\n   17 | #warning \"Using deprecated NumPy API, disable it with \" \\\n      |  ^~~~~~~\nIn file included from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_589345fa6bff5b4626acd816a6344081cba53474/.venv/lib/python3.7/site-packages/numpy/core/include/numpy/ndarraytypes.h:1830,\n                 from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_589345fa6bff5b4626acd816a6344081cba53474/.venv/lib/python3.7/site-packages/numpy/core/include/numpy/ndarrayobject.h:12,\n                 from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_589345fa6bff5b4626acd816a6344081cba53474/.venv/lib/python3.7/site-packages/numpy/core/include/numpy/arrayobject.h:4,\n                 from Orange/projection/_som.c:775:\n/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_589345fa6bff5b4626acd816a6344081cba53474/.venv/lib/python3.7/site-packages/numpy/core/include/numpy/npy_1_7_deprecated_api.h:17:2: warning: #warning \"Using deprecated NumPy API, disable it with \" \"#define NPY_NO_DEPRECATED_API NPY_1_7_API_VERSION\" [-Wcpp]\n   17 | #warning \"Using deprecated NumPy API, disable it with \" \\\n      |  ^~~~~~~\nOrange/projection/_som.c: In function ‘__pyx_pw_6Orange_10projection_4_som_5update_hex’:\nOrange/projection/_som.c:3863:7: warning: ‘__pyx_v_win_y’ may be used uninitialized in this function [-Wmaybe-uninitialized]\n 3863 |   int __pyx_v_win_y;\n      |       ^~~~~~~~~~~~~\nOrange/projection/_som.c:3862:7: warning: ‘__pyx_v_win_x’ may be used uninitialized in this function [-Wmaybe-uninitialized]\n 3862 |   int __pyx_v_win_x;\n      |       ^~~~~~~~~~~~~\nOrange/projection/_som.c: In function ‘__pyx_pf_6Orange_10projection_4_som_8update_sparse.constprop’:\nOrange/projection/_som.c:4934:7: warning: ‘__pyx_v_win_y’ may be used uninitialized in this function [-Wmaybe-uninitialized]\n 4934 |   int __pyx_v_win_y;\n      |       ^~~~~~~~~~~~~\nOrange/projection/_som.c:4933:7: warning: ‘__pyx_v_win_x’ may be used uninitialized in this function [-Wmaybe-uninitialized]\n 4933 |   int __pyx_v_win_x;\n      |       ^~~~~~~~~~~~~\nOrange/projection/_som.c: In function ‘__pyx_pw_6Orange_10projection_4_som_7get_winners_sparse’:\nOrange/projection/_som.c:4732:223: warning: ‘__pyx_v_win_y’ may be used uninitialized in this function [-Wmaybe-uninitialized]\n 4732 |           *__Pyx_BufPtrStrided2d(__pyx_t_5numpy_int16_t *, __pyx_pybuffernd_winners.rcbuffer->pybuffer.buf, __pyx_t_26, __pyx_pybuffernd_winners.diminfo[0].strides, __pyx_t_27, __pyx_pybuffernd_winners.diminfo[1].strides) = __pyx_v_win_y;\n      |           ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^~~~~~~~~~~~~~~\nOrange/projection/_som.c:4307:7: note: ‘__pyx_v_win_y’ was declared here\n 4307 |   int __pyx_v_win_y;\n      |       ^~~~~~~~~~~~~\nOrange/projection/_som.c:4721:223: warning: ‘__pyx_v_win_x’ may be used uninitialized in this function [-Wmaybe-uninitialized]\n 4721 |           *__Pyx_BufPtrStrided2d(__pyx_t_5numpy_int16_t *, __pyx_pybuffernd_winners.rcbuffer->pybuffer.buf, __pyx_t_27, __pyx_pybuffernd_winners.diminfo[0].strides, __pyx_t_26, __pyx_pybuffernd_winners.diminfo[1].strides) = __pyx_v_win_x;\n      |           ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^~~~~~~~~~~~~~~\nOrange/projection/_som.c:4306:7: note: ‘__pyx_v_win_x’ was declared here\n 4306 |   int __pyx_v_win_x;\n      |       ^~~~~~~~~~~~~\nOrange/projection/_som.c: In function ‘__pyx_pf_6Orange_10projection_4_som_10update_sparse_hex.constprop’:\nOrange/projection/_som.c:5682:7: warning: ‘__pyx_v_win_y’ may be used uninitialized in this function [-Wmaybe-uninitialized]\n 5682 |   int __pyx_v_win_y;\n      |       ^~~~~~~~~~~~~\nOrange/projection/_som.c:5681:7: warning: ‘__pyx_v_win_x’ may be used uninitialized in this function [-Wmaybe-uninitialized]\n 5681 |   int __pyx_v_win_x;\n      |       ^~~~~~~~~~~~~\nOrange/projection/_som.c: In function ‘__pyx_pw_6Orange_10projection_4_som_1get_winners’:\nOrange/projection/_som.c:3184:223: warning: ‘__pyx_v_win_y’ may be used uninitialized in this function [-Wmaybe-uninitialized]\n 3184 |           *__Pyx_BufPtrStrided2d(__pyx_t_5numpy_int16_t *, __pyx_pybuffernd_winners.rcbuffer->pybuffer.buf, __pyx_t_22, __pyx_pybuffernd_winners.diminfo[0].strides, __pyx_t_23, __pyx_pybuffernd_winners.diminfo[1].strides) = __pyx_v_win_y;\n      |           ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^~~~~~~~~~~~~~~\nOrange/projection/_som.c:2887:7: note: ‘__pyx_v_win_y’ was declared here\n 2887 |   int __pyx_v_win_y;\n      |       ^~~~~~~~~~~~~\nOrange/projection/_som.c:3173:223: warning: ‘__pyx_v_win_x’ may be used uninitialized in this function [-Wmaybe-uninitialized]\n 3173 |           *__Pyx_BufPtrStrided2d(__pyx_t_5numpy_int16_t *, __pyx_pybuffernd_winners.rcbuffer->pybuffer.buf, __pyx_t_23, __pyx_pybuffernd_winners.diminfo[0].strides, __pyx_t_22, __pyx_pybuffernd_winners.diminfo[1].strides) = __pyx_v_win_x;\n      |           ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^~~~~~~~~~~~~~~\nOrange/projection/_som.c:2886:7: note: ‘__pyx_v_win_x’ was declared here\n 2886 |   int __pyx_v_win_x;\n      |       ^~~~~~~~~~~~~\nOrange/projection/_som.c: In function ‘__pyx_pw_6Orange_10projection_4_som_3update’:\nOrange/projection/_som.c:3357:7: warning: ‘__pyx_v_win_y’ may be used uninitialized in this function [-Wmaybe-uninitialized]\n 3357 |   int __pyx_v_win_y;\n      |       ^~~~~~~~~~~~~\nOrange/classification/_simple_tree.c: In function ‘gain_ratio_c’:\nOrange/classification/_simple_tree.c:63:9: warning: implicit declaration of function ‘qsort_r’; did you mean ‘qsort’? [-Wimplicit-function-declaration]\n   63 |         qsort_r(base, nel, size, compar, thunk)\n      |         ^~~~~~~\nOrange/classification/_simple_tree.c:147:9: note: in expansion of macro ‘QSORT_R_FUNC’\n  147 |         QSORT_R_FUNC(examples, size, sizeof(struct Example), (void*) &compar_attr, compar_examples);\n      |         ^~~~~~~~~~~~\n/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_589345fa6bff5b4626acd816a6344081cba53474/.venv/lib/python3.7/site-packages/setuptools/command/easy_install.py:159: EasyInstallDeprecationWarning: easy_install command is deprecated. Use build and pip and other standards-based tools.\n  EasyInstallDeprecationWarning,\n/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_589345fa6bff5b4626acd816a6344081cba53474/.venv/lib/python3.7/site-packages/setuptools/command/install.py:37: SetuptoolsDeprecationWarning: setup.py install is deprecated. Use build and pip and other standards-based tools.\n  setuptools.SetuptoolsDeprecationWarning,\nwarning: no files found matching 'README*' under directory 'Orange'\nwarning: no files found matching 'LICENSE' under directory 'Orange'\nwarning: no files found matching '*.basket' under directory 'Orange/datasets'\nwarning: no files found matching '*.svg' under directory 'Orange/canvas'\nwarning: no files found matching '*.js' under directory 'Orange/widgets'\nwarning: no files found matching '*.css' under directory 'Orange/widgets'\nwarning: no files found matching '*.html' under directory 'Orange/widgets'\nwarning: no files found matching '*.svg' under directory 'distribute'\nwarning: no files found matching 'COPYING'\n",
    "new_commit_res_code": 0,
    "new_commit_res_stdout": "============================= test session starts ==============================\nplatform linux -- Python 3.7.9, pytest-7.4.4, pluggy-1.2.0\nrootdir: /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_589345fa6bff5b4626acd816a6344081cba53474\nplugins: anyio-3.7.1\ncollected 79 items\n\nr2e_tests/test_1.py sss................................................. [ 65%]\n...........................                                              [100%]\n\n==================================== PASSES ====================================\n=========================== short test summary info ============================\nPASSED r2e_tests/test_1.py::TestOWScatterPlot::test_add_line_calls_proper_regressor\nPASSED r2e_tests/test_1.py::TestOWScatterPlot::test_attr_label_metas\nPASSED r2e_tests/test_1.py::TestOWScatterPlot::test_attr_models\nPASSED r2e_tests/test_1.py::TestOWScatterPlot::test_auto_send_selection\nPASSED r2e_tests/test_1.py::TestOWScatterPlot::test_class_density\nPASSED r2e_tests/test_1.py::TestOWScatterPlot::test_color_is_optional\nPASSED r2e_tests/test_1.py::TestOWScatterPlot::test_data_column_infs\nPASSED r2e_tests/test_1.py::TestOWScatterPlot::test_data_column_nans\nPASSED r2e_tests/test_1.py::TestOWScatterPlot::test_datasets\nPASSED r2e_tests/test_1.py::TestOWScatterPlot::test_default_attrs\nPASSED r2e_tests/test_1.py::TestOWScatterPlot::test_dragging_tooltip\nPASSED r2e_tests/test_1.py::TestOWScatterPlot::test_error_message\nPASSED r2e_tests/test_1.py::TestOWScatterPlot::test_features_and_data\nPASSED r2e_tests/test_1.py::TestOWScatterPlot::test_features_and_no_data\nPASSED r2e_tests/test_1.py::TestOWScatterPlot::test_group_selections\nPASSED r2e_tests/test_1.py::TestOWScatterPlot::test_handle_metas\nPASSED r2e_tests/test_1.py::TestOWScatterPlot::test_handle_primitive_metas\nPASSED r2e_tests/test_1.py::TestOWScatterPlot::test_hidden_effective_variables\nPASSED r2e_tests/test_1.py::TestOWScatterPlot::test_image_export\nPASSED r2e_tests/test_1.py::TestOWScatterPlot::test_invalid_points_selection\nPASSED r2e_tests/test_1.py::TestOWScatterPlot::test_invalidated_diff_features\nPASSED r2e_tests/test_1.py::TestOWScatterPlot::test_invalidated_diff_features_features_first\nPASSED r2e_tests/test_1.py::TestOWScatterPlot::test_invalidated_diff_features_same_time\nPASSED r2e_tests/test_1.py::TestOWScatterPlot::test_invalidated_diff_features_same_time_features_first\nPASSED r2e_tests/test_1.py::TestOWScatterPlot::test_invalidated_embedding\nPASSED r2e_tests/test_1.py::TestOWScatterPlot::test_invalidated_features_first\nPASSED r2e_tests/test_1.py::TestOWScatterPlot::test_invalidated_same_features\nPASSED r2e_tests/test_1.py::TestOWScatterPlot::test_invalidated_same_time\nPASSED r2e_tests/test_1.py::TestOWScatterPlot::test_invalidated_same_time_features_first\nPASSED r2e_tests/test_1.py::TestOWScatterPlot::test_jittering\nPASSED r2e_tests/test_1.py::TestOWScatterPlot::test_many_discrete_values\nPASSED r2e_tests/test_1.py::TestOWScatterPlot::test_metas_zero_column\nPASSED r2e_tests/test_1.py::TestOWScatterPlot::test_migrate_selection\nPASSED r2e_tests/test_1.py::TestOWScatterPlot::test_minimum_size\nPASSED r2e_tests/test_1.py::TestOWScatterPlot::test_msg_base_class\nPASSED r2e_tests/test_1.py::TestOWScatterPlot::test_no_regression_line\nPASSED r2e_tests/test_1.py::TestOWScatterPlot::test_none_data\nPASSED r2e_tests/test_1.py::TestOWScatterPlot::test_on_manual_change\nPASSED r2e_tests/test_1.py::TestOWScatterPlot::test_opacity_warning\nPASSED r2e_tests/test_1.py::TestOWScatterPlot::test_optional_combos\nPASSED r2e_tests/test_1.py::TestOWScatterPlot::test_orthonormal_line\nPASSED r2e_tests/test_1.py::TestOWScatterPlot::test_output_features\nPASSED r2e_tests/test_1.py::TestOWScatterPlot::test_outputs\nPASSED r2e_tests/test_1.py::TestOWScatterPlot::test_plot_once\nPASSED r2e_tests/test_1.py::TestOWScatterPlot::test_points_combo_boxes\nPASSED r2e_tests/test_1.py::TestOWScatterPlot::test_points_selection\nPASSED r2e_tests/test_1.py::TestOWScatterPlot::test_regression_line\nPASSED r2e_tests/test_1.py::TestOWScatterPlot::test_regression_line_coeffs\nPASSED r2e_tests/test_1.py::TestOWScatterPlot::test_regression_lines_appear\nPASSED r2e_tests/test_1.py::TestOWScatterPlot::test_report_on_empty\nPASSED r2e_tests/test_1.py::TestOWScatterPlot::test_saved_selection\nPASSED r2e_tests/test_1.py::TestOWScatterPlot::test_saving_selection\nPASSED r2e_tests/test_1.py::TestOWScatterPlot::test_score_heuristics\nPASSED r2e_tests/test_1.py::TestOWScatterPlot::test_send_report\nPASSED r2e_tests/test_1.py::TestOWScatterPlot::test_set_data\nPASSED r2e_tests/test_1.py::TestOWScatterPlot::test_set_strings_settings\nPASSED r2e_tests/test_1.py::TestOWScatterPlot::test_setup_graph\nPASSED r2e_tests/test_1.py::TestOWScatterPlot::test_sparse_data\nPASSED r2e_tests/test_1.py::TestOWScatterPlot::test_subset_data\nPASSED r2e_tests/test_1.py::TestOWScatterPlot::test_subset_data_color\nPASSED r2e_tests/test_1.py::TestOWScatterPlot::test_time_axis\nPASSED r2e_tests/test_1.py::TestOWScatterPlot::test_tooltip\nPASSED r2e_tests/test_1.py::TestOWScatterPlot::test_update_regression_line_calls_add_line\nPASSED r2e_tests/test_1.py::TestOWScatterPlot::test_update_regression_line_is_called\nPASSED r2e_tests/test_1.py::TestOWScatterPlot::test_visual_settings\nPASSED r2e_tests/test_1.py::TestOWScatterPlot::test_vizrank\nPASSED r2e_tests/test_1.py::TestOWScatterPlot::test_vizrank_class_nan\nPASSED r2e_tests/test_1.py::TestOWScatterPlot::test_vizrank_enabled\nPASSED r2e_tests/test_1.py::TestOWScatterPlot::test_vizrank_enabled_color_var_nans\nPASSED r2e_tests/test_1.py::TestOWScatterPlot::test_vizrank_enabled_constant_data\nPASSED r2e_tests/test_1.py::TestOWScatterPlot::test_vizrank_enabled_no_color_var\nPASSED r2e_tests/test_1.py::TestOWScatterPlot::test_vizrank_enabled_no_data\nPASSED r2e_tests/test_1.py::TestOWScatterPlot::test_vizrank_enabled_sparse_data\nPASSED r2e_tests/test_1.py::TestOWScatterPlot::test_vizrank_enabled_two_features\nPASSED r2e_tests/test_1.py::TestOWScatterPlot::test_vizrank_nonprimitives\nPASSED r2e_tests/test_1.py::TestOWScatterPlot::test_vizrank_receives_manual_change\nSKIPPED [1] .venv/lib/python3.7/site-packages/orangewidget/tests/base.py:244: image exporting not tested as .widget was not set\nSKIPPED [1] .venv/lib/python3.7/site-packages/orangewidget/tests/base.py:238: minimum size not tested as .widget was not set\nSKIPPED [1] .venv/lib/python3.7/site-packages/orangewidget/tests/base.py:250: msg base class not tested as .widget was not set\n======================== 76 passed, 3 skipped in 16.33s ========================\nWidgets left: 0    Max widgets: 23511 \n\n",
    "new_commit_res_stderr": "",
    "old_commit_res_code": 1,
    "old_commit_res_stdout": "============================= test session starts ==============================\nplatform linux -- Python 3.7.9, pytest-7.4.4, pluggy-1.2.0\nrootdir: /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_589345fa6bff5b4626acd816a6344081cba53474\nplugins: anyio-3.7.1\ncollected 79 items\n\nr2e_tests/test_1.py sss............................F.................... [ 65%]\n...........................                                              [100%]\n\n=================================== FAILURES ===================================\n_________ TestOWScatterPlot.test_invalidated_same_time_features_first __________\n\nself = <r2e_tests.test_1.TestOWScatterPlot testMethod=test_invalidated_same_time_features_first>\n\n    def test_invalidated_same_time_features_first(self):\n        self.widget.setup_plot = Mock()\n        # send features and data at the same time (features first)\n        features = self.data.domain.attributes[:2]\n        signals = [(self.widget.Inputs.features, AttributeList(features)),\n                   (self.widget.Inputs.data, self.data)]\n        self.send_signals(signals)\n        self.widget.setup_plot.assert_called_once()\n        self.assertListEqual(self.widget.effective_variables, list(features))\n    \n        self.widget.setup_plot.reset_mock()\n        features = self.data.domain.attributes[2:]\n        signals = [(self.widget.Inputs.features, AttributeList(features)),\n                   (self.widget.Inputs.data, self.data)]\n        self.send_signals(signals)\n>       self.widget.setup_plot.assert_called_once()\n\nr2e_tests/test_1.py:751: \n_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ \n\n_mock_self = <Mock id='128774288085392'>\n\n    def assert_called_once(_mock_self):\n        \"\"\"assert that the mock was called only once.\n        \"\"\"\n        self = _mock_self\n        if not self.call_count == 1:\n            msg = (\"Expected '%s' to have been called once. Called %s times.\" %\n                   (self._mock_name or 'mock', self.call_count))\n>           raise AssertionError(msg)\nE           AssertionError: Expected 'mock' to have been called once. Called 0 times.\n\n../../../../.local/share/uv/python/cpython-3.7.9-linux-x86_64-gnu/lib/python3.7/unittest/mock.py:859: AssertionError\n==================================== PASSES ====================================\n=========================== short test summary info ============================\nPASSED r2e_tests/test_1.py::TestOWScatterPlot::test_add_line_calls_proper_regressor\nPASSED r2e_tests/test_1.py::TestOWScatterPlot::test_attr_label_metas\nPASSED r2e_tests/test_1.py::TestOWScatterPlot::test_attr_models\nPASSED r2e_tests/test_1.py::TestOWScatterPlot::test_auto_send_selection\nPASSED r2e_tests/test_1.py::TestOWScatterPlot::test_class_density\nPASSED r2e_tests/test_1.py::TestOWScatterPlot::test_color_is_optional\nPASSED r2e_tests/test_1.py::TestOWScatterPlot::test_data_column_infs\nPASSED r2e_tests/test_1.py::TestOWScatterPlot::test_data_column_nans\nPASSED r2e_tests/test_1.py::TestOWScatterPlot::test_datasets\nPASSED r2e_tests/test_1.py::TestOWScatterPlot::test_default_attrs\nPASSED r2e_tests/test_1.py::TestOWScatterPlot::test_dragging_tooltip\nPASSED r2e_tests/test_1.py::TestOWScatterPlot::test_error_message\nPASSED r2e_tests/test_1.py::TestOWScatterPlot::test_features_and_data\nPASSED r2e_tests/test_1.py::TestOWScatterPlot::test_features_and_no_data\nPASSED r2e_tests/test_1.py::TestOWScatterPlot::test_group_selections\nPASSED r2e_tests/test_1.py::TestOWScatterPlot::test_handle_metas\nPASSED r2e_tests/test_1.py::TestOWScatterPlot::test_handle_primitive_metas\nPASSED r2e_tests/test_1.py::TestOWScatterPlot::test_hidden_effective_variables\nPASSED r2e_tests/test_1.py::TestOWScatterPlot::test_image_export\nPASSED r2e_tests/test_1.py::TestOWScatterPlot::test_invalid_points_selection\nPASSED r2e_tests/test_1.py::TestOWScatterPlot::test_invalidated_diff_features\nPASSED r2e_tests/test_1.py::TestOWScatterPlot::test_invalidated_diff_features_features_first\nPASSED r2e_tests/test_1.py::TestOWScatterPlot::test_invalidated_diff_features_same_time\nPASSED r2e_tests/test_1.py::TestOWScatterPlot::test_invalidated_diff_features_same_time_features_first\nPASSED r2e_tests/test_1.py::TestOWScatterPlot::test_invalidated_embedding\nPASSED r2e_tests/test_1.py::TestOWScatterPlot::test_invalidated_features_first\nPASSED r2e_tests/test_1.py::TestOWScatterPlot::test_invalidated_same_features\nPASSED r2e_tests/test_1.py::TestOWScatterPlot::test_invalidated_same_time\nPASSED r2e_tests/test_1.py::TestOWScatterPlot::test_jittering\nPASSED r2e_tests/test_1.py::TestOWScatterPlot::test_many_discrete_values\nPASSED r2e_tests/test_1.py::TestOWScatterPlot::test_metas_zero_column\nPASSED r2e_tests/test_1.py::TestOWScatterPlot::test_migrate_selection\nPASSED r2e_tests/test_1.py::TestOWScatterPlot::test_minimum_size\nPASSED r2e_tests/test_1.py::TestOWScatterPlot::test_msg_base_class\nPASSED r2e_tests/test_1.py::TestOWScatterPlot::test_no_regression_line\nPASSED r2e_tests/test_1.py::TestOWScatterPlot::test_none_data\nPASSED r2e_tests/test_1.py::TestOWScatterPlot::test_on_manual_change\nPASSED r2e_tests/test_1.py::TestOWScatterPlot::test_opacity_warning\nPASSED r2e_tests/test_1.py::TestOWScatterPlot::test_optional_combos\nPASSED r2e_tests/test_1.py::TestOWScatterPlot::test_orthonormal_line\nPASSED r2e_tests/test_1.py::TestOWScatterPlot::test_output_features\nPASSED r2e_tests/test_1.py::TestOWScatterPlot::test_outputs\nPASSED r2e_tests/test_1.py::TestOWScatterPlot::test_plot_once\nPASSED r2e_tests/test_1.py::TestOWScatterPlot::test_points_combo_boxes\nPASSED r2e_tests/test_1.py::TestOWScatterPlot::test_points_selection\nPASSED r2e_tests/test_1.py::TestOWScatterPlot::test_regression_line\nPASSED r2e_tests/test_1.py::TestOWScatterPlot::test_regression_line_coeffs\nPASSED r2e_tests/test_1.py::TestOWScatterPlot::test_regression_lines_appear\nPASSED r2e_tests/test_1.py::TestOWScatterPlot::test_report_on_empty\nPASSED r2e_tests/test_1.py::TestOWScatterPlot::test_saved_selection\nPASSED r2e_tests/test_1.py::TestOWScatterPlot::test_saving_selection\nPASSED r2e_tests/test_1.py::TestOWScatterPlot::test_score_heuristics\nPASSED r2e_tests/test_1.py::TestOWScatterPlot::test_send_report\nPASSED r2e_tests/test_1.py::TestOWScatterPlot::test_set_data\nPASSED r2e_tests/test_1.py::TestOWScatterPlot::test_set_strings_settings\nPASSED r2e_tests/test_1.py::TestOWScatterPlot::test_setup_graph\nPASSED r2e_tests/test_1.py::TestOWScatterPlot::test_sparse_data\nPASSED r2e_tests/test_1.py::TestOWScatterPlot::test_subset_data\nPASSED r2e_tests/test_1.py::TestOWScatterPlot::test_subset_data_color\nPASSED r2e_tests/test_1.py::TestOWScatterPlot::test_time_axis\nPASSED r2e_tests/test_1.py::TestOWScatterPlot::test_tooltip\nPASSED r2e_tests/test_1.py::TestOWScatterPlot::test_update_regression_line_calls_add_line\nPASSED r2e_tests/test_1.py::TestOWScatterPlot::test_update_regression_line_is_called\nPASSED r2e_tests/test_1.py::TestOWScatterPlot::test_visual_settings\nPASSED r2e_tests/test_1.py::TestOWScatterPlot::test_vizrank\nPASSED r2e_tests/test_1.py::TestOWScatterPlot::test_vizrank_class_nan\nPASSED r2e_tests/test_1.py::TestOWScatterPlot::test_vizrank_enabled\nPASSED r2e_tests/test_1.py::TestOWScatterPlot::test_vizrank_enabled_color_var_nans\nPASSED r2e_tests/test_1.py::TestOWScatterPlot::test_vizrank_enabled_constant_data\nPASSED r2e_tests/test_1.py::TestOWScatterPlot::test_vizrank_enabled_no_color_var\nPASSED r2e_tests/test_1.py::TestOWScatterPlot::test_vizrank_enabled_no_data\nPASSED r2e_tests/test_1.py::TestOWScatterPlot::test_vizrank_enabled_sparse_data\nPASSED r2e_tests/test_1.py::TestOWScatterPlot::test_vizrank_enabled_two_features\nPASSED r2e_tests/test_1.py::TestOWScatterPlot::test_vizrank_nonprimitives\nPASSED r2e_tests/test_1.py::TestOWScatterPlot::test_vizrank_receives_manual_change\nSKIPPED [1] .venv/lib/python3.7/site-packages/orangewidget/tests/base.py:244: image exporting not tested as .widget was not set\nSKIPPED [1] .venv/lib/python3.7/site-packages/orangewidget/tests/base.py:238: minimum size not tested as .widget was not set\nSKIPPED [1] .venv/lib/python3.7/site-packages/orangewidget/tests/base.py:250: msg base class not tested as .widget was not set\nFAILED r2e_tests/test_1.py::TestOWScatterPlot::test_invalidated_same_time_features_first\n=================== 1 failed, 75 passed, 3 skipped in 12.95s ===================\nWidgets left: 0    Max widgets: 23511 \n\n",
    "old_commit_res_stderr": ""
}
Orange/widgets/visualize/owscatterplot.py Orange/widgets/visualize/tests/test_owscatterplot.py
We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
[ISSUE]
**Title:** Scatter Plot does not update when input attributes change simultaneously

**Description:**
When updating the Scatter Plot widget by providing new input attributes and data at the same time, the plot fails to refresh with the updated attributes. This results in the visualization not reflecting the new attributes, causing inconsistent or stale plots.

**Example Code:**
```python
# Simulate setting new features and data simultaneously
features = data.domain.attributes[:2]
signals = [
    (widget.Inputs.features, AttributeList(features)),
    (widget.Inputs.data, data)
]
send_signals(signals)

# Expect the plot to update with the new features
```

**Expected Behavior:**
The Scatter Plot should replot using the new input attributes, updating the visualization to accurately reflect the changes.

**Actual Behavior:**
The plot does not update when features and data are set at the same time, leaving the visualization unchanged.

[/ISSUE]
--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.

--- BEGIN FILE ---
```
### Orange/widgets/visualize/owscatterplot.py
from itertools import chain
from xml.sax.saxutils import escape

import numpy as np
from scipy.stats import linregress
from sklearn.neighbors import NearestNeighbors
from sklearn.metrics import r2_score

from AnyQt.QtCore import Qt, QTimer, QPointF, Signal
from AnyQt.QtGui import QColor, QFont
from AnyQt.QtWidgets import QGroupBox, QPushButton

import pyqtgraph as pg

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, 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, \
    ScatterBaseParameterSetter
from Orange.widgets.visualize.utils import VizRankDialogAttrPair
from Orange.widgets.visualize.utils.customizableplot import Updater
from Orange.widgets.visualize.utils.widget import OWDataProjectionWidget
from Orange.widgets.widget import AttributeList, Msg, Input, Output


class ScatterPlotVizRank(VizRankDialogAttrPair):
    captionTitle = "Score Plots"
    minK = 10
    attr_color = None

    def __init__(self, master):
        super().__init__(master)
        self.attr_color = self.master.attr_color

    def initialize(self):
        self.attr_color = self.master.attr_color
        super().initialize()

    def check_preconditions(self):
        self.Information.add_message(
            "color_required", "Color variable is not selected")
        self.Information.color_required.clear()
        if not super().check_preconditions():
            return False
        if not self.attr_color:
            self.Information.color_required()
            return False
        return True

    def iterate_states(self, initial_state):
        # If we put initialization of `self.attrs` to `initialize`,
        # `score_heuristic` would be run on every call to `set_data`.
        if initial_state is None:  # on the first call, compute order
            self.attrs = self.score_heuristic()
        yield from super().iterate_states(initial_state)

    def compute_score(self, state):
        # pylint: disable=invalid-unary-operand-type
        attrs = [self.attrs[i] for i in state]
        data = self.master.data
        data = data.transform(Domain(attrs, self.attr_color))
        data = data[~np.isnan(data.X).any(axis=1) & ~np.isnan(data.Y).T]
        if len(data) < self.minK:
            return None
        n_neighbors = min(self.minK, len(data) - 1)
        knn = NearestNeighbors(n_neighbors=n_neighbors).fit(data.X)
        ind = knn.kneighbors(return_distance=False)
        if data.domain.has_discrete_class:
            return -np.sum(data.Y[ind] == data.Y.reshape(-1, 1)) / \
                   n_neighbors / len(data.Y)
        else:
            return -r2_score(data.Y, np.mean(data.Y[ind], axis=1)) * \
                   (len(data.Y) / len(self.master.data))

    def bar_length(self, score):
        return max(0, -score)

    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_primitive()]
        domain = Domain(attributes=vars, class_vars=self.attr_color)
        data = self.master.data.transform(domain)
        relief = ReliefF if isinstance(domain.class_var, DiscreteVariable) \
            else RReliefF
        weights = relief(n_iterations=100, k_nearest=self.minK)(data)
        attrs = sorted(zip(weights, domain.attributes),
                       key=lambda x: (-x[0], x[1].name))
        return [a for _, a in attrs]


class ParameterSetter(ScatterBaseParameterSetter):
    DEFAULT_LINE_WIDTH = 3
    DEFAULT_LINE_ALPHA = 255

    def __init__(self, master):
        super().__init__(master)
        self.reg_line_label_font = QFont()
        self.reg_line_settings = {
            Updater.WIDTH_LABEL: self.DEFAULT_LINE_WIDTH,
            Updater.ALPHA_LABEL: self.DEFAULT_LINE_ALPHA,
            Updater.STYLE_LABEL: Updater.DEFAULT_LINE_STYLE,
        }

    def update_setters(self):
        super().update_setters()
        self.initial_settings[self.LABELS_BOX].update({
            self.AXIS_TITLE_LABEL: self.FONT_SETTING,
            self.AXIS_TICKS_LABEL: self.FONT_SETTING,
            self.LINE_LAB_LABEL: self.FONT_SETTING
        })
        self.initial_settings[self.PLOT_BOX] = {}
        self.initial_settings[self.PLOT_BOX][self.LINE_LABEL] = {
            Updater.WIDTH_LABEL: (range(1, 10), self.DEFAULT_LINE_WIDTH),
            Updater.ALPHA_LABEL: (range(0, 255, 5), self.DEFAULT_LINE_ALPHA),
            Updater.STYLE_LABEL: (list(Updater.LINE_STYLES),
                                  Updater.DEFAULT_LINE_STYLE),
        }

        def update_lines(**settings):
            self.reg_line_settings.update(**settings)
            Updater.update_inf_lines(self.reg_line_items,
                                     **self.reg_line_settings)
            self.master.update_reg_line_label_colors()

        def update_line_label(**settings):
            self.reg_line_label_font = \
                Updater.change_font(self.reg_line_label_font, settings)
            Updater.update_label_font(self.reg_line_label_items,
                                      self.reg_line_label_font)

        self._setters[self.LABELS_BOX][self.LINE_LAB_LABEL] = update_line_label
        self._setters[self.PLOT_BOX] = {self.LINE_LABEL: update_lines}

    @property
    def axis_items(self):
        return [value["item"] for value in
                self.master.plot_widget.plotItem.axes.values()]

    @property
    def reg_line_items(self):
        return self.master.reg_line_items

    @property
    def reg_line_label_items(self):
        return [line.label for line in self.master.reg_line_items
                if hasattr(line, "label")]


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)
        self.parameter_setter = ParameterSetter(self)
        self.reg_line_items = []

    def clear(self):
        super().clear()
        self.reg_line_items.clear()

    def update_coordinates(self):
        super().update_coordinates()
        self.update_axes()
        # Don't update_regression line here: update_coordinates is always
        # followed by update_point_props, which calls update_colors

    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, 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=var or "")
            if not var:
                self.plot_widget.hideAxis(axis)

    @staticmethod
    def _orthonormal_line(x, y, color, width, style=Qt.SolidLine):
        # https://en.wikipedia.org/wiki/Deming_regression, with δ=0.
        pen = pg.mkPen(color=color, width=width, style=style)
        xm = np.mean(x)
        ym = np.mean(y)
        sxx, sxy, _, syy = np.cov(x, y, ddof=1).flatten()

        if sxy != 0:  # also covers sxx != 0 and syy != 0
            slope = (syy - sxx + np.sqrt((syy - sxx) ** 2 + 4 * sxy ** 2)) \
                    / (2 * sxy)
            intercept = ym - slope * xm
            xmin = x.min()
            return pg.InfiniteLine(
                QPointF(xmin, xmin * slope + intercept),
                np.degrees(np.arctan(slope)),
                pen)
        elif (sxx == 0) == (syy == 0):  # both zero or non-zero -> can't draw
            return None
        elif sxx != 0:
            return pg.InfiniteLine(QPointF(x.min(), ym), 0, pen)
        else:
            return pg.InfiniteLine(QPointF(xm, y.min()), 90, pen)

    @staticmethod
    def _regression_line(x, y, color, width, style=Qt.SolidLine):
        min_x, max_x = np.min(x), np.max(x)
        if min_x == max_x:
            return None
        slope, intercept, rvalue, _, _ = linregress(x, y)
        angle = np.degrees(np.arctan(slope))
        start_y = min_x * slope + intercept
        rotate = 135 < angle % 360 < 315
        l_opts = dict(color=color, position=abs(rotate - 0.85),
                      rotateAxis=(1, 0), movable=True)
        reg_line_item = pg.InfiniteLine(
            pos=QPointF(min_x, start_y), angle=angle,
            pen=pg.mkPen(color=color, width=width, style=style),
            label=f"r = {rvalue:.2f}", labelOpts=l_opts)
        if rotate:
            reg_line_item.label.angle = 180
            reg_line_item.label.updateTransform()
        return reg_line_item

    def _add_line(self, x, y, color):
        width = self.parameter_setter.reg_line_settings[Updater.WIDTH_LABEL]
        alpha = self.parameter_setter.reg_line_settings[Updater.ALPHA_LABEL]
        style = self.parameter_setter.reg_line_settings[Updater.STYLE_LABEL]
        style = Updater.LINE_STYLES[style]
        color.setAlpha(alpha)
        if self.orthonormal_regression:
            line = self._orthonormal_line(x, y, color, width, style)
        else:
            line = self._regression_line(x, y, color, width, style)
        if line is None:
            return
        self.plot_widget.addItem(line)
        self.reg_line_items.append(line)

        if hasattr(line, "label"):
            Updater.update_label_font(
                [line.label], self.parameter_setter.reg_line_label_font
            )

    def update_reg_line_label_colors(self):
        for line in self.reg_line_items:
            if hasattr(line, "label"):
                color = 0.0 if self.class_density \
                    else line.pen.color().darker(175)
                line.label.setColor(color)

    def update_density(self):
        super().update_density()
        self.update_reg_line_label_colors()

    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
                and self.master.can_draw_regresssion_line()):
            return
        x, y = self.master.get_coordinates_data()
        if x is None:
            return
        self._add_line(x, y, QColor("#505050"))
        if self.master.is_continuous_color() or self.palette is None:
            return
        c_data = self.master.get_color_data()
        if c_data is None:
            return
        c_data = c_data.astype(int)
        for val in range(c_data.max() + 1):
            mask = c_data == val
            if mask.sum() > 1:
                self._add_line(x[mask], y[mask], self.palette[val].darker(135))
        self.update_reg_line_label_colors()


class OWScatterPlot(OWDataProjectionWidget):
    """Scatterplot visualization with explorative analysis and intelligent
    data visualization enhancements."""

    name = 'Scatter Plot'
    description = "Interactive scatter plot visualization with " \
                  "intelligent data visualization enhancements."
    icon = "icons/ScatterPlot.svg"
    priority = 140
    keywords = []

    class Inputs(OWDataProjectionWidget.Inputs):
        features = Input("Features", AttributeList)

    class Outputs(OWDataProjectionWidget.Outputs):
        features = Output("Features", AttributeList, dynamic=False)

    settings_version = 5
    auto_sample = Setting(True)
    attr_x = ContextSetting(None)
    attr_y = ContextSetting(None)
    tooltip_shows_all = Setting(True)

    GRAPH_CLASS = OWScatterPlotGraph
    graph = SettingProvider(OWScatterPlotGraph)
    embedding_variables_names = None

    xy_changed_manually = Signal(Variable, Variable)

    class Warning(OWDataProjectionWidget.Warning):
        missing_coords = Msg(
            "Plot cannot be displayed because '{}' or '{}' "
            "is missing for all data points.")

    class Information(OWDataProjectionWidget.Information):
        sampled_sql = Msg("Large SQL table; showing a sample.")
        missing_coords = Msg(
            "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)
        self.__timer.timeout.connect(self.add_data)
        super().__init__()

        # manually register Matplotlib file writers
        self.graph_writers = self.graph_writers.copy()
        for w in [MatplotlibFormat, MatplotlibPDFFormat]:
            self.graph_writers.append(w)

    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,
             self.gui.RegressionLine],
            self._plot_box)
        gui.checkBox(
            self._plot_box, self,
            value="graph.orthonormal_regression",
            label="Treat variables as independent",
            callback=self.graph.update_regression_line,
            tooltip=
            "If checked, fit line to group (minimize distance from points);\n"
            "otherwise fit y as a function of x (minimize vertical distances)",
            disabledBy=self.cb_reg_line)

    def _add_controls_axis(self):
        common_options = dict(
            labelWidth=50, orientation=Qt.Horizontal, sendSelectedValue=True,
            contentsLength=12, searchable=True
        )
        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=dmod.PRIMITIVE)
        self.cb_attr_x = gui.comboBox(
            self.attr_box, self, "attr_x", label="Axis x:",
            callback=self.set_attr_from_combo,
            model=self.xy_model, **common_options,
        )
        self.cb_attr_y = gui.comboBox(
            self.attr_box, self, "attr_y", label="Axis y:",
            callback=self.set_attr_from_combo,
            model=self.xy_model, **common_options,
        )
        vizrank_box = gui.hBox(self.attr_box)
        self.vizrank, self.vizrank_button = ScatterPlotVizRank.add_vizrank(
            vizrank_box, self, "Find Informative Projections", self.set_attr)

    def _add_controls_sampling(self):
        self.sampling = gui.auto_commit(
            self.controlArea, self, "auto_sample", "Sample", box="Sampling",
            callback=self.switch_sampling, commit=lambda: self.add_data(1))
        self.sampling.setVisible(False)

    @property
    def effective_variables(self):
        return [self.attr_x, self.attr_y] if self.attr_x and self.attr_y else []

    @property
    def effective_data(self):
        eff_var = self.effective_variables
        if eff_var and self.attr_x.name == self.attr_y.name:
            eff_var = [self.attr_x]
        return self.data.transform(Domain(eff_var))

    def _vizrank_color_change(self):
        self.vizrank.initialize()
        err_msg = ""
        if self.data is None:
            err_msg = "No data on input"
        elif self.data.is_sparse():
            err_msg = "Data is sparse"
        elif len(self.xy_model) < 3:
            err_msg = "Not enough features for ranking"
        elif self.attr_color is None:
            err_msg = "Color variable is not selected"
        elif np.isnan(self.data.get_column_view(
                self.attr_color)[0].astype(float)).all():
            err_msg = "Color variable has no values"
        self.vizrank_button.setEnabled(not err_msg)
        self.vizrank_button.setToolTip(err_msg)

    @OWDataProjectionWidget.Inputs.data
    def set_data(self, data):
        super().set_data(data)
        self._vizrank_color_change()

        def findvar(name, iterable):
            """Find a Orange.data.Variable in `iterable` by name"""
            for el in iterable:
                if isinstance(el, Variable) and el.name == name:
                    return el
            return None

        # handle restored settings from  < 3.3.9 when attr_* were stored
        # by name
        if isinstance(self.attr_x, str):
            self.attr_x = findvar(self.attr_x, self.xy_model)
        if isinstance(self.attr_y, str):
            self.attr_y = findvar(self.attr_y, self.xy_model)
        if isinstance(self.attr_label, str):
            self.attr_label = findvar(self.attr_label, self.gui.label_model)
        if isinstance(self.attr_color, str):
            self.attr_color = findvar(self.attr_color, self.gui.color_model)
        if isinstance(self.attr_shape, str):
            self.attr_shape = findvar(self.attr_shape, self.gui.shape_model)
        if isinstance(self.attr_size, str):
            self.attr_size = findvar(self.attr_size, self.gui.size_model)

    def check_data(self):
        super().check_data()
        self.__timer.stop()
        self.sampling.setVisible(False)
        self.sql_data = None
        if isinstance(self.data, SqlTable):
            if self.data.approx_len() < 4000:
                self.data = Table(self.data)
            else:
                self.Information.sampled_sql()
                self.sql_data = self.data
                data_sample = self.data.sample_time(0.8, no_cache=True)
                data_sample.download_data(2000, partial=True)
                self.data = Table(data_sample)
                self.sampling.setVisible(True)
                if self.auto_sample:
                    self.__timer.start()

        if self.data is not None and (len(self.data) == 0 or
                                      len(self.data.domain.variables) == 0):
            self.data = None

    def get_embedding(self):
        self.valid_data = None
        if self.data is None:
            return None

        x_data = self.get_column(self.attr_x, filter_valid=False)
        y_data = self.get_column(self.attr_y, filter_valid=False)
        if x_data is None or y_data is None:
            return None

        self.Warning.missing_coords.clear()
        self.Information.missing_coords.clear()
        self.valid_data = np.isfinite(x_data) & np.isfinite(y_data)
        if self.valid_data is not None and not np.all(self.valid_data):
            msg = self.Information if np.any(self.valid_data) else self.Warning
            msg.missing_coords(self.attr_x.name, self.attr_y.name)
        return np.vstack((x_data, y_data)).T

    # Tooltip
    def _point_tooltip(self, point_id, skip_attrs=()):
        point_data = self.data[point_id]
        xy_attrs = (self.attr_x, self.attr_y)
        text = "<br/>".join(
            escape('{} = {}'.format(var.name, point_data[var]))
            for var in xy_attrs)
        if self.tooltip_shows_all:
            others = super()._point_tooltip(point_id, skip_attrs=xy_attrs)
            if others:
                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()
            return
        data_sample = self.sql_data.sample_time(time, no_cache=True)
        if data_sample:
            data_sample.download_data(2000, partial=True)
            data = Table(data_sample)
            self.data = Table.concatenate((self.data, data), axis=0)
            self.handleNewSignals()

    def init_attr_values(self):
        super().init_attr_values()
        data = self.data
        domain = data.domain if data and len(data) else None
        self.xy_model.set_domain(domain)
        self.attr_x = self.xy_model[0] if self.xy_model else None
        self.attr_y = self.xy_model[1] if len(self.xy_model) >= 2 \
            else self.attr_x

    def switch_sampling(self):
        self.__timer.stop()
        if self.auto_sample and self.sql_data:
            self.add_data()
            self.__timer.start()

    @OWDataProjectionWidget.Inputs.data_subset
    def set_subset_data(self, subset_data):
        self.warning()
        if isinstance(subset_data, SqlTable):
            if subset_data.approx_len() < AUTO_DL_LIMIT:
                subset_data = Table(subset_data)
            else:
                self.warning("Data subset does not support large Sql tables")
                subset_data = None
        super().set_subset_data(subset_data)

    # called when all signals are received, so the graph is updated only once
    def handleNewSignals(self):
        self.attr_box.setEnabled(True)
        self.vizrank.setEnabled(True)
        if self.attribute_selection_list and self.data is not None and \
                self.data.domain is not None and \
                all(attr in self.data.domain for attr
                        in self.attribute_selection_list):
            self.attr_x, self.attr_y = self.attribute_selection_list[:2]
            self.attr_box.setEnabled(False)
            self.vizrank.setEnabled(False)
        super().handleNewSignals()
        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):
        if attributes and len(attributes) >= 2:
            self.attribute_selection_list = attributes[:2]
            self._invalidated = self._invalidated \
                or self.attr_x != attributes[0] \
                or self.attr_y != attributes[1]
        else:
            self.attribute_selection_list = None

    def set_attr(self, attr_x, attr_y):
        if attr_x != self.attr_x or attr_y != self.attr_y:
            self.attr_x, self.attr_y = attr_x, attr_y
            self.attr_changed()

    def set_attr_from_combo(self):
        self.attr_changed()
        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.deferred()

    def get_axes(self):
        return {"bottom": self.attr_x, "left": self.attr_y}

    def colors_changed(self):
        super().colors_changed()
        self._vizrank_color_change()

    @gui.deferred
    def commit(self):
        super().commit()
        self.send_features()

    def send_features(self):
        features = [attr for attr in [self.attr_x, self.attr_y] if attr]
        self.Outputs.features.send(AttributeList(features) or None)

    def get_widget_name_extension(self):
        if self.data is not None:
            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"]:
            settings["selection_group"] = [(a, 1) for a in settings["selection"]]
        if version < 3:
            if "auto_send_selection" in settings:
                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):
        values = context.values
        if version < 3:
            values["attr_color"] = values["graph"]["attr_color"]
            values["attr_size"] = values["graph"]["attr_size"]
            values["attr_shape"] = values["graph"]["attr_shape"]
            values["attr_label"] = values["graph"]["attr_label"]
        if version < 4:
            if values["attr_x"][1] % 100 == 1 or values["attr_y"][1] % 100 == 1:
                raise IncompatibleContext()


if __name__ == "__main__":  # pragma: no cover
    table = Table("iris")
    WidgetPreview(OWScatterPlot).run(set_data=table,
                                     set_subset_data=table[:30])

```
--- END FILE ---

Please first localize the bug based on the issue statement, and then generate *SEARCH/REPLACE* edits to fix the issue.

Every *SEARCH/REPLACE* edit must use this format:
1. The file path
2. The start of search block: <<<<<<< SEARCH
3. A contiguous chunk of lines to search for in the existing source code
4. The dividing line: =======
5. The lines to replace into the source code
6. The end of the replace block: >>>>>>> REPLACE

Here is an example:

```python
### mathweb/flask/app.py
<<<<<<< SEARCH
from flask import Flask
=======
import math
from flask import Flask
>>>>>>> REPLACE
```
</solution>

Please note that the *SEARCH/REPLACE* edit REQUIRES PROPER INDENTATION. If you would like to add the line '        print(x)', you must fully write that out, with all those spaces before the code!
Wrap each *SEARCH/REPLACE* edit in a code block as shown in the example above. If you have multiple *SEARCH/REPLACE* edits, use a separate code block for each one.
[ISSUE]
**Title:** Scatter Plot does not update when input attributes change simultaneously

**Description:**
When updating the Scatter Plot widget by providing new input attributes and data at the same time, the plot fails to refresh with the updated attributes. This results in the visualization not reflecting the new attributes, causing inconsistent or stale plots.

**Example Code:**
```python
# Simulate setting new features and data simultaneously
features = data.domain.attributes[:2]
signals = [
    (widget.Inputs.features, AttributeList(features)),
    (widget.Inputs.data, data)
]
send_signals(signals)

# Expect the plot to update with the new features
```

**Expected Behavior:**
The Scatter Plot should replot using the new input attributes, updating the visualization to accurately reflect the changes.

**Actual Behavior:**
The plot does not update when features and data are set at the same time, leaving the visualization unchanged.

[/ISSUE]
{
    "TestOWScatterPlot.test_add_line_calls_proper_regressor": "PASSED",
    "TestOWScatterPlot.test_attr_label_metas": "PASSED",
    "TestOWScatterPlot.test_attr_models": "PASSED",
    "TestOWScatterPlot.test_auto_send_selection": "PASSED",
    "TestOWScatterPlot.test_class_density": "PASSED",
    "TestOWScatterPlot.test_color_is_optional": "PASSED",
    "TestOWScatterPlot.test_data_column_infs": "PASSED",
    "TestOWScatterPlot.test_data_column_nans": "PASSED",
    "TestOWScatterPlot.test_datasets": "PASSED",
    "TestOWScatterPlot.test_default_attrs": "PASSED",
    "TestOWScatterPlot.test_dragging_tooltip": "PASSED",
    "TestOWScatterPlot.test_error_message": "PASSED",
    "TestOWScatterPlot.test_features_and_data": "PASSED",
    "TestOWScatterPlot.test_features_and_no_data": "PASSED",
    "TestOWScatterPlot.test_group_selections": "PASSED",
    "TestOWScatterPlot.test_handle_metas": "PASSED",
    "TestOWScatterPlot.test_handle_primitive_metas": "PASSED",
    "TestOWScatterPlot.test_hidden_effective_variables": "PASSED",
    "TestOWScatterPlot.test_image_export": "PASSED",
    "TestOWScatterPlot.test_invalid_points_selection": "PASSED",
    "TestOWScatterPlot.test_invalidated_diff_features": "PASSED",
    "TestOWScatterPlot.test_invalidated_diff_features_features_first": "PASSED",
    "TestOWScatterPlot.test_invalidated_diff_features_same_time": "PASSED",
    "TestOWScatterPlot.test_invalidated_diff_features_same_time_features_first": "PASSED",
    "TestOWScatterPlot.test_invalidated_embedding": "PASSED",
    "TestOWScatterPlot.test_invalidated_features_first": "PASSED",
    "TestOWScatterPlot.test_invalidated_same_features": "PASSED",
    "TestOWScatterPlot.test_invalidated_same_time": "PASSED",
    "TestOWScatterPlot.test_invalidated_same_time_features_first": "PASSED",
    "TestOWScatterPlot.test_jittering": "PASSED",
    "TestOWScatterPlot.test_many_discrete_values": "PASSED",
    "TestOWScatterPlot.test_metas_zero_column": "PASSED",
    "TestOWScatterPlot.test_migrate_selection": "PASSED",
    "TestOWScatterPlot.test_minimum_size": "PASSED",
    "TestOWScatterPlot.test_msg_base_class": "PASSED",
    "TestOWScatterPlot.test_no_regression_line": "PASSED",
    "TestOWScatterPlot.test_none_data": "PASSED",
    "TestOWScatterPlot.test_on_manual_change": "PASSED",
    "TestOWScatterPlot.test_opacity_warning": "PASSED",
    "TestOWScatterPlot.test_optional_combos": "PASSED",
    "TestOWScatterPlot.test_orthonormal_line": "PASSED",
    "TestOWScatterPlot.test_output_features": "PASSED",
    "TestOWScatterPlot.test_outputs": "PASSED",
    "TestOWScatterPlot.test_plot_once": "PASSED",
    "TestOWScatterPlot.test_points_combo_boxes": "PASSED",
    "TestOWScatterPlot.test_points_selection": "PASSED",
    "TestOWScatterPlot.test_regression_line": "PASSED",
    "TestOWScatterPlot.test_regression_line_coeffs": "PASSED",
    "TestOWScatterPlot.test_regression_lines_appear": "PASSED",
    "TestOWScatterPlot.test_report_on_empty": "PASSED",
    "TestOWScatterPlot.test_saved_selection": "PASSED",
    "TestOWScatterPlot.test_saving_selection": "PASSED",
    "TestOWScatterPlot.test_score_heuristics": "PASSED",
    "TestOWScatterPlot.test_send_report": "PASSED",
    "TestOWScatterPlot.test_set_data": "PASSED",
    "TestOWScatterPlot.test_set_strings_settings": "PASSED",
    "TestOWScatterPlot.test_setup_graph": "PASSED",
    "TestOWScatterPlot.test_sparse_data": "PASSED",
    "TestOWScatterPlot.test_subset_data": "PASSED",
    "TestOWScatterPlot.test_subset_data_color": "PASSED",
    "TestOWScatterPlot.test_time_axis": "PASSED",
    "TestOWScatterPlot.test_tooltip": "PASSED",
    "TestOWScatterPlot.test_update_regression_line_calls_add_line": "PASSED",
    "TestOWScatterPlot.test_update_regression_line_is_called": "PASSED",
    "TestOWScatterPlot.test_visual_settings": "PASSED",
    "TestOWScatterPlot.test_vizrank": "PASSED",
    "TestOWScatterPlot.test_vizrank_class_nan": "PASSED",
    "TestOWScatterPlot.test_vizrank_enabled": "PASSED",
    "TestOWScatterPlot.test_vizrank_enabled_color_var_nans": "PASSED",
    "TestOWScatterPlot.test_vizrank_enabled_constant_data": "PASSED",
    "TestOWScatterPlot.test_vizrank_enabled_no_color_var": "PASSED",
    "TestOWScatterPlot.test_vizrank_enabled_no_data": "PASSED",
    "TestOWScatterPlot.test_vizrank_enabled_sparse_data": "PASSED",
    "TestOWScatterPlot.test_vizrank_enabled_two_features": "PASSED",
    "TestOWScatterPlot.test_vizrank_nonprimitives": "PASSED",
    "TestOWScatterPlot.test_vizrank_receives_manual_change": "PASSED"
}
diff --git a/Orange/widgets/visualize/owscatterplot.py b/Orange/widgets/visualize/owscatterplot.py
index d877d0c72..8a12ea558 100644
--- a/Orange/widgets/visualize/owscatterplot.py
+++ b/Orange/widgets/visualize/owscatterplot.py
@@ -358,6 +358,7 @@ class OWScatterPlot(OWDataProjectionWidget):
         self.vizrank: ScatterPlotVizRank = None
         self.vizrank_button: QPushButton = None
         self.sampling: QGroupBox = None
+        self._xy_invalidated: bool = True
 
         self.sql_data = None  # Orange.data.sql.table.SqlTable
         self.attribute_selection_list = None  # list of Orange.data.Variable
@@ -581,6 +582,8 @@ class OWScatterPlot(OWDataProjectionWidget):
             self.attr_x, self.attr_y = self.attribute_selection_list[:2]
             self.attr_box.setEnabled(False)
             self.vizrank.setEnabled(False)
+        self._invalidated = self._invalidated or self._xy_invalidated
+        self._xy_invalidated = False
         super().handleNewSignals()
         if self._domain_invalidated:
             self.graph.update_axes()
@@ -591,7 +594,7 @@ class OWScatterPlot(OWDataProjectionWidget):
     def set_shown_attributes(self, attributes):
         if attributes and len(attributes) >= 2:
             self.attribute_selection_list = attributes[:2]
-            self._invalidated = self._invalidated \
+            self._xy_invalidated = self._xy_invalidated \
                 or self.attr_x != attributes[0] \
                 or self.attr_y != attributes[1]
         else:

{"Orange/widgets/visualize/owscatterplot.py": "from itertools import chain\nfrom xml.sax.saxutils import escape\n\nimport numpy as np\nfrom scipy.stats import linregress\nfrom sklearn.neighbors import NearestNeighbors\nfrom sklearn.metrics import r2_score\n\nfrom AnyQt.QtCore import Qt, QTimer, QPointF, Signal\nfrom AnyQt.QtGui import QColor, QFont\nfrom AnyQt.QtWidgets import QGroupBox, QPushButton\n\nimport pyqtgraph as pg\n\nfrom orangewidget.utils.combobox import ComboBoxSearch\n\nfrom Orange.data import Table, Domain, DiscreteVariable, Variable\nfrom Orange.data.sql.table import SqlTable, AUTO_DL_LIMIT\nfrom Orange.preprocess.score import ReliefF, RReliefF\n\nfrom Orange.widgets import gui, report\nfrom Orange.widgets.io import MatplotlibFormat, MatplotlibPDFFormat\nfrom Orange.widgets.settings import (\n    Setting, ContextSetting, SettingProvider, IncompatibleContext)\nfrom Orange.widgets.utils import get_variable_values_sorted\nfrom Orange.widgets.utils.itemmodels import DomainModel\nfrom Orange.widgets.utils.widgetpreview import WidgetPreview\nfrom Orange.widgets.visualize.owscatterplotgraph import OWScatterPlotBase, \\\n    ScatterBaseParameterSetter\nfrom Orange.widgets.visualize.utils import VizRankDialogAttrPair\nfrom Orange.widgets.visualize.utils.customizableplot import Updater\nfrom Orange.widgets.visualize.utils.widget import OWDataProjectionWidget\nfrom Orange.widgets.widget import AttributeList, Msg, Input, Output\n\n\nclass ScatterPlotVizRank(VizRankDialogAttrPair):\n    captionTitle = \"Score Plots\"\n    minK = 10\n    attr_color = None\n\n    def __init__(self, master):\n        super().__init__(master)\n        self.attr_color = self.master.attr_color\n\n    def initialize(self):\n        self.attr_color = self.master.attr_color\n        super().initialize()\n\n    def check_preconditions(self):\n        self.Information.add_message(\n            \"color_required\", \"Color variable is not selected\")\n        self.Information.color_required.clear()\n        if not super().check_preconditions():\n            return False\n        if not self.attr_color:\n            self.Information.color_required()\n            return False\n        return True\n\n    def iterate_states(self, initial_state):\n        # If we put initialization of `self.attrs` to `initialize`,\n        # `score_heuristic` would be run on every call to `set_data`.\n        if initial_state is None:  # on the first call, compute order\n            self.attrs = self.score_heuristic()\n        yield from super().iterate_states(initial_state)\n\n    def compute_score(self, state):\n        # pylint: disable=invalid-unary-operand-type\n        attrs = [self.attrs[i] for i in state]\n        data = self.master.data\n        data = data.transform(Domain(attrs, self.attr_color))\n        data = data[~np.isnan(data.X).any(axis=1) & ~np.isnan(data.Y).T]\n        if len(data) < self.minK:\n            return None\n        n_neighbors = min(self.minK, len(data) - 1)\n        knn = NearestNeighbors(n_neighbors=n_neighbors).fit(data.X)\n        ind = knn.kneighbors(return_distance=False)\n        if data.domain.has_discrete_class:\n            return -np.sum(data.Y[ind] == data.Y.reshape(-1, 1)) / \\\n                   n_neighbors / len(data.Y)\n        else:\n            return -r2_score(data.Y, np.mean(data.Y[ind], axis=1)) * \\\n                   (len(data.Y) / len(self.master.data))\n\n    def bar_length(self, score):\n        return max(0, -score)\n\n    def score_heuristic(self):\n        assert self.attr_color is not None\n        master_domain = self.master.data.domain\n        vars = [v for v in chain(master_domain.variables, master_domain.metas)\n                if v is not self.attr_color and v.is_primitive()]\n        domain = Domain(attributes=vars, class_vars=self.attr_color)\n        data = self.master.data.transform(domain)\n        relief = ReliefF if isinstance(domain.class_var, DiscreteVariable) \\\n            else RReliefF\n        weights = relief(n_iterations=100, k_nearest=self.minK)(data)\n        attrs = sorted(zip(weights, domain.attributes),\n                       key=lambda x: (-x[0], x[1].name))\n        return [a for _, a in attrs]\n\n\nclass ParameterSetter(ScatterBaseParameterSetter):\n    DEFAULT_LINE_WIDTH = 3\n    DEFAULT_LINE_ALPHA = 255\n\n    def __init__(self, master):\n        super().__init__(master)\n        self.reg_line_label_font = QFont()\n        self.reg_line_settings = {\n            Updater.WIDTH_LABEL: self.DEFAULT_LINE_WIDTH,\n            Updater.ALPHA_LABEL: self.DEFAULT_LINE_ALPHA,\n            Updater.STYLE_LABEL: Updater.DEFAULT_LINE_STYLE,\n        }\n\n    def update_setters(self):\n        super().update_setters()\n        self.initial_settings[self.LABELS_BOX].update({\n            self.AXIS_TITLE_LABEL: self.FONT_SETTING,\n            self.AXIS_TICKS_LABEL: self.FONT_SETTING,\n            self.LINE_LAB_LABEL: self.FONT_SETTING\n        })\n        self.initial_settings[self.PLOT_BOX] = {}\n        self.initial_settings[self.PLOT_BOX][self.LINE_LABEL] = {\n            Updater.WIDTH_LABEL: (range(1, 10), self.DEFAULT_LINE_WIDTH),\n            Updater.ALPHA_LABEL: (range(0, 255, 5), self.DEFAULT_LINE_ALPHA),\n            Updater.STYLE_LABEL: (list(Updater.LINE_STYLES),\n                                  Updater.DEFAULT_LINE_STYLE),\n        }\n\n        def update_lines(**settings):\n            self.reg_line_settings.update(**settings)\n            Updater.update_inf_lines(self.reg_line_items,\n                                     **self.reg_line_settings)\n            self.master.update_reg_line_label_colors()\n\n        def update_line_label(**settings):\n            self.reg_line_label_font = \\\n                Updater.change_font(self.reg_line_label_font, settings)\n            Updater.update_label_font(self.reg_line_label_items,\n                                      self.reg_line_label_font)\n\n        self._setters[self.LABELS_BOX][self.LINE_LAB_LABEL] = update_line_label\n        self._setters[self.PLOT_BOX] = {self.LINE_LABEL: update_lines}\n\n    @property\n    def axis_items(self):\n        return [value[\"item\"] for value in\n                self.master.plot_widget.plotItem.axes.values()]\n\n    @property\n    def reg_line_items(self):\n        return self.master.reg_line_items\n\n    @property\n    def reg_line_label_items(self):\n        return [line.label for line in self.master.reg_line_items\n                if hasattr(line, \"label\")]\n\n\nclass OWScatterPlotGraph(OWScatterPlotBase):\n    show_reg_line = Setting(False)\n    orthonormal_regression = Setting(False)\n    jitter_continuous = Setting(False)\n\n    def __init__(self, scatter_widget, parent):\n        super().__init__(scatter_widget, parent)\n        self.parameter_setter = ParameterSetter(self)\n        self.reg_line_items = []\n\n    def clear(self):\n        super().clear()\n        self.reg_line_items.clear()\n\n    def update_coordinates(self):\n        super().update_coordinates()\n        self.update_axes()\n        # Don't update_regression line here: update_coordinates is always\n        # followed by update_point_props, which calls update_colors\n\n    def update_colors(self):\n        super().update_colors()\n        self.update_regression_line()\n\n    def jitter_coordinates(self, x, y):\n        def get_span(attr):\n            if attr.is_discrete:\n                # Assuming the maximal jitter size is 10, a span of 4 will\n                # jitter by 4 * 10 / 100 = 0.4, so there will be no overlap\n                return 4\n            elif self.jitter_continuous:\n                return None  # Let _jitter_data determine the span\n            else:\n                return 0  # No jittering\n        span_x = get_span(self.master.attr_x)\n        span_y = get_span(self.master.attr_y)\n        if self.jitter_size == 0 or (span_x == 0 and span_y == 0):\n            return x, y\n        return self._jitter_data(x, y, span_x, span_y)\n\n    def update_axes(self):\n        for axis, var in self.master.get_axes().items():\n            axis_item = self.plot_widget.plotItem.getAxis(axis)\n            if var and var.is_discrete:\n                ticks = [list(enumerate(get_variable_values_sorted(var)))]\n                axis_item.setTicks(ticks)\n            else:\n                axis_item.setTicks(None)\n            use_time = var and var.is_time\n            self.plot_widget.plotItem.getAxis(axis).use_time(use_time)\n            self.plot_widget.setLabel(axis=axis, text=var or \"\")\n            if not var:\n                self.plot_widget.hideAxis(axis)\n\n    @staticmethod\n    def _orthonormal_line(x, y, color, width, style=Qt.SolidLine):\n        # https://en.wikipedia.org/wiki/Deming_regression, with \u03b4=0.\n        pen = pg.mkPen(color=color, width=width, style=style)\n        xm = np.mean(x)\n        ym = np.mean(y)\n        sxx, sxy, _, syy = np.cov(x, y, ddof=1).flatten()\n\n        if sxy != 0:  # also covers sxx != 0 and syy != 0\n            slope = (syy - sxx + np.sqrt((syy - sxx) ** 2 + 4 * sxy ** 2)) \\\n                    / (2 * sxy)\n            intercept = ym - slope * xm\n            xmin = x.min()\n            return pg.InfiniteLine(\n                QPointF(xmin, xmin * slope + intercept),\n                np.degrees(np.arctan(slope)),\n                pen)\n        elif (sxx == 0) == (syy == 0):  # both zero or non-zero -> can't draw\n            return None\n        elif sxx != 0:\n            return pg.InfiniteLine(QPointF(x.min(), ym), 0, pen)\n        else:\n            return pg.InfiniteLine(QPointF(xm, y.min()), 90, pen)\n\n    @staticmethod\n    def _regression_line(x, y, color, width, style=Qt.SolidLine):\n        min_x, max_x = np.min(x), np.max(x)\n        if min_x == max_x:\n            return None\n        slope, intercept, rvalue, _, _ = linregress(x, y)\n        angle = np.degrees(np.arctan(slope))\n        start_y = min_x * slope + intercept\n        rotate = 135 < angle % 360 < 315\n        l_opts = dict(color=color, position=abs(rotate - 0.85),\n                      rotateAxis=(1, 0), movable=True)\n        reg_line_item = pg.InfiniteLine(\n            pos=QPointF(min_x, start_y), angle=angle,\n            pen=pg.mkPen(color=color, width=width, style=style),\n            label=f\"r = {rvalue:.2f}\", labelOpts=l_opts)\n        if rotate:\n            reg_line_item.label.angle = 180\n            reg_line_item.label.updateTransform()\n        return reg_line_item\n\n    def _add_line(self, x, y, color):\n        width = self.parameter_setter.reg_line_settings[Updater.WIDTH_LABEL]\n        alpha = self.parameter_setter.reg_line_settings[Updater.ALPHA_LABEL]\n        style = self.parameter_setter.reg_line_settings[Updater.STYLE_LABEL]\n        style = Updater.LINE_STYLES[style]\n        color.setAlpha(alpha)\n        if self.orthonormal_regression:\n            line = self._orthonormal_line(x, y, color, width, style)\n        else:\n            line = self._regression_line(x, y, color, width, style)\n        if line is None:\n            return\n        self.plot_widget.addItem(line)\n        self.reg_line_items.append(line)\n\n        if hasattr(line, \"label\"):\n            Updater.update_label_font(\n                [line.label], self.parameter_setter.reg_line_label_font\n            )\n\n    def update_reg_line_label_colors(self):\n        for line in self.reg_line_items:\n            if hasattr(line, \"label\"):\n                color = 0.0 if self.class_density \\\n                    else line.pen.color().darker(175)\n                line.label.setColor(color)\n\n    def update_density(self):\n        super().update_density()\n        self.update_reg_line_label_colors()\n\n    def update_regression_line(self):\n        for line in self.reg_line_items:\n            self.plot_widget.removeItem(line)\n        self.reg_line_items.clear()\n        if not (self.show_reg_line\n                and self.master.can_draw_regresssion_line()):\n            return\n        x, y = self.master.get_coordinates_data()\n        if x is None:\n            return\n        self._add_line(x, y, QColor(\"#505050\"))\n        if self.master.is_continuous_color() or self.palette is None:\n            return\n        c_data = self.master.get_color_data()\n        if c_data is None:\n            return\n        c_data = c_data.astype(int)\n        for val in range(c_data.max() + 1):\n            mask = c_data == val\n            if mask.sum() > 1:\n                self._add_line(x[mask], y[mask], self.palette[val].darker(135))\n        self.update_reg_line_label_colors()\n\n\nclass OWScatterPlot(OWDataProjectionWidget):\n    \"\"\"Scatterplot visualization with explorative analysis and intelligent\n    data visualization enhancements.\"\"\"\n\n    name = 'Scatter Plot'\n    description = \"Interactive scatter plot visualization with \" \\\n                  \"intelligent data visualization enhancements.\"\n    icon = \"icons/ScatterPlot.svg\"\n    priority = 140\n    keywords = []\n\n    class Inputs(OWDataProjectionWidget.Inputs):\n        features = Input(\"Features\", AttributeList)\n\n    class Outputs(OWDataProjectionWidget.Outputs):\n        features = Output(\"Features\", AttributeList, dynamic=False)\n\n    settings_version = 5\n    auto_sample = Setting(True)\n    attr_x = ContextSetting(None)\n    attr_y = ContextSetting(None)\n    tooltip_shows_all = Setting(True)\n\n    GRAPH_CLASS = OWScatterPlotGraph\n    graph = SettingProvider(OWScatterPlotGraph)\n    embedding_variables_names = None\n\n    xy_changed_manually = Signal(Variable, Variable)\n\n    class Warning(OWDataProjectionWidget.Warning):\n        missing_coords = Msg(\n            \"Plot cannot be displayed because '{}' or '{}' \"\n            \"is missing for all data points.\")\n\n    class Information(OWDataProjectionWidget.Information):\n        sampled_sql = Msg(\"Large SQL table; showing a sample.\")\n        missing_coords = Msg(\n            \"Points with missing '{}' or '{}' are not displayed\")\n\n    def __init__(self):\n        self.attr_box: QGroupBox = None\n        self.xy_model: DomainModel = None\n        self.cb_attr_x: ComboBoxSearch = None\n        self.cb_attr_y: ComboBoxSearch = None\n        self.vizrank: ScatterPlotVizRank = None\n        self.vizrank_button: QPushButton = None\n        self.sampling: QGroupBox = None\n\n        self.sql_data = None  # Orange.data.sql.table.SqlTable\n        self.attribute_selection_list = None  # list of Orange.data.Variable\n        self.__timer = QTimer(self, interval=1200)\n        self.__timer.timeout.connect(self.add_data)\n        super().__init__()\n\n        # manually register Matplotlib file writers\n        self.graph_writers = self.graph_writers.copy()\n        for w in [MatplotlibFormat, MatplotlibPDFFormat]:\n            self.graph_writers.append(w)\n\n    def _add_controls(self):\n        self._add_controls_axis()\n        self._add_controls_sampling()\n        super()._add_controls()\n        self.gui.add_widget(self.gui.JitterNumericValues, self._effects_box)\n        self.gui.add_widgets(\n            [self.gui.ShowGridLines,\n             self.gui.ToolTipShowsAll,\n             self.gui.RegressionLine],\n            self._plot_box)\n        gui.checkBox(\n            self._plot_box, self,\n            value=\"graph.orthonormal_regression\",\n            label=\"Treat variables as independent\",\n            callback=self.graph.update_regression_line,\n            tooltip=\n            \"If checked, fit line to group (minimize distance from points);\\n\"\n            \"otherwise fit y as a function of x (minimize vertical distances)\",\n            disabledBy=self.cb_reg_line)\n\n    def _add_controls_axis(self):\n        common_options = dict(\n            labelWidth=50, orientation=Qt.Horizontal, sendSelectedValue=True,\n            contentsLength=12, searchable=True\n        )\n        self.attr_box = gui.vBox(self.controlArea, 'Axes',\n                                 spacing=2 if gui.is_macstyle() else 8)\n        dmod = DomainModel\n        self.xy_model = DomainModel(dmod.MIXED, valid_types=dmod.PRIMITIVE)\n        self.cb_attr_x = gui.comboBox(\n            self.attr_box, self, \"attr_x\", label=\"Axis x:\",\n            callback=self.set_attr_from_combo,\n            model=self.xy_model, **common_options,\n        )\n        self.cb_attr_y = gui.comboBox(\n            self.attr_box, self, \"attr_y\", label=\"Axis y:\",\n            callback=self.set_attr_from_combo,\n            model=self.xy_model, **common_options,\n        )\n        vizrank_box = gui.hBox(self.attr_box)\n        self.vizrank, self.vizrank_button = ScatterPlotVizRank.add_vizrank(\n            vizrank_box, self, \"Find Informative Projections\", self.set_attr)\n\n    def _add_controls_sampling(self):\n        self.sampling = gui.auto_commit(\n            self.controlArea, self, \"auto_sample\", \"Sample\", box=\"Sampling\",\n            callback=self.switch_sampling, commit=lambda: self.add_data(1))\n        self.sampling.setVisible(False)\n\n    @property\n    def effective_variables(self):\n        return [self.attr_x, self.attr_y] if self.attr_x and self.attr_y else []\n\n    @property\n    def effective_data(self):\n        eff_var = self.effective_variables\n        if eff_var and self.attr_x.name == self.attr_y.name:\n            eff_var = [self.attr_x]\n        return self.data.transform(Domain(eff_var))\n\n    def _vizrank_color_change(self):\n        self.vizrank.initialize()\n        err_msg = \"\"\n        if self.data is None:\n            err_msg = \"No data on input\"\n        elif self.data.is_sparse():\n            err_msg = \"Data is sparse\"\n        elif len(self.xy_model) < 3:\n            err_msg = \"Not enough features for ranking\"\n        elif self.attr_color is None:\n            err_msg = \"Color variable is not selected\"\n        elif np.isnan(self.data.get_column_view(\n                self.attr_color)[0].astype(float)).all():\n            err_msg = \"Color variable has no values\"\n        self.vizrank_button.setEnabled(not err_msg)\n        self.vizrank_button.setToolTip(err_msg)\n\n    @OWDataProjectionWidget.Inputs.data\n    def set_data(self, data):\n        super().set_data(data)\n        self._vizrank_color_change()\n\n        def findvar(name, iterable):\n            \"\"\"Find a Orange.data.Variable in `iterable` by name\"\"\"\n            for el in iterable:\n                if isinstance(el, Variable) and el.name == name:\n                    return el\n            return None\n\n        # handle restored settings from  < 3.3.9 when attr_* were stored\n        # by name\n        if isinstance(self.attr_x, str):\n            self.attr_x = findvar(self.attr_x, self.xy_model)\n        if isinstance(self.attr_y, str):\n            self.attr_y = findvar(self.attr_y, self.xy_model)\n        if isinstance(self.attr_label, str):\n            self.attr_label = findvar(self.attr_label, self.gui.label_model)\n        if isinstance(self.attr_color, str):\n            self.attr_color = findvar(self.attr_color, self.gui.color_model)\n        if isinstance(self.attr_shape, str):\n            self.attr_shape = findvar(self.attr_shape, self.gui.shape_model)\n        if isinstance(self.attr_size, str):\n            self.attr_size = findvar(self.attr_size, self.gui.size_model)\n\n    def check_data(self):\n        super().check_data()\n        self.__timer.stop()\n        self.sampling.setVisible(False)\n        self.sql_data = None\n        if isinstance(self.data, SqlTable):\n            if self.data.approx_len() < 4000:\n                self.data = Table(self.data)\n            else:\n                self.Information.sampled_sql()\n                self.sql_data = self.data\n                data_sample = self.data.sample_time(0.8, no_cache=True)\n                data_sample.download_data(2000, partial=True)\n                self.data = Table(data_sample)\n                self.sampling.setVisible(True)\n                if self.auto_sample:\n                    self.__timer.start()\n\n        if self.data is not None and (len(self.data) == 0 or\n                                      len(self.data.domain.variables) == 0):\n            self.data = None\n\n    def get_embedding(self):\n        self.valid_data = None\n        if self.data is None:\n            return None\n\n        x_data = self.get_column(self.attr_x, filter_valid=False)\n        y_data = self.get_column(self.attr_y, filter_valid=False)\n        if x_data is None or y_data is None:\n            return None\n\n        self.Warning.missing_coords.clear()\n        self.Information.missing_coords.clear()\n        self.valid_data = np.isfinite(x_data) & np.isfinite(y_data)\n        if self.valid_data is not None and not np.all(self.valid_data):\n            msg = self.Information if np.any(self.valid_data) else self.Warning\n            msg.missing_coords(self.attr_x.name, self.attr_y.name)\n        return np.vstack((x_data, y_data)).T\n\n    # Tooltip\n    def _point_tooltip(self, point_id, skip_attrs=()):\n        point_data = self.data[point_id]\n        xy_attrs = (self.attr_x, self.attr_y)\n        text = \"<br/>\".join(\n            escape('{} = {}'.format(var.name, point_data[var]))\n            for var in xy_attrs)\n        if self.tooltip_shows_all:\n            others = super()._point_tooltip(point_id, skip_attrs=xy_attrs)\n            if others:\n                text = \"<b>{}</b><br/><br/>{}\".format(text, others)\n        return text\n\n    def can_draw_regresssion_line(self):\n        return self.data is not None and \\\n               self.data.domain is not None and \\\n               self.attr_x.is_continuous and \\\n               self.attr_y.is_continuous\n\n    def add_data(self, time=0.4):\n        if self.data and len(self.data) > 2000:\n            self.__timer.stop()\n            return\n        data_sample = self.sql_data.sample_time(time, no_cache=True)\n        if data_sample:\n            data_sample.download_data(2000, partial=True)\n            data = Table(data_sample)\n            self.data = Table.concatenate((self.data, data), axis=0)\n            self.handleNewSignals()\n\n    def init_attr_values(self):\n        super().init_attr_values()\n        data = self.data\n        domain = data.domain if data and len(data) else None\n        self.xy_model.set_domain(domain)\n        self.attr_x = self.xy_model[0] if self.xy_model else None\n        self.attr_y = self.xy_model[1] if len(self.xy_model) >= 2 \\\n            else self.attr_x\n\n    def switch_sampling(self):\n        self.__timer.stop()\n        if self.auto_sample and self.sql_data:\n            self.add_data()\n            self.__timer.start()\n\n    @OWDataProjectionWidget.Inputs.data_subset\n    def set_subset_data(self, subset_data):\n        self.warning()\n        if isinstance(subset_data, SqlTable):\n            if subset_data.approx_len() < AUTO_DL_LIMIT:\n                subset_data = Table(subset_data)\n            else:\n                self.warning(\"Data subset does not support large Sql tables\")\n                subset_data = None\n        super().set_subset_data(subset_data)\n\n    # called when all signals are received, so the graph is updated only once\n    def handleNewSignals(self):\n        self.attr_box.setEnabled(True)\n        self.vizrank.setEnabled(True)\n        if self.attribute_selection_list and self.data is not None and \\\n                self.data.domain is not None and \\\n                all(attr in self.data.domain for attr\n                        in self.attribute_selection_list):\n            self.attr_x, self.attr_y = self.attribute_selection_list[:2]\n            self.attr_box.setEnabled(False)\n            self.vizrank.setEnabled(False)\n        super().handleNewSignals()\n        if self._domain_invalidated:\n            self.graph.update_axes()\n            self._domain_invalidated = False\n        self.cb_reg_line.setEnabled(self.can_draw_regresssion_line())\n\n    @Inputs.features\n    def set_shown_attributes(self, attributes):\n        if attributes and len(attributes) >= 2:\n            self.attribute_selection_list = attributes[:2]\n            self._invalidated = self._invalidated \\\n                or self.attr_x != attributes[0] \\\n                or self.attr_y != attributes[1]\n        else:\n            self.attribute_selection_list = None\n\n    def set_attr(self, attr_x, attr_y):\n        if attr_x != self.attr_x or attr_y != self.attr_y:\n            self.attr_x, self.attr_y = attr_x, attr_y\n            self.attr_changed()\n\n    def set_attr_from_combo(self):\n        self.attr_changed()\n        self.xy_changed_manually.emit(self.attr_x, self.attr_y)\n\n    def attr_changed(self):\n        self.cb_reg_line.setEnabled(self.can_draw_regresssion_line())\n        self.setup_plot()\n        self.commit.deferred()\n\n    def get_axes(self):\n        return {\"bottom\": self.attr_x, \"left\": self.attr_y}\n\n    def colors_changed(self):\n        super().colors_changed()\n        self._vizrank_color_change()\n\n    @gui.deferred\n    def commit(self):\n        super().commit()\n        self.send_features()\n\n    def send_features(self):\n        features = [attr for attr in [self.attr_x, self.attr_y] if attr]\n        self.Outputs.features.send(AttributeList(features) or None)\n\n    def get_widget_name_extension(self):\n        if self.data is not None:\n            return \"{} vs {}\".format(self.attr_x.name, self.attr_y.name)\n        return None\n\n    def _get_send_report_caption(self):\n        return report.render_items_vert((\n            (\"Color\", self._get_caption_var_name(self.attr_color)),\n            (\"Label\", self._get_caption_var_name(self.attr_label)),\n            (\"Shape\", self._get_caption_var_name(self.attr_shape)),\n            (\"Size\", self._get_caption_var_name(self.attr_size)),\n            (\"Jittering\", (self.attr_x.is_discrete or\n                           self.attr_y.is_discrete or\n                           self.graph.jitter_continuous) and\n             self.graph.jitter_size)))\n\n    @classmethod\n    def migrate_settings(cls, settings, version):\n        if version < 2 and \"selection\" in settings and settings[\"selection\"]:\n            settings[\"selection_group\"] = [(a, 1) for a in settings[\"selection\"]]\n        if version < 3:\n            if \"auto_send_selection\" in settings:\n                settings[\"auto_commit\"] = settings[\"auto_send_selection\"]\n            if \"selection_group\" in settings:\n                settings[\"selection\"] = settings[\"selection_group\"]\n        if version < 5:\n            if \"graph\" in settings and \\\n                    \"jitter_continuous\" not in settings[\"graph\"]:\n                settings[\"graph\"][\"jitter_continuous\"] = True\n\n    @classmethod\n    def migrate_context(cls, context, version):\n        values = context.values\n        if version < 3:\n            values[\"attr_color\"] = values[\"graph\"][\"attr_color\"]\n            values[\"attr_size\"] = values[\"graph\"][\"attr_size\"]\n            values[\"attr_shape\"] = values[\"graph\"][\"attr_shape\"]\n            values[\"attr_label\"] = values[\"graph\"][\"attr_label\"]\n        if version < 4:\n            if values[\"attr_x\"][1] % 100 == 1 or values[\"attr_y\"][1] % 100 == 1:\n                raise IncompatibleContext()\n\n\nif __name__ == \"__main__\":  # pragma: no cover\n    table = Table(\"iris\")\n    WidgetPreview(OWScatterPlot).run(set_data=table,\n                                     set_subset_data=table[:30])\n"}
A user will ask you to solve a task. You should first draft your thinking process (inner monologue). Then, generate the solution.

Your response format must follow the template below:
<think>
Your thoughts or/and draft, like working through an exercise on scratch paper. Be as casual and as long as you want until you are confident to generate a correct solution.
</think>
<solution>
Final solution presented to the user.
</solution>
system
We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
[ISSUE]
**Title:** Scatter Plot does not update when input attributes change simultaneously

**Description:**
When updating the Scatter Plot widget by providing new input attributes and data at the same time, the plot fails to refresh with the updated attributes. This results in the visualization not reflecting the new attributes, causing inconsistent or stale plots.

**Example Code:**
```python
# Simulate setting new features and data simultaneously
features = data.domain.attributes[:2]
signals = [
    (widget.Inputs.features, AttributeList(features)),
    (widget.Inputs.data, data)
]
send_signals(signals)

# Expect the plot to update with the new features
```

**Expected Behavior:**
The Scatter Plot should replot using the new input attributes, updating the visualization to accurately reflect the changes.

**Actual Behavior:**
The plot does not update when features and data are set at the same time, leaving the visualization unchanged.

[/ISSUE]
--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.

--- BEGIN FILE ---
```
### Orange/widgets/visualize/owscatterplot.py
from itertools import chain
from xml.sax.saxutils import escape

import numpy as np
from scipy.stats import linregress
from sklearn.neighbors import NearestNeighbors
from sklearn.metrics import r2_score

from AnyQt.QtCore import Qt, QTimer, QPointF, Signal
from AnyQt.QtGui import QColor, QFont
from AnyQt.QtWidgets import QGroupBox, QPushButton

import pyqtgraph as pg

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, 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, \
    ScatterBaseParameterSetter
from Orange.widgets.visualize.utils import VizRankDialogAttrPair
from Orange.widgets.visualize.utils.customizableplot import Updater
from Orange.widgets.visualize.utils.widget import OWDataProjectionWidget
from Orange.widgets.widget import AttributeList, Msg, Input, Output


class ScatterPlotVizRank(VizRankDialogAttrPair):
    captionTitle = "Score Plots"
    minK = 10
    attr_color = None

    def __init__(self, master):
        super().__init__(master)
        self.attr_color = self.master.attr_color

    def initialize(self):
        self.attr_color = self.master.attr_color
        super().initialize()

    def check_preconditions(self):
        self.Information.add_message(
            "color_required", "Color variable is not selected")
        self.Information.color_required.clear()
        if not super().check_preconditions():
            return False
        if not self.attr_color:
            self.Information.color_required()
            return False
        return True

    def iterate_states(self, initial_state):
        # If we put initialization of `self.attrs` to `initialize`,
        # `score_heuristic` would be run on every call to `set_data`.
        if initial_state is None:  # on the first call, compute order
            self.attrs = self.score_heuristic()
        yield from super().iterate_states(initial_state)

    def compute_score(self, state):
        # pylint: disable=invalid-unary-operand-type
        attrs = [self.attrs[i] for i in state]
        data = self.master.data
        data = data.transform(Domain(attrs, self.attr_color))
        data = data[~np.isnan(data.X).any(axis=1) & ~np.isnan(data.Y).T]
        if len(data) < self.minK:
            return None
        n_neighbors = min(self.minK, len(data) - 1)
        knn = NearestNeighbors(n_neighbors=n_neighbors).fit(data.X)
        ind = knn.kneighbors(return_distance=False)
        if data.domain.has_discrete_class:
            return -np.sum(data.Y[ind] == data.Y.reshape(-1, 1)) / \
                   n_neighbors / len(data.Y)
        else:
            return -r2_score(data.Y, np.mean(data.Y[ind], axis=1)) * \
                   (len(data.Y) / len(self.master.data))

    def bar_length(self, score):
        return max(0, -score)

    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_primitive()]
        domain = Domain(attributes=vars, class_vars=self.attr_color)
        data = self.master.data.transform(domain)
        relief = ReliefF if isinstance(domain.class_var, DiscreteVariable) \
            else RReliefF
        weights = relief(n_iterations=100, k_nearest=self.minK)(data)
        attrs = sorted(zip(weights, domain.attributes),
                       key=lambda x: (-x[0], x[1].name))
        return [a for _, a in attrs]


class ParameterSetter(ScatterBaseParameterSetter):
    DEFAULT_LINE_WIDTH = 3
    DEFAULT_LINE_ALPHA = 255

    def __init__(self, master):
        super().__init__(master)
        self.reg_line_label_font = QFont()
        self.reg_line_settings = {
            Updater.WIDTH_LABEL: self.DEFAULT_LINE_WIDTH,
            Updater.ALPHA_LABEL: self.DEFAULT_LINE_ALPHA,
            Updater.STYLE_LABEL: Updater.DEFAULT_LINE_STYLE,
        }

    def update_setters(self):
        super().update_setters()
        self.initial_settings[self.LABELS_BOX].update({
            self.AXIS_TITLE_LABEL: self.FONT_SETTING,
            self.AXIS_TICKS_LABEL: self.FONT_SETTING,
            self.LINE_LAB_LABEL: self.FONT_SETTING
        })
        self.initial_settings[self.PLOT_BOX] = {}
        self.initial_settings[self.PLOT_BOX][self.LINE_LABEL] = {
            Updater.WIDTH_LABEL: (range(1, 10), self.DEFAULT_LINE_WIDTH),
            Updater.ALPHA_LABEL: (range(0, 255, 5), self.DEFAULT_LINE_ALPHA),
            Updater.STYLE_LABEL: (list(Updater.LINE_STYLES),
                                  Updater.DEFAULT_LINE_STYLE),
        }

        def update_lines(**settings):
            self.reg_line_settings.update(**settings)
            Updater.update_inf_lines(self.reg_line_items,
                                     **self.reg_line_settings)
            self.master.update_reg_line_label_colors()

        def update_line_label(**settings):
            self.reg_line_label_font = \
                Updater.change_font(self.reg_line_label_font, settings)
            Updater.update_label_font(self.reg_line_label_items,
                                      self.reg_line_label_font)

        self._setters[self.LABELS_BOX][self.LINE_LAB_LABEL] = update_line_label
        self._setters[self.PLOT_BOX] = {self.LINE_LABEL: update_lines}

    @property
    def axis_items(self):
        return [value["item"] for value in
                self.master.plot_widget.plotItem.axes.values()]

    @property
    def reg_line_items(self):
        return self.master.reg_line_items

    @property
    def reg_line_label_items(self):
        return [line.label for line in self.master.reg_line_items
                if hasattr(line, "label")]


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)
        self.parameter_setter = ParameterSetter(self)
        self.reg_line_items = []

    def clear(self):
        super().clear()
        self.reg_line_items.clear()

    def update_coordinates(self):
        super().update_coordinates()
        self.update_axes()
        # Don't update_regression line here: update_coordinates is always
        # followed by update_point_props, which calls update_colors

    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, 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=var or "")
            if not var:
                self.plot_widget.hideAxis(axis)

    @staticmethod
    def _orthonormal_line(x, y, color, width, style=Qt.SolidLine):
        # https://en.wikipedia.org/wiki/Deming_regression, with δ=0.
        pen = pg.mkPen(color=color, width=width, style=style)
        xm = np.mean(x)
        ym = np.mean(y)
        sxx, sxy, _, syy = np.cov(x, y, ddof=1).flatten()

        if sxy != 0:  # also covers sxx != 0 and syy != 0
            slope = (syy - sxx + np.sqrt((syy - sxx) ** 2 + 4 * sxy ** 2)) \
                    / (2 * sxy)
            intercept = ym - slope * xm
            xmin = x.min()
            return pg.InfiniteLine(
                QPointF(xmin, xmin * slope + intercept),
                np.degrees(np.arctan(slope)),
                pen)
        elif (sxx == 0) == (syy == 0):  # both zero or non-zero -> can't draw
            return None
        elif sxx != 0:
            return pg.InfiniteLine(QPointF(x.min(), ym), 0, pen)
        else:
            return pg.InfiniteLine(QPointF(xm, y.min()), 90, pen)

    @staticmethod
    def _regression_line(x, y, color, width, style=Qt.SolidLine):
        min_x, max_x = np.min(x), np.max(x)
        if min_x == max_x:
            return None
        slope, intercept, rvalue, _, _ = linregress(x, y)
        angle = np.degrees(np.arctan(slope))
        start_y = min_x * slope + intercept
        rotate = 135 < angle % 360 < 315
        l_opts = dict(color=color, position=abs(rotate - 0.85),
                      rotateAxis=(1, 0), movable=True)
        reg_line_item = pg.InfiniteLine(
            pos=QPointF(min_x, start_y), angle=angle,
            pen=pg.mkPen(color=color, width=width, style=style),
            label=f"r = {rvalue:.2f}", labelOpts=l_opts)
        if rotate:
            reg_line_item.label.angle = 180
            reg_line_item.label.updateTransform()
        return reg_line_item

    def _add_line(self, x, y, color):
        width = self.parameter_setter.reg_line_settings[Updater.WIDTH_LABEL]
        alpha = self.parameter_setter.reg_line_settings[Updater.ALPHA_LABEL]
        style = self.parameter_setter.reg_line_settings[Updater.STYLE_LABEL]
        style = Updater.LINE_STYLES[style]
        color.setAlpha(alpha)
        if self.orthonormal_regression:
            line = self._orthonormal_line(x, y, color, width, style)
        else:
            line = self._regression_line(x, y, color, width, style)
        if line is None:
            return
        self.plot_widget.addItem(line)
        self.reg_line_items.append(line)

        if hasattr(line, "label"):
            Updater.update_label_font(
                [line.label], self.parameter_setter.reg_line_label_font
            )

    def update_reg_line_label_colors(self):
        for line in self.reg_line_items:
            if hasattr(line, "label"):
                color = 0.0 if self.class_density \
                    else line.pen.color().darker(175)
                line.label.setColor(color)

    def update_density(self):
        super().update_density()
        self.update_reg_line_label_colors()

    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
                and self.master.can_draw_regresssion_line()):
            return
        x, y = self.master.get_coordinates_data()
        if x is None:
            return
        self._add_line(x, y, QColor("#505050"))
        if self.master.is_continuous_color() or self.palette is None:
            return
        c_data = self.master.get_color_data()
        if c_data is None:
            return
        c_data = c_data.astype(int)
        for val in range(c_data.max() + 1):
            mask = c_data == val
            if mask.sum() > 1:
                self._add_line(x[mask], y[mask], self.palette[val].darker(135))
        self.update_reg_line_label_colors()


class OWScatterPlot(OWDataProjectionWidget):
    """Scatterplot visualization with explorative analysis and intelligent
    data visualization enhancements."""

    name = 'Scatter Plot'
    description = "Interactive scatter plot visualization with " \
                  "intelligent data visualization enhancements."
    icon = "icons/ScatterPlot.svg"
    priority = 140
    keywords = []

    class Inputs(OWDataProjectionWidget.Inputs):
        features = Input("Features", AttributeList)

    class Outputs(OWDataProjectionWidget.Outputs):
        features = Output("Features", AttributeList, dynamic=False)

    settings_version = 5
    auto_sample = Setting(True)
    attr_x = ContextSetting(None)
    attr_y = ContextSetting(None)
    tooltip_shows_all = Setting(True)

    GRAPH_CLASS = OWScatterPlotGraph
    graph = SettingProvider(OWScatterPlotGraph)
    embedding_variables_names = None

    xy_changed_manually = Signal(Variable, Variable)

    class Warning(OWDataProjectionWidget.Warning):
        missing_coords = Msg(
            "Plot cannot be displayed because '{}' or '{}' "
            "is missing for all data points.")

    class Information(OWDataProjectionWidget.Information):
        sampled_sql = Msg("Large SQL table; showing a sample.")
        missing_coords = Msg(
            "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)
        self.__timer.timeout.connect(self.add_data)
        super().__init__()

        # manually register Matplotlib file writers
        self.graph_writers = self.graph_writers.copy()
        for w in [MatplotlibFormat, MatplotlibPDFFormat]:
            self.graph_writers.append(w)

    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,
             self.gui.RegressionLine],
            self._plot_box)
        gui.checkBox(
            self._plot_box, self,
            value="graph.orthonormal_regression",
            label="Treat variables as independent",
            callback=self.graph.update_regression_line,
            tooltip=
            "If checked, fit line to group (minimize distance from points);\n"
            "otherwise fit y as a function of x (minimize vertical distances)",
            disabledBy=self.cb_reg_line)

    def _add_controls_axis(self):
        common_options = dict(
            labelWidth=50, orientation=Qt.Horizontal, sendSelectedValue=True,
            contentsLength=12, searchable=True
        )
        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=dmod.PRIMITIVE)
        self.cb_attr_x = gui.comboBox(
            self.attr_box, self, "attr_x", label="Axis x:",
            callback=self.set_attr_from_combo,
            model=self.xy_model, **common_options,
        )
        self.cb_attr_y = gui.comboBox(
            self.attr_box, self, "attr_y", label="Axis y:",
            callback=self.set_attr_from_combo,
            model=self.xy_model, **common_options,
        )
        vizrank_box = gui.hBox(self.attr_box)
        self.vizrank, self.vizrank_button = ScatterPlotVizRank.add_vizrank(
            vizrank_box, self, "Find Informative Projections", self.set_attr)

    def _add_controls_sampling(self):
        self.sampling = gui.auto_commit(
            self.controlArea, self, "auto_sample", "Sample", box="Sampling",
            callback=self.switch_sampling, commit=lambda: self.add_data(1))
        self.sampling.setVisible(False)

    @property
    def effective_variables(self):
        return [self.attr_x, self.attr_y] if self.attr_x and self.attr_y else []

    @property
    def effective_data(self):
        eff_var = self.effective_variables
        if eff_var and self.attr_x.name == self.attr_y.name:
            eff_var = [self.attr_x]
        return self.data.transform(Domain(eff_var))

    def _vizrank_color_change(self):
        self.vizrank.initialize()
        err_msg = ""
        if self.data is None:
            err_msg = "No data on input"
        elif self.data.is_sparse():
            err_msg = "Data is sparse"
        elif len(self.xy_model) < 3:
            err_msg = "Not enough features for ranking"
        elif self.attr_color is None:
            err_msg = "Color variable is not selected"
        elif np.isnan(self.data.get_column_view(
                self.attr_color)[0].astype(float)).all():
            err_msg = "Color variable has no values"
        self.vizrank_button.setEnabled(not err_msg)
        self.vizrank_button.setToolTip(err_msg)

    @OWDataProjectionWidget.Inputs.data
    def set_data(self, data):
        super().set_data(data)
        self._vizrank_color_change()

        def findvar(name, iterable):
            """Find a Orange.data.Variable in `iterable` by name"""
            for el in iterable:
                if isinstance(el, Variable) and el.name == name:
                    return el
            return None

        # handle restored settings from  < 3.3.9 when attr_* were stored
        # by name
        if isinstance(self.attr_x, str):
            self.attr_x = findvar(self.attr_x, self.xy_model)
        if isinstance(self.attr_y, str):
            self.attr_y = findvar(self.attr_y, self.xy_model)
        if isinstance(self.attr_label, str):
            self.attr_label = findvar(self.attr_label, self.gui.label_model)
        if isinstance(self.attr_color, str):
            self.attr_color = findvar(self.attr_color, self.gui.color_model)
        if isinstance(self.attr_shape, str):
            self.attr_shape = findvar(self.attr_shape, self.gui.shape_model)
        if isinstance(self.attr_size, str):
            self.attr_size = findvar(self.attr_size, self.gui.size_model)

    def check_data(self):
        super().check_data()
        self.__timer.stop()
        self.sampling.setVisible(False)
        self.sql_data = None
        if isinstance(self.data, SqlTable):
            if self.data.approx_len() < 4000:
                self.data = Table(self.data)
            else:
                self.Information.sampled_sql()
                self.sql_data = self.data
                data_sample = self.data.sample_time(0.8, no_cache=True)
                data_sample.download_data(2000, partial=True)
                self.data = Table(data_sample)
                self.sampling.setVisible(True)
                if self.auto_sample:
                    self.__timer.start()

        if self.data is not None and (len(self.data) == 0 or
                                      len(self.data.domain.variables) == 0):
            self.data = None

    def get_embedding(self):
        self.valid_data = None
        if self.data is None:
            return None

        x_data = self.get_column(self.attr_x, filter_valid=False)
        y_data = self.get_column(self.attr_y, filter_valid=False)
        if x_data is None or y_data is None:
            return None

        self.Warning.missing_coords.clear()
        self.Information.missing_coords.clear()
        self.valid_data = np.isfinite(x_data) & np.isfinite(y_data)
        if self.valid_data is not None and not np.all(self.valid_data):
            msg = self.Information if np.any(self.valid_data) else self.Warning
            msg.missing_coords(self.attr_x.name, self.attr_y.name)
        return np.vstack((x_data, y_data)).T

    # Tooltip
    def _point_tooltip(self, point_id, skip_attrs=()):
        point_data = self.data[point_id]
        xy_attrs = (self.attr_x, self.attr_y)
        text = "<br/>".join(
            escape('{} = {}'.format(var.name, point_data[var]))
            for var in xy_attrs)
        if self.tooltip_shows_all:
            others = super()._point_tooltip(point_id, skip_attrs=xy_attrs)
            if others:
                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()
            return
        data_sample = self.sql_data.sample_time(time, no_cache=True)
        if data_sample:
            data_sample.download_data(2000, partial=True)
            data = Table(data_sample)
            self.data = Table.concatenate((self.data, data), axis=0)
            self.handleNewSignals()

    def init_attr_values(self):
        super().init_attr_values()
        data = self.data
        domain = data.domain if data and len(data) else None
        self.xy_model.set_domain(domain)
        self.attr_x = self.xy_model[0] if self.xy_model else None
        self.attr_y = self.xy_model[1] if len(self.xy_model) >= 2 \
            else self.attr_x

    def switch_sampling(self):
        self.__timer.stop()
        if self.auto_sample and self.sql_data:
            self.add_data()
            self.__timer.start()

    @OWDataProjectionWidget.Inputs.data_subset
    def set_subset_data(self, subset_data):
        self.warning()
        if isinstance(subset_data, SqlTable):
            if subset_data.approx_len() < AUTO_DL_LIMIT:
                subset_data = Table(subset_data)
            else:
                self.warning("Data subset does not support large Sql tables")
                subset_data = None
        super().set_subset_data(subset_data)

    # called when all signals are received, so the graph is updated only once
    def handleNewSignals(self):
        self.attr_box.setEnabled(True)
        self.vizrank.setEnabled(True)
        if self.attribute_selection_list and self.data is not None and \
                self.data.domain is not None and \
                all(attr in self.data.domain for attr
                        in self.attribute_selection_list):
            self.attr_x, self.attr_y = self.attribute_selection_list[:2]
            self.attr_box.setEnabled(False)
            self.vizrank.setEnabled(False)
        super().handleNewSignals()
        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):
        if attributes and len(attributes) >= 2:
            self.attribute_selection_list = attributes[:2]
            self._invalidated = self._invalidated \
                or self.attr_x != attributes[0] \
                or self.attr_y != attributes[1]
        else:
            self.attribute_selection_list = None

    def set_attr(self, attr_x, attr_y):
        if attr_x != self.attr_x or attr_y != self.attr_y:
            self.attr_x, self.attr_y = attr_x, attr_y
            self.attr_changed()

    def set_attr_from_combo(self):
        self.attr_changed()
        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.deferred()

    def get_axes(self):
        return {"bottom": self.attr_x, "left": self.attr_y}

    def colors_changed(self):
        super().colors_changed()
        self._vizrank_color_change()

    @gui.deferred
    def commit(self):
        super().commit()
        self.send_features()

    def send_features(self):
        features = [attr for attr in [self.attr_x, self.attr_y] if attr]
        self.Outputs.features.send(AttributeList(features) or None)

    def get_widget_name_extension(self):
        if self.data is not None:
            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"]:
            settings["selection_group"] = [(a, 1) for a in settings["selection"]]
        if version < 3:
            if "auto_send_selection" in settings:
                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):
        values = context.values
        if version < 3:
            values["attr_color"] = values["graph"]["attr_color"]
            values["attr_size"] = values["graph"]["attr_size"]
            values["attr_shape"] = values["graph"]["attr_shape"]
            values["attr_label"] = values["graph"]["attr_label"]
        if version < 4:
            if values["attr_x"][1] % 100 == 1 or values["attr_y"][1] % 100 == 1:
                raise IncompatibleContext()


if __name__ == "__main__":  # pragma: no cover
    table = Table("iris")
    WidgetPreview(OWScatterPlot).run(set_data=table,
                                     set_subset_data=table[:30])

```
--- END FILE ---

Please first localize the bug based on the issue statement, and then generate *SEARCH/REPLACE* edits to fix the issue.

Every *SEARCH/REPLACE* edit must use this format:
1. The file path
2. The start of search block: <<<<<<< SEARCH
3. A contiguous chunk of lines to search for in the existing source code
4. The dividing line: =======
5. The lines to replace into the source code
6. The end of the replace block: >>>>>>> REPLACE

Here is an example:

```python
### mathweb/flask/app.py
<<<<<<< SEARCH
from flask import Flask
=======
import math
from flask import Flask
>>>>>>> REPLACE
```
</solution>

Please note that the *SEARCH/REPLACE* edit REQUIRES PROPER INDENTATION. If you would like to add the line '        print(x)', you must fully write that out, with all those spaces before the code!
Wrap each *SEARCH/REPLACE* edit in a code block as shown in the example above. If you have multiple *SEARCH/REPLACE* edits, use a separate code block for each one.
user