{
    "file_diffs": [
        {
            "old_file_content": "import itertools\nimport enum\nfrom xml.sax.saxutils import escape\nfrom types import SimpleNamespace as namespace\n\nfrom typing import Optional, Union, Tuple, cast\n\nimport numpy as np\nimport sklearn.metrics\n\nfrom AnyQt.QtWidgets import (\n    QGraphicsWidget, QGraphicsGridLayout,\n    QGraphicsRectItem, QStyleOptionGraphicsItem, QSizePolicy, QWidget,\n    QVBoxLayout, QGraphicsSimpleTextItem, QWIDGETSIZE_MAX,\n    QGraphicsSceneHelpEvent, QToolTip, QApplication,\n)\nfrom AnyQt.QtGui import (\n    QColor, QPen, QBrush, QPainter, QFontMetrics, QPalette,\n)\nfrom AnyQt.QtCore import (\n    Qt, QEvent, QRectF, QSizeF, QSize, QPointF, QPoint, QRect\n)\nfrom AnyQt.QtCore import pyqtSignal as Signal\n\nimport Orange.data\nfrom Orange.data.util import get_unique_names\nimport Orange.distance\nimport Orange.misc\nfrom Orange.data import Table, Domain\nfrom Orange.misc import DistMatrix\n\nfrom Orange.widgets import widget, gui, settings\nfrom Orange.widgets.utils.graphicsscene import GraphicsScene\nfrom Orange.widgets.utils.stickygraphicsview import StickyGraphicsView\nfrom Orange.widgets.utils import itemmodels, apply_all\nfrom Orange.widgets.utils.annotated_data import (create_annotated_table,\n                                                 ANNOTATED_DATA_SIGNAL_NAME)\nfrom Orange.widgets.utils.graphicstextlist import TextListWidget\nfrom Orange.widgets.utils.graphicslayoutitem import SimpleLayoutItem\nfrom Orange.widgets.utils.sql import check_sql_input\nfrom Orange.widgets.utils.widgetpreview import WidgetPreview\nfrom Orange.widgets.visualize.utils.plotutils import AxisItem\nfrom Orange.widgets.widget import Msg, Input, Output\n\n\nclass InputValidationError(ValueError):\n    message: str\n\n\nclass NoGroupVariable(InputValidationError):\n    message = \"Input does not have any suitable labels\"\n\n\nclass ValidationError(InputValidationError):\n    def __init__(self, message: str):\n        super().__init__(message)\n        self.message = message\n\n\nclass OWSilhouettePlot(widget.OWWidget):\n    name = \"Silhouette Plot\"\n    description = \"Visually assess cluster quality and \" \\\n                  \"the degree of cluster membership.\"\n\n    icon = \"icons/SilhouettePlot.svg\"\n    priority = 300\n    keywords = []\n\n    class Inputs:\n        data = Input(\"Data\", (Orange.data.Table, Orange.misc.DistMatrix))\n\n    class Outputs:\n        selected_data = Output(\"Selected Data\", Orange.data.Table, default=True)\n        annotated_data = Output(ANNOTATED_DATA_SIGNAL_NAME, Orange.data.Table)\n\n    replaces = [\n        \"orangecontrib.prototypes.widgets.owsilhouetteplot.OWSilhouettePlot\",\n        \"Orange.widgets.unsupervised.owsilhouetteplot.OWSilhouettePlot\"\n    ]\n\n    settingsHandler = settings.DomainContextHandler()\n    settings_version = 2\n\n    #: Distance metric index\n    distance_idx = settings.Setting(0)\n    #: Group/cluster variable\n    cluster_var = settings.ContextSetting(None)\n    #: Annotation variable\n    annotation_var = settings.ContextSetting(None)\n    #: Group the (displayed) silhouettes by cluster\n    group_by_cluster = settings.Setting(True)\n    #: A fixed size for an instance bar\n    bar_size = settings.Setting(3)\n    #: Add silhouette scores to output data\n    auto_commit = settings.Setting(True)\n\n    pending_selection = settings.Setting(None, schema_only=True)\n\n    Distances = [(\"Euclidean\", Orange.distance.Euclidean),\n                 (\"Manhattan\", Orange.distance.Manhattan),\n                 (\"Cosine\", Orange.distance.Cosine)]\n\n    graph_name = \"scene\"\n\n    class Error(widget.OWWidget.Error):\n        need_two_clusters = Msg(\"Need at least two non-empty clusters\")\n        singleton_clusters_all = Msg(\"All clusters are singletons\")\n        memory_error = Msg(\"Not enough memory\")\n        value_error = Msg(\"Distances could not be computed: '{}'\")\n        input_validation_error = Msg(\"{}\")\n\n    class Warning(widget.OWWidget.Warning):\n        missing_cluster_assignment = Msg(\n            \"{} instance{s} omitted (missing cluster assignment)\")\n        nan_distances = Msg(\"{} instance{s} omitted (undefined distances)\")\n        ignoring_categorical = Msg(\"Ignoring categorical features\")\n\n    def __init__(self):\n        super().__init__()\n        #: The input data\n        self.data = None         # type: Optional[Orange.data.Table]\n        #: The input distance matrix (if present)\n        self.distances = None  # type: Optional[Orange.misc.DistMatrix]\n        #: The effective distance matrix (is self.distances or computed from\n        #: self.data depending on input)\n        self._matrix = None      # type: Optional[Orange.misc.DistMatrix]\n        #: An bool mask (size == len(data)) indicating missing group/cluster\n        #: assignments\n        self._mask = None        # type: Optional[np.ndarray]\n        #: An array of cluster/group labels for instances with valid group\n        #: assignment\n        self._labels = None      # type: Optional[np.ndarray]\n        #: An array of silhouette scores for instances with valid group\n        #: assignment\n        self._silhouette = None  # type: Optional[np.ndarray]\n        self._silplot = None     # type: Optional[SilhouettePlot]\n\n        controllayout = self.controlArea.layout()\n        assert isinstance(controllayout, QVBoxLayout)\n        self._distances_gui_box = distbox = gui.widgetBox(\n            None, \"Distance\"\n        )\n        self._distances_gui_cb = gui.comboBox(\n            distbox, self, \"distance_idx\",\n            items=[name for name, _ in OWSilhouettePlot.Distances],\n            orientation=Qt.Horizontal, callback=self._invalidate_distances)\n        controllayout.addWidget(distbox)\n\n        box = gui.vBox(self.controlArea, \"Grouping\")\n        self.cluster_var_model = itemmodels.VariableListModel(\n            parent=self, placeholder=\"(None)\")\n        self.cluster_var_cb = gui.comboBox(\n            box, self, \"cluster_var\", contentsLength=14,\n            searchable=True, callback=self._invalidate_scores,\n            model=self.cluster_var_model\n        )\n        gui.checkBox(\n            box, self, \"group_by_cluster\", \"Show in groups\",\n            callback=self._replot)\n\n        box = gui.vBox(self.controlArea, \"Bars\")\n        gui.widgetLabel(box, \"Bar width:\")\n        gui.hSlider(\n            box, self, \"bar_size\", minValue=1, maxValue=10, step=1,\n            callback=self._update_bar_size)\n        gui.widgetLabel(box, \"Annotations:\")\n        self.annotation_var_model = itemmodels.VariableListModel(parent=self)\n        self.annotation_var_model[:] = [None]\n        self.annotation_cb = gui.comboBox(\n            box, self, \"annotation_var\", contentsLength=14,\n            callback=self._update_annotations,\n            model=self.annotation_var_model)\n        ibox = gui.indentedBox(box, 5)\n        self.ann_hidden_warning = warning = gui.widgetLabel(\n            ibox, \"(increase the width to show)\")\n        ibox.setFixedWidth(ibox.sizeHint().width())\n        warning.setVisible(False)\n\n        gui.rubber(self.controlArea)\n\n        gui.auto_send(self.buttonsArea, self, \"auto_commit\")\n\n        self.scene = GraphicsScene(self)\n        self.view = StyledGraphicsView(self.scene)\n        self.view.setRenderHint(QPainter.Antialiasing, True)\n        self.view.setAlignment(Qt.AlignTop | Qt.AlignLeft)\n        self.mainArea.layout().addWidget(self.view)\n\n        self.settingsAboutToBePacked.connect(self.pack_settings)\n\n    def sizeHint(self):\n        sh = self.controlArea.sizeHint()\n        return sh.expandedTo(QSize(600, 720))\n\n    def pack_settings(self):\n        if self.data and self._silplot is not None:\n            self.pending_selection = list(self._silplot.selection())\n        else:\n            self.pending_selection = None\n\n    @Inputs.data\n    @check_sql_input\n    def set_data(self, data: Union[Table, DistMatrix, None]):\n        \"\"\"\n        Set the input dataset or distance matrix.\n        \"\"\"\n        self.closeContext()\n        self.clear()\n        try:\n            if isinstance(data, Orange.misc.DistMatrix):\n                self._set_distances(data)\n            elif isinstance(data, Orange.data.Table):\n                self._set_table(data)\n            else:\n                self.distances = None\n                self.data = None\n        except InputValidationError as err:\n            self.Error.input_validation_error(err.message)\n            self.distances = None\n            self.data = None\n\n    def _set_table(self, data: Table):\n        self._setup_control_models(data.domain)\n        self.data = data\n        self.distances = None\n\n    def _set_distances(self, distances: DistMatrix):\n        if isinstance(distances.row_items, Orange.data.Table) and \\\n                distances.axis == 1:\n            data = distances.row_items\n        else:\n            raise ValidationError(\"Input matrix does not have associated data\")\n\n        if data is not None:\n            self._setup_control_models(data.domain)\n            self.distances = distances\n            self.data = data\n\n    def handleNewSignals(self):\n        if not self._is_empty():\n            self._update()\n            self._replot()\n            if self.pending_selection is not None and self._silplot is not None:\n                # If selection contains indices that are too large, the data\n                # file must had been modified, so we ignore selection\n                if max(self.pending_selection, default=-1) < len(self.data):\n                    self._silplot.setSelection(np.array(self.pending_selection))\n                self.pending_selection = None\n\n        # Disable/enable the Distances GUI controls if applicable\n        self._distances_gui_box.setEnabled(self.distances is None)\n\n        self.commit.now()\n\n    def _setup_control_models(self, domain: Domain):\n        groupvars = [\n            v for v in domain.variables + domain.metas\n            if v.is_discrete and len(v.values) >= 2]\n        if not groupvars:\n            raise NoGroupVariable()\n        self.cluster_var_model[:] = groupvars\n        if domain.class_var in groupvars:\n            self.cluster_var = domain.class_var\n        else:\n            self.cluster_var = groupvars[0]\n        annotvars = [var for var in domain.variables + domain.metas\n                     if var.is_string or var.is_discrete]\n        self.annotation_var_model[:] = [None] + annotvars\n        self.annotation_var = annotvars[0] if annotvars else None\n        self.openContext(Orange.data.Domain(groupvars))\n\n    def _is_empty(self) -> bool:\n        # Is empty (does not have any input).\n        return (self.data is None or len(self.data) == 0) \\\n               and self.distances is None\n\n    def clear(self):\n        \"\"\"\n        Clear the widget state.\n        \"\"\"\n        self.data = None\n        self.distances = None\n        self._matrix = None\n        self._mask = None\n        self._silhouette = None\n        self._labels = None\n        self.cluster_var_model[:] = []\n        self.annotation_var_model[:] = [None]\n        self._clear_scene()\n        self.Error.clear()\n        self.Warning.clear()\n\n    def _clear_scene(self):\n        # Clear the graphics scene and associated objects\n        self.scene.clear()\n        self.scene.setSceneRect(QRectF())\n        self.view.setSceneRect(QRectF())\n        self.view.setHeaderSceneRect(QRectF())\n        self.view.setFooterSceneRect(QRectF())\n        self._silplot = None\n\n    def _invalidate_distances(self):\n        # Invalidate the computed distance matrix and recompute the silhouette.\n        self._matrix = None\n        self._invalidate_scores()\n\n    def _invalidate_scores(self):\n        # Invalidate and recompute the current silhouette scores.\n        self._labels = self._silhouette = self._mask = None\n        self._update()\n        self._replot()\n        if self.data is not None:\n            self.commit.deferred()\n\n    def _ensure_matrix(self):\n        # ensure self._matrix is computed if necessary\n        if self._is_empty():\n            return\n        if self._matrix is None:\n            if self.distances is not None:\n                self._matrix = np.asarray(self.distances)\n            elif self.data is not None:\n                data = self.data\n                _, metric = self.Distances[self.distance_idx]\n                if not metric.supports_discrete and any(\n                        a.is_discrete for a in data.domain.attributes):\n                    self.Warning.ignoring_categorical()\n                    data = Orange.distance.remove_discrete_features(data)\n                try:\n                    self._matrix = np.asarray(metric(data))\n                except MemoryError:\n                    self.Error.memory_error()\n                    return\n                except ValueError as err:\n                    self.Error.value_error(str(err))\n                    return\n            else:\n                assert False, \"invalid state\"\n\n    def _update(self):\n        # Update/recompute the effective distances and scores as required.\n        self._clear_messages()\n        if self._is_empty():\n            self._reset_all()\n            return\n\n        self._ensure_matrix()\n        if self._matrix is None:\n            return\n\n        labels, _ = self.data.get_column_view(self.cluster_var)\n        labels = np.asarray(labels, dtype=float)\n        cluster_mask = np.isnan(labels)\n        dist_mask = np.isnan(self._matrix).all(axis=0)\n        mask = cluster_mask | dist_mask\n        labels = labels.astype(int)\n        labels = labels[~mask]\n\n        labels_unq = np.unique(labels)\n\n        if len(labels_unq) < 2:\n            self.Error.need_two_clusters()\n            labels = silhouette = mask = None\n        elif len(labels_unq) == len(labels):\n            self.Error.singleton_clusters_all()\n            labels = silhouette = mask = None\n        else:\n            silhouette = sklearn.metrics.silhouette_samples(\n                self._matrix[~mask, :][:, ~mask], labels, metric=\"precomputed\")\n        self._mask = mask\n        self._labels = labels\n        self._silhouette = silhouette\n\n        if mask is not None:\n            count_missing = np.count_nonzero(cluster_mask)\n            if count_missing:\n                self.Warning.missing_cluster_assignment(\n                    count_missing, s=\"s\" if count_missing > 1 else \"\")\n            count_nandist = np.count_nonzero(dist_mask)\n            if count_nandist:\n                self.Warning.nan_distances(\n                    count_nandist, s=\"s\" if count_nandist > 1 else \"\")\n\n    def _reset_all(self):\n        self._mask = None\n        self._silhouette = None\n        self._labels = None\n        self._matrix = None\n        self._clear_scene()\n\n    def _clear_messages(self):\n        self.Error.clear()\n        self.Warning.clear()\n\n    def _set_bar_height(self):\n        visible = self.bar_size >= 5\n        self._silplot.setBarHeight(self.bar_size)\n        self._silplot.setRowNamesVisible(visible)\n        self.ann_hidden_warning.setVisible(\n            not visible and self.annotation_var is not None)\n\n    def _replot(self):\n        # Clear and replot/initialize the scene\n        self._clear_scene()\n        if self._silhouette is not None and self._labels is not None:\n            self._silplot = silplot = SilhouettePlot()\n            self._set_bar_height()\n\n            if self.group_by_cluster:\n                silplot.setScores(\n                    self._silhouette, self._labels,\n                    self.cluster_var.values, self.cluster_var.colors)\n            else:\n                silplot.setScores(\n                    self._silhouette,\n                    np.zeros(len(self._silhouette), dtype=int),\n                    [\"\"], np.array([[63, 207, 207]])\n                )\n\n            self.scene.addItem(silplot)\n            self._update_annotations()\n            silplot.selectionChanged.connect(self.commit.deferred)\n            silplot.layout().activate()\n            self._update_scene_rect()\n            silplot.geometryChanged.connect(self._update_scene_rect)\n\n    def _update_bar_size(self):\n        if self._silplot is not None:\n            self._set_bar_height()\n\n    def _update_annotations(self):\n        annot_var = self.annotation_var\n        self.ann_hidden_warning.setVisible(\n            self.bar_size < 5 and annot_var is not None)\n\n        if self._silplot is not None:\n            if annot_var is not None:\n                column, _ = self.data.get_column_view(annot_var)\n                if self._mask is not None:\n                    assert column.shape == self._mask.shape\n                    # pylint: disable=invalid-unary-operand-type\n                    column = column[~self._mask]\n                self._silplot.setRowNames(\n                    [annot_var.str_val(value) for value in column])\n            else:\n                self._silplot.setRowNames(None)\n\n    def _update_scene_rect(self):\n        geom = self._silplot.geometry()\n        self.scene.setSceneRect(geom)\n        self.view.setSceneRect(geom)\n\n        header = self._silplot.topScaleItem()\n        footer = self._silplot.bottomScaleItem()\n\n        def extend_horizontal(rect):\n            # type: (QRectF) -> QRectF\n            rect = QRectF(rect)\n            rect.setLeft(geom.left())\n            rect.setRight(geom.right())\n            return rect\n\n        margin = 3\n        if header is not None:\n            self.view.setHeaderSceneRect(\n                extend_horizontal(header.geometry().adjusted(0, 0, 0, margin)))\n        if footer is not None:\n            self.view.setFooterSceneRect(\n                extend_horizontal(footer.geometry().adjusted(0, -margin, 0, 0)))\n\n    @gui.deferred\n    def commit(self):\n        \"\"\"\n        Commit/send the current selection to the output.\n        \"\"\"\n        selected = indices = data = None\n        if self.data is not None:\n            selectedmask = np.full(len(self.data), False, dtype=bool)\n            if self._silplot is not None:\n                indices = self._silplot.selection()\n                assert (np.diff(indices) > 0).all(), \"strictly increasing\"\n                if self._mask is not None:\n                    # pylint: disable=invalid-unary-operand-type\n                    indices = np.flatnonzero(~self._mask)[indices]\n                selectedmask[indices] = True\n\n            if self._mask is not None:\n                scores = np.full(shape=selectedmask.shape,\n                                 fill_value=np.nan)\n                # pylint: disable=invalid-unary-operand-type\n                scores[~self._mask] = self._silhouette\n            else:\n                scores = self._silhouette\n\n            domain = self.data.domain\n            proposed = \"Silhouette ({})\".format(escape(self.cluster_var.name))\n            names = [var.name for var in itertools.chain(domain.attributes,\n                                                         domain.class_vars,\n                                                         domain.metas)]\n            unique = get_unique_names(names, proposed)\n            silhouette_var = Orange.data.ContinuousVariable(unique)\n            domain = Orange.data.Domain(\n                domain.attributes,\n                domain.class_vars,\n                domain.metas + (silhouette_var, ))\n\n            if np.count_nonzero(selectedmask):\n                selected = self.data.from_table(\n                    domain, self.data, np.flatnonzero(selectedmask))\n\n            if selected is not None:\n                with selected.unlocked(selected.metas):\n                    selected[:, silhouette_var] = np.c_[scores[selectedmask]]\n\n            data = self.data.transform(domain)\n            with data.unlocked(data.metas):\n                data[:, silhouette_var] = np.c_[scores]\n\n        self.Outputs.selected_data.send(selected)\n        self.Outputs.annotated_data.send(create_annotated_table(data, indices))\n\n    def send_report(self):\n        if not len(self.cluster_var_model):\n            return\n\n        self.report_plot()\n        caption = \"Silhouette plot \" \\\n                  f\"({self.Distances[self.distance_idx][0]} distance), \" \\\n                  f\"clustered by '{self.cluster_var.name}'\"\n        if self.annotation_var and self._silplot.rowNamesVisible():\n            caption += f\", annotated with '{self.annotation_var.name}'\"\n        self.report_caption(caption)\n\n    def onDeleteWidget(self):\n        self.clear()\n        super().onDeleteWidget()\n\n    @classmethod\n    def migrate_context(cls, context, version):\n        values = context.values\n        if version < 2:\n            discrete_vars = (\n                name\n                for name, type_ in itertools.chain(\n                    context.attributes, context.class_vars, context.metas)\n                if type_ == 1)\n            index, _ = values.pop(\"cluster_var_idx\")\n            name = next(itertools.islice(discrete_vars, index, None)\n            )\n            values[\"cluster_var\"] = (name, 101)\n\n            index = values.pop(\"annotation_var_idx\")[0] - 1\n            if index == -1:\n                values[\"annotation_var\"] = None\n            else:\n                annot_vars = (\n                    (name, type_ + 100)\n                    for name, type_ in itertools.chain(\n                        context.attributes, context.class_vars, context.metas)\n                    if type_ in (1, 3))\n                values[\"annotation_var\"] = next(\n                    itertools.islice(annot_vars, index, None))\n\n\nclass SelectAction(enum.IntEnum):\n    NoUpdate, Clear, Select, Deselect, Toogle, Current = 1, 2, 4, 8, 16, 32\n\n\ndef show_tool_tip(pos: QPoint, text: str, widget: Optional[QWidget] = None,\n                  rect=QRect(), elide=Qt.ElideRight):\n    \"\"\"\n    Show a plain text tool tip with limited length, eliding if necessary.\n    \"\"\"\n    if widget is not None:\n        screen = widget.screen()\n    else:\n        screen = QApplication.screenAt(pos)\n    font = QApplication.font(\"QTipLabel\")\n    fm = QFontMetrics(font)\n    geom = screen.availableSize()\n    etext = fm.elidedText(text, elide, geom.width())\n    if etext != text:\n        text = f\"<span>{etext}</span>\"\n    QToolTip.showText(pos, text, widget, rect)\n\n\nclass _SilhouettePlotTextListWidget(TextListWidget):\n    # disable default tooltips, SilhouettePlot handles them\n    def helpEvent(self, event: QGraphicsSceneHelpEvent):\n        return\n\n\nclass StyledGraphicsView(StickyGraphicsView):\n    \"\"\"\n    Propagate style and palette changes to the visualized scene.\n    \"\"\"\n    def __init__(self, *args, **kwargs):\n        super().__init__(*args, **kwargs)\n        self.ensurePolished()\n        if self.scene() is not None:\n            self.scene().setPalette(self.palette())\n\n    def setScene(self, scene):\n        super().setScene(scene)\n        if self.scene() is not None:\n            self.scene().setPalette(self.palette())\n\n    def changeEvent(self, event):\n        if event.type() == QEvent.PaletteChange and \\\n                self.scene() is not None and self.scene().parent() is self:\n            self.scene().setPalette(self.palette())\n        super().changeEvent(event)\n\n\nclass SilhouettePlot(QGraphicsWidget):\n    \"\"\"\n    A silhouette plot widget.\n    \"\"\"\n    #: Emitted when the current selection has changed\n    selectionChanged = Signal()\n\n    def __init__(self, parent=None, **kwargs):\n        super().__init__(parent, **kwargs)\n        self.setAcceptHoverEvents(True)\n        self.__groups = []\n        self.__rowNamesVisible = True\n        self.__barHeight = 3\n        self.__selectionRect = None\n        self.__selection = np.asarray([], dtype=int)\n        self.__selstate = None\n        self.__pen = QPen(Qt.NoPen)\n        self.__layout = QGraphicsGridLayout()\n        self.__hoveredItem = None\n        self.__topScale = None     # type: Optional[AxisItem]\n        self.__bottomScale = None  # type: Optional[AxisItem]\n        self.__layout.setColumnSpacing(0, 1.)\n        self.setLayout(self.__layout)\n        self.setFocusPolicy(Qt.StrongFocus)\n\n    def setScores(self, scores, labels, values, colors, rownames=None):\n        \"\"\"\n        Set the silhouette scores/labels to for display.\n\n        Arguments\n        ---------\n        scores : (N,) ndarray\n            The silhouette scores.\n        labels : (N,) ndarray\n            A ndarray (dtype=int) of label/clusters indices.\n        values : list of str\n            A list of label/cluster names.\n        colors : (N, 3) ndarray\n            A ndarray of RGB values.\n        rownames : list of str, optional\n            A list (len == N) of row names.\n        \"\"\"\n        scores = np.asarray(scores, dtype=float)\n        labels = np.asarray(labels, dtype=int)\n        if rownames is not None:\n            rownames = np.asarray(rownames, dtype=object)\n\n        if not scores.ndim == labels.ndim == 1:\n            raise ValueError(\"scores and labels must be 1 dimensional\")\n        if scores.shape != labels.shape:\n            raise ValueError(\"scores and labels must have the same shape\")\n        if rownames is not None and rownames.shape != scores.shape:\n            raise ValueError(\"rownames must have the same size as scores\")\n\n        Ck = np.unique(labels)\n        if not Ck[0] >= 0 and Ck[-1] < len(values):\n            raise ValueError(\n                \"All indices in `labels` must be in `range(len(values))`\")\n        cluster_indices = [np.flatnonzero(labels == i)\n                           for i in range(len(values))]\n        cluster_indices = [indices[np.argsort(scores[indices])[::-1]]\n                           for indices in cluster_indices]\n        groups = [\n            namespace(scores=scores[indices], indices=indices, label=label,\n                      rownames=(rownames[indices] if rownames is not None\n                                else None),\n                      color=color)\n            for indices, label, color in zip(cluster_indices, values, colors)\n        ]\n        self.clear()\n        self.__groups = groups\n        self.__setup()\n\n    def setRowNames(self, names):\n        if names is not None:\n            names = np.asarray(names, dtype=object)\n\n        layout = self.__layout\n        assert layout is self.layout()\n\n        font = self.font()\n        font.setPixelSize(self.__barHeight)\n\n        for i, grp in enumerate(self.__groups):\n            grp.rownames = names[grp.indices] if names is not None else None\n            item = layout.itemAt(i + 1, 3)\n            assert isinstance(item, TextListWidget)\n            if grp.rownames is not None:\n                item.setItems(grp.rownames)\n                item.setVisible(self.__rowNamesVisible)\n            else:\n                item.setItems([])\n                item.setVisible(False)\n        layout.activate()\n\n    def setRowNamesVisible(self, visible):\n        if self.__rowNamesVisible != visible:\n            self.__rowNamesVisible = visible\n            for item in self.__textItems():\n                item.setVisible(visible)\n            self.updateGeometry()\n\n    def rowNamesVisible(self):\n        return self.__rowNamesVisible\n\n    def setBarHeight(self, height):\n        \"\"\"\n        Set silhouette bar height (row height).\n        \"\"\"\n        if height != self.__barHeight:\n            self.__barHeight = height\n            for item in self.__plotItems():\n                item.setPreferredBarSize(height)\n            font = self.font()\n            font.setPixelSize(height)\n            for item in self.__textItems():\n                item.setFont(font)\n\n    def barHeight(self):\n        \"\"\"\n        Return the silhouette bar (row) height.\n        \"\"\"\n        return self.__barHeight\n\n    def clear(self):\n        \"\"\"\n        Clear the widget state\n        \"\"\"\n        scene = self.scene()\n        for child in self.childItems():\n            child.setParentItem(None)\n            scene.removeItem(child)\n        self.__groups = []\n        self.__topScale = None\n        self.__bottomScale = None\n\n    def __setup(self):\n        # Setup the subwidgets/groups/layout\n        smax = max((np.nanmax(g.scores) for g in self.__groups\n                    if g.scores.size),\n                   default=1)\n        smax = 1 if np.isnan(smax) else smax\n\n        smin = min((np.nanmin(g.scores) for g in self.__groups\n                    if g.scores.size),\n                   default=-1)\n        smin = -1 if np.isnan(smin) else smin\n        smin = min(smin, 0)\n\n        font = self.font()\n        font.setPixelSize(self.__barHeight)\n        foreground = self.palette().brush(QPalette.Foreground)\n        ax = AxisItem(parent=self, orientation=\"top\", maxTickLength=7)\n        ax.setRange(smin, smax)\n        self.__topScale = ax\n        layout = self.__layout\n        assert layout is self.layout()\n        layout.addItem(ax, 0, 2)\n\n        for i, group in enumerate(self.__groups):\n            silhouettegroup = BarPlotItem(parent=self)\n            silhouettegroup.setBrush(QBrush(QColor(*group.color)))\n            silhouettegroup.setPen(self.__pen)\n            silhouettegroup.setDataRange(smin, smax)\n            silhouettegroup.setPlotData(group.scores)\n            silhouettegroup.setPreferredBarSize(self.__barHeight)\n            silhouettegroup.setData(0, group.indices)\n            layout.addItem(silhouettegroup, i + 1, 2)\n\n            if group.label:\n                layout.addItem(Line(orientation=Qt.Vertical), i + 1, 1)\n                label = QGraphicsSimpleTextItem(\n                    \"{} ({})\".format(group.label, len(group.scores)), self\n                )\n                label.setBrush(foreground)\n                label.setPen(QPen(Qt.NoPen))\n                label.setRotation(-90)\n                item = SimpleLayoutItem(\n                    label,\n                    anchor=(0., 1.0),\n                    anchorItem=(0., 0.),\n                )\n                item.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)\n                layout.addItem(item, i + 1, 0, Qt.AlignCenter)\n\n            textlist = _SilhouettePlotTextListWidget(\n                self, font=font, elideMode=Qt.ElideRight,\n                alignment=Qt.AlignLeft | Qt.AlignVCenter\n            )\n            textlist.setMaximumWidth(750)\n            textlist.setFlag(TextListWidget.ItemClipsChildrenToShape, False)\n            sp = textlist.sizePolicy()\n            sp.setVerticalPolicy(QSizePolicy.Ignored)\n            textlist.setSizePolicy(sp)\n            if group.rownames is not None:\n                textlist.setItems(group.items)\n                textlist.setVisible(self.__rowNamesVisible)\n            else:\n                textlist.setVisible(False)\n\n            layout.addItem(textlist, i + 1, 3)\n\n        ax = AxisItem(parent=self, orientation=\"bottom\", maxTickLength=7)\n        ax.setRange(smin, smax)\n        self.__bottomScale = ax\n        layout.addItem(ax, len(self.__groups) + 1, 2)\n\n    def topScaleItem(self):\n        # type: () -> Optional[QGraphicsWidget]\n        return self.__topScale\n\n    def bottomScaleItem(self):\n        # type: () -> Optional[QGraphicsWidget]\n        return self.__bottomScale\n\n    def __updateSizeConstraints(self):\n        # set/update fixed height constraint on the text annotation items so\n        # it matches the silhouette's height\n        for silitem, textitem in zip(self.__plotItems(), self.__textItems()):\n            height = silitem.effectiveSizeHint(Qt.PreferredSize).height()\n            textitem.setMaximumHeight(height)\n            textitem.setMinimumHeight(height)\n        mwidth = max((silitem.effectiveSizeHint(Qt.PreferredSize).width()\n                     for silitem in self.__plotItems()), default=300)\n        # match the AxisItem's width to the bars\n        for axis in self.__axisItems():\n            axis.setMaximumWidth(mwidth)\n            axis.setMinimumWidth(mwidth)\n\n    def changeEvent(self, event: QEvent) -> None:\n        if event.type() == QEvent.PaletteChange:\n            brush = self.palette().brush(QPalette.Text)\n            labels = [it for it in self.childItems()\n                      if isinstance(it, QGraphicsSimpleTextItem)]\n            apply_all(labels, lambda it: it.setBrush(brush))\n        super().changeEvent(event)\n\n    def event(self, event: QEvent) -> bool:\n        # Reimplemented\n        if event.type() == QEvent.LayoutRequest and \\\n                self.parentLayoutItem() is None:\n            self.__updateSizeConstraints()\n            self.resize(self.effectiveSizeHint(Qt.PreferredSize))\n        elif event.type() == QEvent.GraphicsSceneHelp:\n            self.helpEvent(cast(QGraphicsSceneHelpEvent, event))\n            if event.isAccepted():\n                return True\n        return super().event(event)\n\n    def helpEvent(self, event: QGraphicsSceneHelpEvent):\n        pos = self.mapFromScene(event.scenePos())\n        item = self.__itemDataAtPos(pos)\n        if item is None:\n            return\n        data, index, rect = item\n        if data.rownames is None:\n            return\n        ttip = data.rownames[index]\n        if ttip:\n            view = event.widget().parentWidget()\n            rect = view.mapFromScene(self.mapToScene(rect)).boundingRect()\n            show_tool_tip(event.screenPos(), ttip, event.widget(), rect)\n            event.setAccepted(True)\n\n    def __setHoveredItem(self, item):\n        # Set the current hovered `item` (:class:`QGraphicsRectItem`)\n        if self.__hoveredItem is not item:\n            if self.__hoveredItem is not None:\n                self.__hoveredItem.setPen(QPen(Qt.NoPen))\n            self.__hoveredItem = item\n            if item is not None:\n                item.setPen(QPen(Qt.lightGray))\n\n    def hoverEnterEvent(self, event):\n        # Reimplemented\n        event.accept()\n\n    def hoverMoveEvent(self, event):\n        # Reimplemented\n        event.accept()\n        item = self.itemAtPos(event.pos())\n        self.__setHoveredItem(item)\n\n    def hoverLeaveEvent(self, event):\n        # Reimplemented\n        self.__setHoveredItem(None)\n        event.accept()\n\n    def mousePressEvent(self, event):\n        # Reimplemented\n        if event.button() == Qt.LeftButton:\n            if event.modifiers() & Qt.ControlModifier:\n                saction = SelectAction.Toogle\n            elif event.modifiers() & Qt.AltModifier:\n                saction = SelectAction.Deselect\n            elif event.modifiers() & Qt.ShiftModifier:\n                saction = SelectAction.Select\n            else:\n                saction = SelectAction.Clear | SelectAction.Select\n            self.__selstate = namespace(\n                modifiers=event.modifiers(),\n                selection=self.__selection,\n                action=saction,\n                rect=None,\n            )\n            if saction & SelectAction.Clear:\n                self.__selstate.selection = np.array([], dtype=int)\n                self.setSelection(self.__selstate.selection)\n            event.accept()\n\n    def keyPressEvent(self, event):\n        if event.key() in (Qt.Key_Up, Qt.Key_Down):\n            if event.key() == Qt.Key_Up:\n                self.__move_selection(self.selection(), -1)\n            elif event.key() == Qt.Key_Down:\n                self.__move_selection(self.selection(), 1)\n            event.accept()\n            return\n        super().keyPressEvent(event)\n\n    def mouseMoveEvent(self, event):\n        # Reimplemented\n        if event.buttons() & Qt.LeftButton:\n            assert self.__selstate is not None\n            if self.__selectionRect is None:\n                self.__selectionRect = QGraphicsRectItem(self)\n\n            rect = (QRectF(event.buttonDownPos(Qt.LeftButton),\n                           event.pos()).normalized())\n\n            if not rect.width():\n                rect = rect.adjusted(-1e-7, -1e-7, 1e-7, 1e-7)\n\n            rect = rect.intersected(self.contentsRect())\n            self.__selectionRect.setRect(rect)\n            self.__selstate.rect = rect\n            self.__selstate.action |= SelectAction.Current\n\n            self.__setSelectionRect(rect, self.__selstate.action)\n            event.accept()\n\n    def mouseReleaseEvent(self, event):\n        # Reimplemented\n        if event.button() == Qt.LeftButton:\n            if self.__selectionRect is not None:\n                self.__selectionRect.setParentItem(None)\n                if self.scene() is not None:\n                    self.scene().removeItem(self.__selectionRect)\n                self.__selectionRect = None\n            event.accept()\n\n            rect = (QRectF(event.buttonDownPos(Qt.LeftButton), event.pos())\n                    .normalized())\n\n            if not rect.isValid():\n                rect = rect.adjusted(-1e-7, -1e-7, 1e-7, 1e-7)\n\n            rect = rect.intersected(self.contentsRect())\n            action = self.__selstate.action & ~SelectAction.Current\n            self.__setSelectionRect(rect, action)\n            self.__selstate = None\n\n    def __move_selection(self, selection, offset):\n        ids = np.asarray([pi.data(0) for pi in self.__plotItems()]).ravel()\n        indices = [np.where(ids == i)[0] for i in selection]\n        indices = np.asarray(indices) + offset\n        if min(indices) >= 0 and max(indices) < len(ids):\n            self.setSelection(ids[indices])\n\n    def __setSelectionRect(self, rect, action):\n        # Set the current mouse drag selection rectangle\n        if not rect.isValid():\n            rect = rect.adjusted(-0.01, -0.01, 0.01, 0.01)\n\n        rect = rect.intersected(self.contentsRect())\n\n        indices = self.__selectionIndices(rect)\n\n        if action & SelectAction.Clear:\n            selection = []\n        elif self.__selstate is not None:\n            # Mouse drag selection is in progress. Update only the current\n            # selection\n            selection = self.__selstate.selection\n        else:\n            selection = self.__selection\n\n        if action & SelectAction.Toogle:\n            selection = np.setxor1d(selection, indices)\n        elif action & SelectAction.Deselect:\n            selection = np.setdiff1d(selection, indices)\n        elif action & SelectAction.Select:\n            selection = np.union1d(selection, indices)\n\n        self.setSelection(selection)\n\n    def __selectionIndices(self, rect):\n        items = [item for item in self.__plotItems()\n                 if item.geometry().intersects(rect)]\n        selection = [np.array([], dtype=int)]\n        for item in items:\n            indices = item.data(0)\n            itemrect = item.geometry().intersected(rect)\n            crect = item.contentsRect()\n            itemrect = (item.mapFromParent(itemrect).boundingRect()\n                        .intersected(crect))\n            assert itemrect.top() >= 0\n            rowh = crect.height() / item.count()\n            indextop = np.floor(itemrect.top() / rowh)\n            indexbottom = np.ceil(itemrect.bottom() / rowh)\n            selection.append(indices[int(indextop): int(indexbottom)])\n        return np.hstack(selection)\n\n    def itemAtPos(self, pos):\n        items = [item for item in self.__plotItems()\n                 if item.geometry().contains(pos)]\n        if not items:\n            return None\n        else:\n            item = items[0]\n        crect = item.contentsRect()\n        pos = item.mapFromParent(pos)\n        if not crect.contains(pos):\n            return None\n\n        assert pos.x() >= 0\n        rowh = crect.height() / item.count()\n        index = int(np.floor(pos.y() / rowh))\n        index = min(index, item.count() - 1)\n        if index >= 0:\n            return item.items()[index]\n        else:\n            return None\n\n    def __itemDataAtPos(self, pos) -> Optional[Tuple[namespace, int, QRectF]]:\n        items = [(sitem, tlist, grp) for sitem, tlist, grp\n                 in zip(self.__plotItems(), self.__textItems(), self.__groups)\n                 if sitem.geometry().contains(pos) or tlist.isVisible()\n                 and tlist.geometry().contains(pos)]\n        if not items:\n            return None\n        else:\n            sitem, _, grp = items[0]\n        indices = grp.indices\n        assert (isinstance(indices, np.ndarray) and\n                indices.shape == (sitem.count(),))\n        crect = sitem.contentsRect()\n        pos = sitem.mapFromParent(pos)\n        if not crect.top() <= pos.y() <= crect.bottom():\n            return None\n        rowh = crect.height() / sitem.count()\n        index = int(np.floor(pos.y() / rowh))\n        index = min(index, indices.size - 1)\n        baritem = sitem.items()[index]\n        rect = self.mapRectFromItem(baritem, baritem.rect())\n        crect = self.contentsRect()\n        rect.setLeft(crect.left())\n        rect.setRight(crect.right())\n        return grp, index, rect\n\n    def __selectionChanged(self, selected, deselected):\n        for item, grp in zip(self.__plotItems(), self.__groups):\n            select = np.flatnonzero(\n                np.in1d(grp.indices, selected, assume_unique=True))\n            items = item.items()\n            if select.size:\n                for i in select:\n                    color = np.hstack((grp.color, np.array([130])))\n                    items[i].setBrush(QBrush(QColor(*color)))\n\n            deselect = np.flatnonzero(\n                np.in1d(grp.indices, deselected, assume_unique=True))\n            if deselect.size:\n                for i in deselect:\n                    items[i].setBrush(QBrush(QColor(*grp.color)))\n\n    def __plotItems(self):\n        for i in range(len(self.__groups)):\n            item = self.layout().itemAt(i + 1, 2)\n            if item is not None:\n                assert isinstance(item, BarPlotItem)\n                yield item\n\n    def __textItems(self):\n        for i in range(len(self.__groups)):\n            item = self.layout().itemAt(i + 1, 3)\n            if item is not None:\n                assert isinstance(item, TextListWidget)\n                yield item\n\n    def __axisItems(self):\n        return self.__topScale, self.__bottomScale\n\n    def setSelection(self, indices):\n        indices = np.unique(np.asarray(indices, dtype=int))\n        select = np.setdiff1d(indices, self.__selection)\n        deselect = np.setdiff1d(self.__selection, indices)\n\n        self.__selectionChanged(select, deselect)\n\n        self.__selection = indices\n\n        if deselect.size or select.size:\n            self.selectionChanged.emit()\n\n    def selection(self):\n        return np.asarray(self.__selection, dtype=int)\n\n\nclass Line(QGraphicsWidget):\n    \"\"\"\n    A line separator graphics widget\n    \"\"\"\n    def __init__(self, parent=None, orientation=Qt.Horizontal, **kwargs):\n        sizePolicy = kwargs.pop(\"sizePolicy\", None)\n        super().__init__(None, **kwargs)\n        self.__orientation = Qt.Horizontal\n        if sizePolicy is None:\n            sizePolicy = QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)\n            sizePolicy.setControlType(QSizePolicy.Frame)\n            self.setSizePolicy(sizePolicy)\n        else:\n            self.setSizePolicy(sizePolicy)\n\n        self.setOrientation(orientation)\n\n        if parent is not None:\n            self.setParentItem(parent)\n\n    def setOrientation(self, orientation):\n        if self.__orientation != orientation:\n            self.__orientation = orientation\n            sp = self.sizePolicy()\n            if orientation == Qt.Vertical:\n                sp.setVerticalPolicy(QSizePolicy.Expanding)\n                sp.setHorizontalPolicy(QSizePolicy.Fixed)\n            else:\n                sp.setVerticalPolicy(QSizePolicy.Fixed)\n                sp.setHorizontalPolicy(QSizePolicy.Expanding)\n            self.setSizePolicy(sp)\n            self.updateGeometry()\n\n    def sizeHint(self, which, constraint=QRectF()):\n        # type: (Qt.SizeHint, QSizeF) -> QSizeF\n        pw = 1.\n        sh = QSizeF()\n        if which == Qt.MinimumSize:\n            sh = QSizeF(pw, pw)\n        elif which == Qt.PreferredSize:\n            sh = QSizeF(pw, 30.)\n        elif which == Qt.MaximumSize:\n            sh = QSizeF(pw, QWIDGETSIZE_MAX)\n\n        if self.__orientation == Qt.Horizontal:\n            sh.transpose()  # Qt4 compatible\n        return sh\n\n    def paint(self, painter, option, widget=None):\n        # type: (QPainter, QStyleOptionGraphicsItem, Optional[QWidget]) -> None\n        palette = self.palette()  # type: QPalette\n        color = palette.color(QPalette.Foreground)\n        painter.setPen(QPen(color, 1))\n        rect = self.contentsRect()\n        center = rect.center()\n        if self.__orientation == Qt.Vertical:\n            p1 = QPointF(center.x(), rect.top())\n            p2 = QPointF(center.x(), rect.bottom())\n        elif self.__orientation == Qt.Horizontal:\n            p1 = QPointF(rect.left(), center.y())\n            p2 = QPointF(rect.right(), center.y())\n        else:\n            assert False\n        painter.drawLine(p1, p2)\n\n\nclass BarPlotItem(QGraphicsWidget):\n    def __init__(self, parent=None, **kwargs):\n        super().__init__(parent, **kwargs)\n        self.__barsize = 5\n        self.__spacing = 1\n        self.__pen = QPen(Qt.NoPen)\n        self.__brush = QBrush(QColor(\"#3FCFCF\"))\n        self.__range = (0., 1.)\n        self.__data = np.array([], dtype=float)\n        self.__items = []\n\n    def count(self):\n        return self.__data.size\n\n    def items(self):\n        return list(self.__items)\n\n    def setGeometry(self, geom):\n        super().setGeometry(geom)\n        self.__layout()\n\n    def event(self, event):\n        if event.type() == QEvent.GraphicsSceneResize:\n            self.__layout()\n        return super().event(event)\n\n    def sizeHint(self, which, constraint=QSizeF()):\n        spacing = max(self.__spacing * (self.count() - 1), 0)\n        return QSizeF(300, self.__barsize * self.count() + spacing)\n\n    def setPreferredBarSize(self, size):\n        if self.__barsize != size:\n            self.__barsize = size\n            self.updateGeometry()\n\n    def spacing(self):\n        return self.__spacing\n\n    def setSpacing(self, spacing):\n        if self.__spacing != spacing:\n            self.__spacing = spacing\n            self.updateGeometry()\n\n    def setPen(self, pen):\n        pen = QPen(pen)\n        if self.__pen != pen:\n            self.__pen = pen\n            for item in self.__items:\n                item.setPen(pen)\n\n    def pen(self):\n        return QPen(self.__pen)\n\n    def setBrush(self, brush):\n        brush = QBrush(brush)\n        if self.__brush != brush:\n            self.__brush = brush\n            for item in self.__items:\n                item.setBrush(brush)\n\n    def brush(self):\n        return QBrush(self.__brush)\n\n    def setPlotData(self, values):\n        self.__data = np.array(values, copy=True)\n        self.__update()\n        self.updateGeometry()\n\n    def setDataRange(self, rangemin, rangemax):\n        if self.__range != (rangemin, rangemax):\n            self.__range = (rangemin, rangemax)\n            self.__layout()\n\n    def __clear(self):\n        for item in self.__items:\n            item.setParentItem(None)\n        scene = self.scene()\n        if scene is not None:\n            for item in self.__items:\n                scene.removeItem(item)\n        self.__items = []\n\n    def __update(self):\n        self.__clear()\n\n        pen = self.pen()\n        brush = self.brush()\n        for _ in range(self.count()):\n            item = QGraphicsRectItem(self)\n            item.setPen(pen)\n            item.setBrush(brush)\n            self.__items.append(item)\n\n        self.__layout()\n\n    def __layout(self):\n        (N, ) = self.__data.shape\n        if not N:\n            return\n\n        spacing = self.__spacing\n        rect = self.contentsRect()\n        w = rect.width()\n        if rect.height() - (spacing * (N - 1)) <= 0:\n            spacing = 0\n\n        h = (rect.height() - (spacing * (N - 1))) / N\n        xmin, xmax = self.__range\n        span = xmax - xmin\n        if span < 1e-9:\n            span = 1\n        scalef = w * 1 / span\n\n        base = 0\n        base = (base - xmin) * scalef\n        datascaled = (self.__data - xmin) * scalef\n\n        for i, (v, item) in enumerate(zip(datascaled, self.__items)):\n            item.setRect(QRectF(base, rect.top() + i * (h + spacing),\n                                v - base, h).normalized())\n\n\nif __name__ == \"__main__\":  # pragma: no cover\n    WidgetPreview(OWSilhouettePlot).run(Orange.data.Table(\"brown-selected\"))\n",
            "new_file_content": "import itertools\nimport enum\nfrom xml.sax.saxutils import escape\nfrom types import SimpleNamespace as namespace\n\nfrom typing import Optional, Union, Tuple, cast\n\nimport numpy as np\nimport sklearn.metrics\n\nfrom AnyQt.QtWidgets import (\n    QGraphicsWidget, QGraphicsGridLayout,\n    QGraphicsRectItem, QStyleOptionGraphicsItem, QSizePolicy, QWidget,\n    QVBoxLayout, QGraphicsSimpleTextItem, QWIDGETSIZE_MAX,\n    QGraphicsSceneHelpEvent, QToolTip, QApplication,\n)\nfrom AnyQt.QtGui import (\n    QColor, QPen, QBrush, QPainter, QFontMetrics, QPalette,\n)\nfrom AnyQt.QtCore import (\n    Qt, QEvent, QRectF, QSizeF, QSize, QPointF, QPoint, QRect\n)\nfrom AnyQt.QtCore import pyqtSignal as Signal\n\nimport Orange.data\nfrom Orange.data.util import get_unique_names\nimport Orange.distance\nimport Orange.misc\nfrom Orange.data import Table, Domain\nfrom Orange.misc import DistMatrix\n\nfrom Orange.widgets import widget, gui, settings\nfrom Orange.widgets.utils.graphicsscene import GraphicsScene\nfrom Orange.widgets.utils.stickygraphicsview import StickyGraphicsView\nfrom Orange.widgets.utils import itemmodels, apply_all\nfrom Orange.widgets.utils.annotated_data import (create_annotated_table,\n                                                 ANNOTATED_DATA_SIGNAL_NAME)\nfrom Orange.widgets.utils.graphicstextlist import TextListWidget\nfrom Orange.widgets.utils.graphicslayoutitem import SimpleLayoutItem\nfrom Orange.widgets.utils.sql import check_sql_input\nfrom Orange.widgets.utils.widgetpreview import WidgetPreview\nfrom Orange.widgets.visualize.utils.plotutils import AxisItem\nfrom Orange.widgets.widget import Msg, Input, Output\n\n\nclass InputValidationError(ValueError):\n    message: str\n\n\nclass NoGroupVariable(InputValidationError):\n    message = \"Input does not have any suitable labels\"\n\n\nclass ValidationError(InputValidationError):\n    def __init__(self, message: str):\n        super().__init__(message)\n        self.message = message\n\n\nclass OWSilhouettePlot(widget.OWWidget):\n    name = \"Silhouette Plot\"\n    description = \"Visually assess cluster quality and \" \\\n                  \"the degree of cluster membership.\"\n\n    icon = \"icons/SilhouettePlot.svg\"\n    priority = 300\n    keywords = []\n\n    class Inputs:\n        data = Input(\"Data\", (Orange.data.Table, Orange.misc.DistMatrix))\n\n    class Outputs:\n        selected_data = Output(\"Selected Data\", Orange.data.Table, default=True)\n        annotated_data = Output(ANNOTATED_DATA_SIGNAL_NAME, Orange.data.Table)\n\n    replaces = [\n        \"orangecontrib.prototypes.widgets.owsilhouetteplot.OWSilhouettePlot\",\n        \"Orange.widgets.unsupervised.owsilhouetteplot.OWSilhouettePlot\"\n    ]\n\n    settingsHandler = settings.DomainContextHandler()\n    settings_version = 2\n\n    #: Distance metric index\n    distance_idx = settings.Setting(0)\n    #: Group/cluster variable\n    cluster_var = settings.ContextSetting(None)\n    #: Annotation variable\n    annotation_var = settings.ContextSetting(None)\n    #: Group the (displayed) silhouettes by cluster\n    group_by_cluster = settings.Setting(True)\n    #: A fixed size for an instance bar\n    bar_size = settings.Setting(3)\n    #: Add silhouette scores to output data\n    auto_commit = settings.Setting(True)\n\n    pending_selection = settings.Setting(None, schema_only=True)\n\n    Distances = [(\"Euclidean\", Orange.distance.Euclidean),\n                 (\"Manhattan\", Orange.distance.Manhattan),\n                 (\"Cosine\", Orange.distance.Cosine)]\n\n    graph_name = \"scene\"\n\n    class Error(widget.OWWidget.Error):\n        need_two_clusters = Msg(\"Need at least two non-empty clusters\")\n        singleton_clusters_all = Msg(\"All clusters are singletons\")\n        memory_error = Msg(\"Not enough memory\")\n        value_error = Msg(\"Distances could not be computed: '{}'\")\n        input_validation_error = Msg(\"{}\")\n\n    class Warning(widget.OWWidget.Warning):\n        missing_cluster_assignment = Msg(\n            \"{} instance{s} omitted (missing cluster assignment)\")\n        nan_distances = Msg(\"{} instance{s} omitted (undefined distances)\")\n        ignoring_categorical = Msg(\"Ignoring categorical features\")\n\n    def __init__(self):\n        super().__init__()\n        #: The input data\n        self.data = None         # type: Optional[Orange.data.Table]\n        #: The input distance matrix (if present)\n        self.distances = None  # type: Optional[Orange.misc.DistMatrix]\n        #: The effective distance matrix (is self.distances or computed from\n        #: self.data depending on input)\n        self._matrix = None      # type: Optional[Orange.misc.DistMatrix]\n        #: An bool mask (size == len(data)) indicating missing group/cluster\n        #: assignments\n        self._mask = None        # type: Optional[np.ndarray]\n        #: An array of cluster/group labels for instances with valid group\n        #: assignment\n        self._labels = None      # type: Optional[np.ndarray]\n        #: An array of silhouette scores for instances with valid group\n        #: assignment\n        self._silhouette = None  # type: Optional[np.ndarray]\n        self._silplot = None     # type: Optional[SilhouettePlot]\n\n        controllayout = self.controlArea.layout()\n        assert isinstance(controllayout, QVBoxLayout)\n        self._distances_gui_box = distbox = gui.widgetBox(\n            None, \"Distance\"\n        )\n        self._distances_gui_cb = gui.comboBox(\n            distbox, self, \"distance_idx\",\n            items=[name for name, _ in OWSilhouettePlot.Distances],\n            orientation=Qt.Horizontal, callback=self._invalidate_distances)\n        controllayout.addWidget(distbox)\n\n        box = gui.vBox(self.controlArea, \"Grouping\")\n        self.cluster_var_model = itemmodels.VariableListModel(\n            parent=self, placeholder=\"(None)\")\n        self.cluster_var_cb = gui.comboBox(\n            box, self, \"cluster_var\", contentsLength=14,\n            searchable=True, callback=self._invalidate_scores,\n            model=self.cluster_var_model\n        )\n        gui.checkBox(\n            box, self, \"group_by_cluster\", \"Show in groups\",\n            callback=self._replot)\n\n        box = gui.vBox(self.controlArea, \"Bars\")\n        gui.widgetLabel(box, \"Bar width:\")\n        gui.hSlider(\n            box, self, \"bar_size\", minValue=1, maxValue=10, step=1,\n            callback=self._update_bar_size)\n        gui.widgetLabel(box, \"Annotations:\")\n        self.annotation_var_model = itemmodels.VariableListModel(parent=self)\n        self.annotation_var_model[:] = [None]\n        self.annotation_cb = gui.comboBox(\n            box, self, \"annotation_var\", contentsLength=14,\n            callback=self._update_annotations,\n            model=self.annotation_var_model)\n        ibox = gui.indentedBox(box, 5)\n        self.ann_hidden_warning = warning = gui.widgetLabel(\n            ibox, \"(increase the width to show)\")\n        ibox.setFixedWidth(ibox.sizeHint().width())\n        warning.setVisible(False)\n\n        gui.rubber(self.controlArea)\n\n        gui.auto_send(self.buttonsArea, self, \"auto_commit\")\n\n        self.scene = GraphicsScene(self)\n        self.view = StyledGraphicsView(self.scene)\n        self.view.setRenderHint(QPainter.Antialiasing, True)\n        self.view.setAlignment(Qt.AlignTop | Qt.AlignLeft)\n        self.mainArea.layout().addWidget(self.view)\n\n        self.settingsAboutToBePacked.connect(self.pack_settings)\n\n    def sizeHint(self):\n        sh = self.controlArea.sizeHint()\n        return sh.expandedTo(QSize(600, 720))\n\n    def pack_settings(self):\n        if self.data and self._silplot is not None:\n            self.pending_selection = list(self._silplot.selection())\n        else:\n            self.pending_selection = None\n\n    @Inputs.data\n    @check_sql_input\n    def set_data(self, data: Union[Table, DistMatrix, None]):\n        \"\"\"\n        Set the input dataset or distance matrix.\n        \"\"\"\n        self.closeContext()\n        self.clear()\n        try:\n            if isinstance(data, Orange.misc.DistMatrix):\n                self._set_distances(data)\n            elif isinstance(data, Orange.data.Table):\n                self._set_table(data)\n            else:\n                self.distances = None\n                self.data = None\n        except InputValidationError as err:\n            self.Error.input_validation_error(err.message)\n            self.distances = None\n            self.data = None\n\n    def _set_table(self, data: Table):\n        self._setup_control_models(data.domain)\n        self.data = data\n        self.distances = None\n\n    def _set_distances(self, distances: DistMatrix):\n        if isinstance(distances.row_items, Orange.data.Table) and \\\n                distances.axis == 1:\n            data = distances.row_items\n        else:\n            raise ValidationError(\"Input matrix does not have associated data\")\n\n        if data is not None:\n            self._setup_control_models(data.domain)\n            self.distances = distances\n            self.data = data\n\n    def handleNewSignals(self):\n        if not self._is_empty():\n            self._update()\n            self._replot()\n            if self.pending_selection is not None and self._silplot is not None:\n                # If selection contains indices that are too large, the data\n                # file must had been modified, so we ignore selection\n                if max(self.pending_selection, default=-1) < len(self.data):\n                    self._silplot.setSelection(np.array(self.pending_selection))\n                self.pending_selection = None\n\n        # Disable/enable the Distances GUI controls if applicable\n        self._distances_gui_box.setEnabled(self.distances is None)\n\n        self.commit.now()\n\n    def _setup_control_models(self, domain: Domain):\n        groupvars = [\n            v for v in domain.variables + domain.metas\n            if v.is_discrete and len(v.values) >= 2]\n        if not groupvars:\n            raise NoGroupVariable()\n        self.cluster_var_model[:] = groupvars\n        if domain.class_var in groupvars:\n            self.cluster_var = domain.class_var\n        else:\n            self.cluster_var = groupvars[0]\n        annotvars = [var for var in domain.variables + domain.metas\n                     if var.is_string or var.is_discrete]\n        self.annotation_var_model[:] = [None] + annotvars\n        self.annotation_var = annotvars[0] if annotvars else None\n        self.openContext(domain)\n\n    def _is_empty(self) -> bool:\n        # Is empty (does not have any input).\n        return (self.data is None or len(self.data) == 0) \\\n               and self.distances is None\n\n    def clear(self):\n        \"\"\"\n        Clear the widget state.\n        \"\"\"\n        self.data = None\n        self.distances = None\n        self._matrix = None\n        self._mask = None\n        self._silhouette = None\n        self._labels = None\n        self.cluster_var_model[:] = []\n        self.annotation_var_model[:] = [None]\n        self._clear_scene()\n        self.Error.clear()\n        self.Warning.clear()\n\n    def _clear_scene(self):\n        # Clear the graphics scene and associated objects\n        self.scene.clear()\n        self.scene.setSceneRect(QRectF())\n        self.view.setSceneRect(QRectF())\n        self.view.setHeaderSceneRect(QRectF())\n        self.view.setFooterSceneRect(QRectF())\n        self._silplot = None\n\n    def _invalidate_distances(self):\n        # Invalidate the computed distance matrix and recompute the silhouette.\n        self._matrix = None\n        self._invalidate_scores()\n\n    def _invalidate_scores(self):\n        # Invalidate and recompute the current silhouette scores.\n        self._labels = self._silhouette = self._mask = None\n        self._update()\n        self._replot()\n        if self.data is not None:\n            self.commit.deferred()\n\n    def _ensure_matrix(self):\n        # ensure self._matrix is computed if necessary\n        if self._is_empty():\n            return\n        if self._matrix is None:\n            if self.distances is not None:\n                self._matrix = np.asarray(self.distances)\n            elif self.data is not None:\n                data = self.data\n                _, metric = self.Distances[self.distance_idx]\n                if not metric.supports_discrete and any(\n                        a.is_discrete for a in data.domain.attributes):\n                    self.Warning.ignoring_categorical()\n                    data = Orange.distance.remove_discrete_features(data)\n                try:\n                    self._matrix = np.asarray(metric(data))\n                except MemoryError:\n                    self.Error.memory_error()\n                    return\n                except ValueError as err:\n                    self.Error.value_error(str(err))\n                    return\n            else:\n                assert False, \"invalid state\"\n\n    def _update(self):\n        # Update/recompute the effective distances and scores as required.\n        self._clear_messages()\n        if self._is_empty():\n            self._reset_all()\n            return\n\n        self._ensure_matrix()\n        if self._matrix is None:\n            return\n\n        labels, _ = self.data.get_column_view(self.cluster_var)\n        labels = np.asarray(labels, dtype=float)\n        cluster_mask = np.isnan(labels)\n        dist_mask = np.isnan(self._matrix).all(axis=0)\n        mask = cluster_mask | dist_mask\n        labels = labels.astype(int)\n        labels = labels[~mask]\n\n        labels_unq = np.unique(labels)\n\n        if len(labels_unq) < 2:\n            self.Error.need_two_clusters()\n            labels = silhouette = mask = None\n        elif len(labels_unq) == len(labels):\n            self.Error.singleton_clusters_all()\n            labels = silhouette = mask = None\n        else:\n            silhouette = sklearn.metrics.silhouette_samples(\n                self._matrix[~mask, :][:, ~mask], labels, metric=\"precomputed\")\n        self._mask = mask\n        self._labels = labels\n        self._silhouette = silhouette\n\n        if mask is not None:\n            count_missing = np.count_nonzero(cluster_mask)\n            if count_missing:\n                self.Warning.missing_cluster_assignment(\n                    count_missing, s=\"s\" if count_missing > 1 else \"\")\n            count_nandist = np.count_nonzero(dist_mask)\n            if count_nandist:\n                self.Warning.nan_distances(\n                    count_nandist, s=\"s\" if count_nandist > 1 else \"\")\n\n    def _reset_all(self):\n        self._mask = None\n        self._silhouette = None\n        self._labels = None\n        self._matrix = None\n        self._clear_scene()\n\n    def _clear_messages(self):\n        self.Error.clear()\n        self.Warning.clear()\n\n    def _set_bar_height(self):\n        visible = self.bar_size >= 5\n        self._silplot.setBarHeight(self.bar_size)\n        self._silplot.setRowNamesVisible(visible)\n        self.ann_hidden_warning.setVisible(\n            not visible and self.annotation_var is not None)\n\n    def _replot(self):\n        # Clear and replot/initialize the scene\n        self._clear_scene()\n        if self._silhouette is not None and self._labels is not None:\n            self._silplot = silplot = SilhouettePlot()\n            self._set_bar_height()\n\n            if self.group_by_cluster:\n                silplot.setScores(\n                    self._silhouette, self._labels,\n                    self.cluster_var.values, self.cluster_var.colors)\n            else:\n                silplot.setScores(\n                    self._silhouette,\n                    np.zeros(len(self._silhouette), dtype=int),\n                    [\"\"], np.array([[63, 207, 207]])\n                )\n\n            self.scene.addItem(silplot)\n            self._update_annotations()\n            silplot.selectionChanged.connect(self.commit.deferred)\n            silplot.layout().activate()\n            self._update_scene_rect()\n            silplot.geometryChanged.connect(self._update_scene_rect)\n\n    def _update_bar_size(self):\n        if self._silplot is not None:\n            self._set_bar_height()\n\n    def _update_annotations(self):\n        annot_var = self.annotation_var\n        self.ann_hidden_warning.setVisible(\n            self.bar_size < 5 and annot_var is not None)\n\n        if self._silplot is not None:\n            if annot_var is not None:\n                column, _ = self.data.get_column_view(annot_var)\n                if self._mask is not None:\n                    assert column.shape == self._mask.shape\n                    # pylint: disable=invalid-unary-operand-type\n                    column = column[~self._mask]\n                self._silplot.setRowNames(\n                    [annot_var.str_val(value) for value in column])\n            else:\n                self._silplot.setRowNames(None)\n\n    def _update_scene_rect(self):\n        geom = self._silplot.geometry()\n        self.scene.setSceneRect(geom)\n        self.view.setSceneRect(geom)\n\n        header = self._silplot.topScaleItem()\n        footer = self._silplot.bottomScaleItem()\n\n        def extend_horizontal(rect):\n            # type: (QRectF) -> QRectF\n            rect = QRectF(rect)\n            rect.setLeft(geom.left())\n            rect.setRight(geom.right())\n            return rect\n\n        margin = 3\n        if header is not None:\n            self.view.setHeaderSceneRect(\n                extend_horizontal(header.geometry().adjusted(0, 0, 0, margin)))\n        if footer is not None:\n            self.view.setFooterSceneRect(\n                extend_horizontal(footer.geometry().adjusted(0, -margin, 0, 0)))\n\n    @gui.deferred\n    def commit(self):\n        \"\"\"\n        Commit/send the current selection to the output.\n        \"\"\"\n        selected = indices = data = None\n        if self.data is not None:\n            selectedmask = np.full(len(self.data), False, dtype=bool)\n            if self._silplot is not None:\n                indices = self._silplot.selection()\n                assert (np.diff(indices) > 0).all(), \"strictly increasing\"\n                if self._mask is not None:\n                    # pylint: disable=invalid-unary-operand-type\n                    indices = np.flatnonzero(~self._mask)[indices]\n                selectedmask[indices] = True\n\n            if self._mask is not None:\n                scores = np.full(shape=selectedmask.shape,\n                                 fill_value=np.nan)\n                # pylint: disable=invalid-unary-operand-type\n                scores[~self._mask] = self._silhouette\n            else:\n                scores = self._silhouette\n\n            domain = self.data.domain\n            proposed = \"Silhouette ({})\".format(escape(self.cluster_var.name))\n            names = [var.name for var in itertools.chain(domain.attributes,\n                                                         domain.class_vars,\n                                                         domain.metas)]\n            unique = get_unique_names(names, proposed)\n            silhouette_var = Orange.data.ContinuousVariable(unique)\n            domain = Orange.data.Domain(\n                domain.attributes,\n                domain.class_vars,\n                domain.metas + (silhouette_var, ))\n\n            if np.count_nonzero(selectedmask):\n                selected = self.data.from_table(\n                    domain, self.data, np.flatnonzero(selectedmask))\n\n            if selected is not None:\n                with selected.unlocked(selected.metas):\n                    selected[:, silhouette_var] = np.c_[scores[selectedmask]]\n\n            data = self.data.transform(domain)\n            with data.unlocked(data.metas):\n                data[:, silhouette_var] = np.c_[scores]\n\n        self.Outputs.selected_data.send(selected)\n        self.Outputs.annotated_data.send(create_annotated_table(data, indices))\n\n    def send_report(self):\n        if not len(self.cluster_var_model):\n            return\n\n        self.report_plot()\n        caption = \"Silhouette plot \" \\\n                  f\"({self.Distances[self.distance_idx][0]} distance), \" \\\n                  f\"clustered by '{self.cluster_var.name}'\"\n        if self.annotation_var and self._silplot.rowNamesVisible():\n            caption += f\", annotated with '{self.annotation_var.name}'\"\n        self.report_caption(caption)\n\n    def onDeleteWidget(self):\n        self.clear()\n        super().onDeleteWidget()\n\n    @classmethod\n    def migrate_context(cls, context, version):\n        values = context.values\n        if version < 2:\n            # contexts were constructed from Domain containing vars shown in\n            # the list view, context.class_vars and context.metas were always\n            # empty, and context.attributes contained discrete attributes\n            index, _ = values.pop(\"cluster_var_idx\")\n            values[\"cluster_var\"] = (context.attributes[index][0], 101)\n\n            index = values.pop(\"annotation_var_idx\")[0] - 1\n            if index == -1:\n                values[\"annotation_var\"] = None\n            elif index < len(context.attributes):\n                name, _ = context.attributes[index]\n                values[\"annotation_var\"] = (name, 101)\n            # else we cannot migrate\n            # Even this migration can be erroneous if metas contained a mixture\n            # of discrete and string attributes; the latter were not stored in\n            # context, so indices in context could have been wrong\n\n\nclass SelectAction(enum.IntEnum):\n    NoUpdate, Clear, Select, Deselect, Toogle, Current = 1, 2, 4, 8, 16, 32\n\n\ndef show_tool_tip(pos: QPoint, text: str, widget: Optional[QWidget] = None,\n                  rect=QRect(), elide=Qt.ElideRight):\n    \"\"\"\n    Show a plain text tool tip with limited length, eliding if necessary.\n    \"\"\"\n    if widget is not None:\n        screen = widget.screen()\n    else:\n        screen = QApplication.screenAt(pos)\n    font = QApplication.font(\"QTipLabel\")\n    fm = QFontMetrics(font)\n    geom = screen.availableSize()\n    etext = fm.elidedText(text, elide, geom.width())\n    if etext != text:\n        text = f\"<span>{etext}</span>\"\n    QToolTip.showText(pos, text, widget, rect)\n\n\nclass _SilhouettePlotTextListWidget(TextListWidget):\n    # disable default tooltips, SilhouettePlot handles them\n    def helpEvent(self, event: QGraphicsSceneHelpEvent):\n        return\n\n\nclass StyledGraphicsView(StickyGraphicsView):\n    \"\"\"\n    Propagate style and palette changes to the visualized scene.\n    \"\"\"\n    def __init__(self, *args, **kwargs):\n        super().__init__(*args, **kwargs)\n        self.ensurePolished()\n        if self.scene() is not None:\n            self.scene().setPalette(self.palette())\n\n    def setScene(self, scene):\n        super().setScene(scene)\n        if self.scene() is not None:\n            self.scene().setPalette(self.palette())\n\n    def changeEvent(self, event):\n        if event.type() == QEvent.PaletteChange and \\\n                self.scene() is not None and self.scene().parent() is self:\n            self.scene().setPalette(self.palette())\n        super().changeEvent(event)\n\n\nclass SilhouettePlot(QGraphicsWidget):\n    \"\"\"\n    A silhouette plot widget.\n    \"\"\"\n    #: Emitted when the current selection has changed\n    selectionChanged = Signal()\n\n    def __init__(self, parent=None, **kwargs):\n        super().__init__(parent, **kwargs)\n        self.setAcceptHoverEvents(True)\n        self.__groups = []\n        self.__rowNamesVisible = True\n        self.__barHeight = 3\n        self.__selectionRect = None\n        self.__selection = np.asarray([], dtype=int)\n        self.__selstate = None\n        self.__pen = QPen(Qt.NoPen)\n        self.__layout = QGraphicsGridLayout()\n        self.__hoveredItem = None\n        self.__topScale = None     # type: Optional[AxisItem]\n        self.__bottomScale = None  # type: Optional[AxisItem]\n        self.__layout.setColumnSpacing(0, 1.)\n        self.setLayout(self.__layout)\n        self.setFocusPolicy(Qt.StrongFocus)\n\n    def setScores(self, scores, labels, values, colors, rownames=None):\n        \"\"\"\n        Set the silhouette scores/labels to for display.\n\n        Arguments\n        ---------\n        scores : (N,) ndarray\n            The silhouette scores.\n        labels : (N,) ndarray\n            A ndarray (dtype=int) of label/clusters indices.\n        values : list of str\n            A list of label/cluster names.\n        colors : (N, 3) ndarray\n            A ndarray of RGB values.\n        rownames : list of str, optional\n            A list (len == N) of row names.\n        \"\"\"\n        scores = np.asarray(scores, dtype=float)\n        labels = np.asarray(labels, dtype=int)\n        if rownames is not None:\n            rownames = np.asarray(rownames, dtype=object)\n\n        if not scores.ndim == labels.ndim == 1:\n            raise ValueError(\"scores and labels must be 1 dimensional\")\n        if scores.shape != labels.shape:\n            raise ValueError(\"scores and labels must have the same shape\")\n        if rownames is not None and rownames.shape != scores.shape:\n            raise ValueError(\"rownames must have the same size as scores\")\n\n        Ck = np.unique(labels)\n        if not Ck[0] >= 0 and Ck[-1] < len(values):\n            raise ValueError(\n                \"All indices in `labels` must be in `range(len(values))`\")\n        cluster_indices = [np.flatnonzero(labels == i)\n                           for i in range(len(values))]\n        cluster_indices = [indices[np.argsort(scores[indices])[::-1]]\n                           for indices in cluster_indices]\n        groups = [\n            namespace(scores=scores[indices], indices=indices, label=label,\n                      rownames=(rownames[indices] if rownames is not None\n                                else None),\n                      color=color)\n            for indices, label, color in zip(cluster_indices, values, colors)\n        ]\n        self.clear()\n        self.__groups = groups\n        self.__setup()\n\n    def setRowNames(self, names):\n        if names is not None:\n            names = np.asarray(names, dtype=object)\n\n        layout = self.__layout\n        assert layout is self.layout()\n\n        font = self.font()\n        font.setPixelSize(self.__barHeight)\n\n        for i, grp in enumerate(self.__groups):\n            grp.rownames = names[grp.indices] if names is not None else None\n            item = layout.itemAt(i + 1, 3)\n            assert isinstance(item, TextListWidget)\n            if grp.rownames is not None:\n                item.setItems(grp.rownames)\n                item.setVisible(self.__rowNamesVisible)\n            else:\n                item.setItems([])\n                item.setVisible(False)\n        layout.activate()\n\n    def setRowNamesVisible(self, visible):\n        if self.__rowNamesVisible != visible:\n            self.__rowNamesVisible = visible\n            for item in self.__textItems():\n                item.setVisible(visible)\n            self.updateGeometry()\n\n    def rowNamesVisible(self):\n        return self.__rowNamesVisible\n\n    def setBarHeight(self, height):\n        \"\"\"\n        Set silhouette bar height (row height).\n        \"\"\"\n        if height != self.__barHeight:\n            self.__barHeight = height\n            for item in self.__plotItems():\n                item.setPreferredBarSize(height)\n            font = self.font()\n            font.setPixelSize(height)\n            for item in self.__textItems():\n                item.setFont(font)\n\n    def barHeight(self):\n        \"\"\"\n        Return the silhouette bar (row) height.\n        \"\"\"\n        return self.__barHeight\n\n    def clear(self):\n        \"\"\"\n        Clear the widget state\n        \"\"\"\n        scene = self.scene()\n        for child in self.childItems():\n            child.setParentItem(None)\n            scene.removeItem(child)\n        self.__groups = []\n        self.__topScale = None\n        self.__bottomScale = None\n\n    def __setup(self):\n        # Setup the subwidgets/groups/layout\n        smax = max((np.nanmax(g.scores) for g in self.__groups\n                    if g.scores.size),\n                   default=1)\n        smax = 1 if np.isnan(smax) else smax\n\n        smin = min((np.nanmin(g.scores) for g in self.__groups\n                    if g.scores.size),\n                   default=-1)\n        smin = -1 if np.isnan(smin) else smin\n        smin = min(smin, 0)\n\n        font = self.font()\n        font.setPixelSize(self.__barHeight)\n        foreground = self.palette().brush(QPalette.Foreground)\n        ax = AxisItem(parent=self, orientation=\"top\", maxTickLength=7)\n        ax.setRange(smin, smax)\n        self.__topScale = ax\n        layout = self.__layout\n        assert layout is self.layout()\n        layout.addItem(ax, 0, 2)\n\n        for i, group in enumerate(self.__groups):\n            silhouettegroup = BarPlotItem(parent=self)\n            silhouettegroup.setBrush(QBrush(QColor(*group.color)))\n            silhouettegroup.setPen(self.__pen)\n            silhouettegroup.setDataRange(smin, smax)\n            silhouettegroup.setPlotData(group.scores)\n            silhouettegroup.setPreferredBarSize(self.__barHeight)\n            silhouettegroup.setData(0, group.indices)\n            layout.addItem(silhouettegroup, i + 1, 2)\n\n            if group.label:\n                layout.addItem(Line(orientation=Qt.Vertical), i + 1, 1)\n                label = QGraphicsSimpleTextItem(\n                    \"{} ({})\".format(group.label, len(group.scores)), self\n                )\n                label.setBrush(foreground)\n                label.setPen(QPen(Qt.NoPen))\n                label.setRotation(-90)\n                item = SimpleLayoutItem(\n                    label,\n                    anchor=(0., 1.0),\n                    anchorItem=(0., 0.),\n                )\n                item.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)\n                layout.addItem(item, i + 1, 0, Qt.AlignCenter)\n\n            textlist = _SilhouettePlotTextListWidget(\n                self, font=font, elideMode=Qt.ElideRight,\n                alignment=Qt.AlignLeft | Qt.AlignVCenter\n            )\n            textlist.setMaximumWidth(750)\n            textlist.setFlag(TextListWidget.ItemClipsChildrenToShape, False)\n            sp = textlist.sizePolicy()\n            sp.setVerticalPolicy(QSizePolicy.Ignored)\n            textlist.setSizePolicy(sp)\n            if group.rownames is not None:\n                textlist.setItems(group.items)\n                textlist.setVisible(self.__rowNamesVisible)\n            else:\n                textlist.setVisible(False)\n\n            layout.addItem(textlist, i + 1, 3)\n\n        ax = AxisItem(parent=self, orientation=\"bottom\", maxTickLength=7)\n        ax.setRange(smin, smax)\n        self.__bottomScale = ax\n        layout.addItem(ax, len(self.__groups) + 1, 2)\n\n    def topScaleItem(self):\n        # type: () -> Optional[QGraphicsWidget]\n        return self.__topScale\n\n    def bottomScaleItem(self):\n        # type: () -> Optional[QGraphicsWidget]\n        return self.__bottomScale\n\n    def __updateSizeConstraints(self):\n        # set/update fixed height constraint on the text annotation items so\n        # it matches the silhouette's height\n        for silitem, textitem in zip(self.__plotItems(), self.__textItems()):\n            height = silitem.effectiveSizeHint(Qt.PreferredSize).height()\n            textitem.setMaximumHeight(height)\n            textitem.setMinimumHeight(height)\n        mwidth = max((silitem.effectiveSizeHint(Qt.PreferredSize).width()\n                     for silitem in self.__plotItems()), default=300)\n        # match the AxisItem's width to the bars\n        for axis in self.__axisItems():\n            axis.setMaximumWidth(mwidth)\n            axis.setMinimumWidth(mwidth)\n\n    def changeEvent(self, event: QEvent) -> None:\n        if event.type() == QEvent.PaletteChange:\n            brush = self.palette().brush(QPalette.Text)\n            labels = [it for it in self.childItems()\n                      if isinstance(it, QGraphicsSimpleTextItem)]\n            apply_all(labels, lambda it: it.setBrush(brush))\n        super().changeEvent(event)\n\n    def event(self, event: QEvent) -> bool:\n        # Reimplemented\n        if event.type() == QEvent.LayoutRequest and \\\n                self.parentLayoutItem() is None:\n            self.__updateSizeConstraints()\n            self.resize(self.effectiveSizeHint(Qt.PreferredSize))\n        elif event.type() == QEvent.GraphicsSceneHelp:\n            self.helpEvent(cast(QGraphicsSceneHelpEvent, event))\n            if event.isAccepted():\n                return True\n        return super().event(event)\n\n    def helpEvent(self, event: QGraphicsSceneHelpEvent):\n        pos = self.mapFromScene(event.scenePos())\n        item = self.__itemDataAtPos(pos)\n        if item is None:\n            return\n        data, index, rect = item\n        if data.rownames is None:\n            return\n        ttip = data.rownames[index]\n        if ttip:\n            view = event.widget().parentWidget()\n            rect = view.mapFromScene(self.mapToScene(rect)).boundingRect()\n            show_tool_tip(event.screenPos(), ttip, event.widget(), rect)\n            event.setAccepted(True)\n\n    def __setHoveredItem(self, item):\n        # Set the current hovered `item` (:class:`QGraphicsRectItem`)\n        if self.__hoveredItem is not item:\n            if self.__hoveredItem is not None:\n                self.__hoveredItem.setPen(QPen(Qt.NoPen))\n            self.__hoveredItem = item\n            if item is not None:\n                item.setPen(QPen(Qt.lightGray))\n\n    def hoverEnterEvent(self, event):\n        # Reimplemented\n        event.accept()\n\n    def hoverMoveEvent(self, event):\n        # Reimplemented\n        event.accept()\n        item = self.itemAtPos(event.pos())\n        self.__setHoveredItem(item)\n\n    def hoverLeaveEvent(self, event):\n        # Reimplemented\n        self.__setHoveredItem(None)\n        event.accept()\n\n    def mousePressEvent(self, event):\n        # Reimplemented\n        if event.button() == Qt.LeftButton:\n            if event.modifiers() & Qt.ControlModifier:\n                saction = SelectAction.Toogle\n            elif event.modifiers() & Qt.AltModifier:\n                saction = SelectAction.Deselect\n            elif event.modifiers() & Qt.ShiftModifier:\n                saction = SelectAction.Select\n            else:\n                saction = SelectAction.Clear | SelectAction.Select\n            self.__selstate = namespace(\n                modifiers=event.modifiers(),\n                selection=self.__selection,\n                action=saction,\n                rect=None,\n            )\n            if saction & SelectAction.Clear:\n                self.__selstate.selection = np.array([], dtype=int)\n                self.setSelection(self.__selstate.selection)\n            event.accept()\n\n    def keyPressEvent(self, event):\n        if event.key() in (Qt.Key_Up, Qt.Key_Down):\n            if event.key() == Qt.Key_Up:\n                self.__move_selection(self.selection(), -1)\n            elif event.key() == Qt.Key_Down:\n                self.__move_selection(self.selection(), 1)\n            event.accept()\n            return\n        super().keyPressEvent(event)\n\n    def mouseMoveEvent(self, event):\n        # Reimplemented\n        if event.buttons() & Qt.LeftButton:\n            assert self.__selstate is not None\n            if self.__selectionRect is None:\n                self.__selectionRect = QGraphicsRectItem(self)\n\n            rect = (QRectF(event.buttonDownPos(Qt.LeftButton),\n                           event.pos()).normalized())\n\n            if not rect.width():\n                rect = rect.adjusted(-1e-7, -1e-7, 1e-7, 1e-7)\n\n            rect = rect.intersected(self.contentsRect())\n            self.__selectionRect.setRect(rect)\n            self.__selstate.rect = rect\n            self.__selstate.action |= SelectAction.Current\n\n            self.__setSelectionRect(rect, self.__selstate.action)\n            event.accept()\n\n    def mouseReleaseEvent(self, event):\n        # Reimplemented\n        if event.button() == Qt.LeftButton:\n            if self.__selectionRect is not None:\n                self.__selectionRect.setParentItem(None)\n                if self.scene() is not None:\n                    self.scene().removeItem(self.__selectionRect)\n                self.__selectionRect = None\n            event.accept()\n\n            rect = (QRectF(event.buttonDownPos(Qt.LeftButton), event.pos())\n                    .normalized())\n\n            if not rect.isValid():\n                rect = rect.adjusted(-1e-7, -1e-7, 1e-7, 1e-7)\n\n            rect = rect.intersected(self.contentsRect())\n            action = self.__selstate.action & ~SelectAction.Current\n            self.__setSelectionRect(rect, action)\n            self.__selstate = None\n\n    def __move_selection(self, selection, offset):\n        ids = np.asarray([pi.data(0) for pi in self.__plotItems()]).ravel()\n        indices = [np.where(ids == i)[0] for i in selection]\n        indices = np.asarray(indices) + offset\n        if min(indices) >= 0 and max(indices) < len(ids):\n            self.setSelection(ids[indices])\n\n    def __setSelectionRect(self, rect, action):\n        # Set the current mouse drag selection rectangle\n        if not rect.isValid():\n            rect = rect.adjusted(-0.01, -0.01, 0.01, 0.01)\n\n        rect = rect.intersected(self.contentsRect())\n\n        indices = self.__selectionIndices(rect)\n\n        if action & SelectAction.Clear:\n            selection = []\n        elif self.__selstate is not None:\n            # Mouse drag selection is in progress. Update only the current\n            # selection\n            selection = self.__selstate.selection\n        else:\n            selection = self.__selection\n\n        if action & SelectAction.Toogle:\n            selection = np.setxor1d(selection, indices)\n        elif action & SelectAction.Deselect:\n            selection = np.setdiff1d(selection, indices)\n        elif action & SelectAction.Select:\n            selection = np.union1d(selection, indices)\n\n        self.setSelection(selection)\n\n    def __selectionIndices(self, rect):\n        items = [item for item in self.__plotItems()\n                 if item.geometry().intersects(rect)]\n        selection = [np.array([], dtype=int)]\n        for item in items:\n            indices = item.data(0)\n            itemrect = item.geometry().intersected(rect)\n            crect = item.contentsRect()\n            itemrect = (item.mapFromParent(itemrect).boundingRect()\n                        .intersected(crect))\n            assert itemrect.top() >= 0\n            rowh = crect.height() / item.count()\n            indextop = np.floor(itemrect.top() / rowh)\n            indexbottom = np.ceil(itemrect.bottom() / rowh)\n            selection.append(indices[int(indextop): int(indexbottom)])\n        return np.hstack(selection)\n\n    def itemAtPos(self, pos):\n        items = [item for item in self.__plotItems()\n                 if item.geometry().contains(pos)]\n        if not items:\n            return None\n        else:\n            item = items[0]\n        crect = item.contentsRect()\n        pos = item.mapFromParent(pos)\n        if not crect.contains(pos):\n            return None\n\n        assert pos.x() >= 0\n        rowh = crect.height() / item.count()\n        index = int(np.floor(pos.y() / rowh))\n        index = min(index, item.count() - 1)\n        if index >= 0:\n            return item.items()[index]\n        else:\n            return None\n\n    def __itemDataAtPos(self, pos) -> Optional[Tuple[namespace, int, QRectF]]:\n        items = [(sitem, tlist, grp) for sitem, tlist, grp\n                 in zip(self.__plotItems(), self.__textItems(), self.__groups)\n                 if sitem.geometry().contains(pos) or tlist.isVisible()\n                 and tlist.geometry().contains(pos)]\n        if not items:\n            return None\n        else:\n            sitem, _, grp = items[0]\n        indices = grp.indices\n        assert (isinstance(indices, np.ndarray) and\n                indices.shape == (sitem.count(),))\n        crect = sitem.contentsRect()\n        pos = sitem.mapFromParent(pos)\n        if not crect.top() <= pos.y() <= crect.bottom():\n            return None\n        rowh = crect.height() / sitem.count()\n        index = int(np.floor(pos.y() / rowh))\n        index = min(index, indices.size - 1)\n        baritem = sitem.items()[index]\n        rect = self.mapRectFromItem(baritem, baritem.rect())\n        crect = self.contentsRect()\n        rect.setLeft(crect.left())\n        rect.setRight(crect.right())\n        return grp, index, rect\n\n    def __selectionChanged(self, selected, deselected):\n        for item, grp in zip(self.__plotItems(), self.__groups):\n            select = np.flatnonzero(\n                np.in1d(grp.indices, selected, assume_unique=True))\n            items = item.items()\n            if select.size:\n                for i in select:\n                    color = np.hstack((grp.color, np.array([130])))\n                    items[i].setBrush(QBrush(QColor(*color)))\n\n            deselect = np.flatnonzero(\n                np.in1d(grp.indices, deselected, assume_unique=True))\n            if deselect.size:\n                for i in deselect:\n                    items[i].setBrush(QBrush(QColor(*grp.color)))\n\n    def __plotItems(self):\n        for i in range(len(self.__groups)):\n            item = self.layout().itemAt(i + 1, 2)\n            if item is not None:\n                assert isinstance(item, BarPlotItem)\n                yield item\n\n    def __textItems(self):\n        for i in range(len(self.__groups)):\n            item = self.layout().itemAt(i + 1, 3)\n            if item is not None:\n                assert isinstance(item, TextListWidget)\n                yield item\n\n    def __axisItems(self):\n        return self.__topScale, self.__bottomScale\n\n    def setSelection(self, indices):\n        indices = np.unique(np.asarray(indices, dtype=int))\n        select = np.setdiff1d(indices, self.__selection)\n        deselect = np.setdiff1d(self.__selection, indices)\n\n        self.__selectionChanged(select, deselect)\n\n        self.__selection = indices\n\n        if deselect.size or select.size:\n            self.selectionChanged.emit()\n\n    def selection(self):\n        return np.asarray(self.__selection, dtype=int)\n\n\nclass Line(QGraphicsWidget):\n    \"\"\"\n    A line separator graphics widget\n    \"\"\"\n    def __init__(self, parent=None, orientation=Qt.Horizontal, **kwargs):\n        sizePolicy = kwargs.pop(\"sizePolicy\", None)\n        super().__init__(None, **kwargs)\n        self.__orientation = Qt.Horizontal\n        if sizePolicy is None:\n            sizePolicy = QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)\n            sizePolicy.setControlType(QSizePolicy.Frame)\n            self.setSizePolicy(sizePolicy)\n        else:\n            self.setSizePolicy(sizePolicy)\n\n        self.setOrientation(orientation)\n\n        if parent is not None:\n            self.setParentItem(parent)\n\n    def setOrientation(self, orientation):\n        if self.__orientation != orientation:\n            self.__orientation = orientation\n            sp = self.sizePolicy()\n            if orientation == Qt.Vertical:\n                sp.setVerticalPolicy(QSizePolicy.Expanding)\n                sp.setHorizontalPolicy(QSizePolicy.Fixed)\n            else:\n                sp.setVerticalPolicy(QSizePolicy.Fixed)\n                sp.setHorizontalPolicy(QSizePolicy.Expanding)\n            self.setSizePolicy(sp)\n            self.updateGeometry()\n\n    def sizeHint(self, which, constraint=QRectF()):\n        # type: (Qt.SizeHint, QSizeF) -> QSizeF\n        pw = 1.\n        sh = QSizeF()\n        if which == Qt.MinimumSize:\n            sh = QSizeF(pw, pw)\n        elif which == Qt.PreferredSize:\n            sh = QSizeF(pw, 30.)\n        elif which == Qt.MaximumSize:\n            sh = QSizeF(pw, QWIDGETSIZE_MAX)\n\n        if self.__orientation == Qt.Horizontal:\n            sh.transpose()  # Qt4 compatible\n        return sh\n\n    def paint(self, painter, option, widget=None):\n        # type: (QPainter, QStyleOptionGraphicsItem, Optional[QWidget]) -> None\n        palette = self.palette()  # type: QPalette\n        color = palette.color(QPalette.Foreground)\n        painter.setPen(QPen(color, 1))\n        rect = self.contentsRect()\n        center = rect.center()\n        if self.__orientation == Qt.Vertical:\n            p1 = QPointF(center.x(), rect.top())\n            p2 = QPointF(center.x(), rect.bottom())\n        elif self.__orientation == Qt.Horizontal:\n            p1 = QPointF(rect.left(), center.y())\n            p2 = QPointF(rect.right(), center.y())\n        else:\n            assert False\n        painter.drawLine(p1, p2)\n\n\nclass BarPlotItem(QGraphicsWidget):\n    def __init__(self, parent=None, **kwargs):\n        super().__init__(parent, **kwargs)\n        self.__barsize = 5\n        self.__spacing = 1\n        self.__pen = QPen(Qt.NoPen)\n        self.__brush = QBrush(QColor(\"#3FCFCF\"))\n        self.__range = (0., 1.)\n        self.__data = np.array([], dtype=float)\n        self.__items = []\n\n    def count(self):\n        return self.__data.size\n\n    def items(self):\n        return list(self.__items)\n\n    def setGeometry(self, geom):\n        super().setGeometry(geom)\n        self.__layout()\n\n    def event(self, event):\n        if event.type() == QEvent.GraphicsSceneResize:\n            self.__layout()\n        return super().event(event)\n\n    def sizeHint(self, which, constraint=QSizeF()):\n        spacing = max(self.__spacing * (self.count() - 1), 0)\n        return QSizeF(300, self.__barsize * self.count() + spacing)\n\n    def setPreferredBarSize(self, size):\n        if self.__barsize != size:\n            self.__barsize = size\n            self.updateGeometry()\n\n    def spacing(self):\n        return self.__spacing\n\n    def setSpacing(self, spacing):\n        if self.__spacing != spacing:\n            self.__spacing = spacing\n            self.updateGeometry()\n\n    def setPen(self, pen):\n        pen = QPen(pen)\n        if self.__pen != pen:\n            self.__pen = pen\n            for item in self.__items:\n                item.setPen(pen)\n\n    def pen(self):\n        return QPen(self.__pen)\n\n    def setBrush(self, brush):\n        brush = QBrush(brush)\n        if self.__brush != brush:\n            self.__brush = brush\n            for item in self.__items:\n                item.setBrush(brush)\n\n    def brush(self):\n        return QBrush(self.__brush)\n\n    def setPlotData(self, values):\n        self.__data = np.array(values, copy=True)\n        self.__update()\n        self.updateGeometry()\n\n    def setDataRange(self, rangemin, rangemax):\n        if self.__range != (rangemin, rangemax):\n            self.__range = (rangemin, rangemax)\n            self.__layout()\n\n    def __clear(self):\n        for item in self.__items:\n            item.setParentItem(None)\n        scene = self.scene()\n        if scene is not None:\n            for item in self.__items:\n                scene.removeItem(item)\n        self.__items = []\n\n    def __update(self):\n        self.__clear()\n\n        pen = self.pen()\n        brush = self.brush()\n        for _ in range(self.count()):\n            item = QGraphicsRectItem(self)\n            item.setPen(pen)\n            item.setBrush(brush)\n            self.__items.append(item)\n\n        self.__layout()\n\n    def __layout(self):\n        (N, ) = self.__data.shape\n        if not N:\n            return\n\n        spacing = self.__spacing\n        rect = self.contentsRect()\n        w = rect.width()\n        if rect.height() - (spacing * (N - 1)) <= 0:\n            spacing = 0\n\n        h = (rect.height() - (spacing * (N - 1))) / N\n        xmin, xmax = self.__range\n        span = xmax - xmin\n        if span < 1e-9:\n            span = 1\n        scalef = w * 1 / span\n\n        base = 0\n        base = (base - xmin) * scalef\n        datascaled = (self.__data - xmin) * scalef\n\n        for i, (v, item) in enumerate(zip(datascaled, self.__items)):\n            item.setRect(QRectF(base, rect.top() + i * (h + spacing),\n                                v - base, h).normalized())\n\n\nif __name__ == \"__main__\":  # pragma: no cover\n    WidgetPreview(OWSilhouettePlot).run(Orange.data.Table(\"brown-selected\"))\n",
            "header": {
                "file": {
                    "path": "Orange/widgets/visualize/owsilhouetteplot.py"
                },
                "misc_line": null
            },
            "index_line": {
                "old_commit_hash": "82b0b33d8",
                "new_commit_hash": "3a4534164",
                "mode": "100644"
            },
            "is_binary_file": false,
            "binary_line": null,
            "minus_file": {
                "path": "a/Orange/widgets/visualize/owsilhouetteplot.py"
            },
            "plus_file": {
                "path": "b/Orange/widgets/visualize/owsilhouetteplot.py"
            },
            "hunks": [
                {
                    "descriptor": {
                        "old_range": {
                            "start": 267,
                            "length": 7
                        },
                        "new_range": {
                            "start": 267,
                            "length": 7
                        },
                        "section": "class OWSilhouettePlot(widget.OWWidget):"
                    },
                    "line_group": {
                        "all_lines": [
                            {
                                "content": "                     if var.is_string or var.is_discrete]",
                                "type": "context"
                            },
                            {
                                "content": "        self.annotation_var_model[:] = [None] + annotvars",
                                "type": "context"
                            },
                            {
                                "content": "        self.annotation_var = annotvars[0] if annotvars else None",
                                "type": "context"
                            },
                            {
                                "content": "        self.openContext(Orange.data.Domain(groupvars))",
                                "type": "deleted"
                            },
                            {
                                "content": "        self.openContext(domain)",
                                "type": "added"
                            },
                            {
                                "content": "",
                                "type": "context"
                            },
                            {
                                "content": "    def _is_empty(self) -> bool:",
                                "type": "context"
                            },
                            {
                                "content": "        # Is empty (does not have any input).",
                                "type": "context"
                            }
                        ]
                    },
                    "modified_entities": [
                        {
                            "file_name": "Orange/widgets/visualize/owsilhouetteplot.py",
                            "type": "class",
                            "name": "OWSilhouettePlot",
                            "content": "class OWSilhouettePlot(widget.OWWidget):\n    name = \"Silhouette Plot\"\n    description = \"Visually assess cluster quality and \" \\\n                  \"the degree of cluster membership.\"\n\n    icon = \"icons/SilhouettePlot.svg\"\n    priority = 300\n    keywords = []\n\n    class Inputs:\n        data = Input(\"Data\", (Orange.data.Table, Orange.misc.DistMatrix))\n\n    class Outputs:\n        selected_data = Output(\"Selected Data\", Orange.data.Table, default=True)\n        annotated_data = Output(ANNOTATED_DATA_SIGNAL_NAME, Orange.data.Table)\n\n    replaces = [\n        \"orangecontrib.prototypes.widgets.owsilhouetteplot.OWSilhouettePlot\",\n        \"Orange.widgets.unsupervised.owsilhouetteplot.OWSilhouettePlot\"\n    ]\n\n    settingsHandler = settings.DomainContextHandler()\n    settings_version = 2\n\n    #: Distance metric index\n    distance_idx = settings.Setting(0)\n    #: Group/cluster variable\n    cluster_var = settings.ContextSetting(None)\n    #: Annotation variable\n    annotation_var = settings.ContextSetting(None)\n    #: Group the (displayed) silhouettes by cluster\n    group_by_cluster = settings.Setting(True)\n    #: A fixed size for an instance bar\n    bar_size = settings.Setting(3)\n    #: Add silhouette scores to output data\n    auto_commit = settings.Setting(True)\n\n    pending_selection = settings.Setting(None, schema_only=True)\n\n    Distances = [(\"Euclidean\", Orange.distance.Euclidean),\n                 (\"Manhattan\", Orange.distance.Manhattan),\n                 (\"Cosine\", Orange.distance.Cosine)]\n\n    graph_name = \"scene\"\n\n    class Error(widget.OWWidget.Error):\n        need_two_clusters = Msg(\"Need at least two non-empty clusters\")\n        singleton_clusters_all = Msg(\"All clusters are singletons\")\n        memory_error = Msg(\"Not enough memory\")\n        value_error = Msg(\"Distances could not be computed: '{}'\")\n        input_validation_error = Msg(\"{}\")\n\n    class Warning(widget.OWWidget.Warning):\n        missing_cluster_assignment = Msg(\n            \"{} instance{s} omitted (missing cluster assignment)\")\n        nan_distances = Msg(\"{} instance{s} omitted (undefined distances)\")\n        ignoring_categorical = Msg(\"Ignoring categorical features\")\n\n    def __init__(self):\n        super().__init__()\n        #: The input data\n        self.data = None         # type: Optional[Orange.data.Table]\n        #: The input distance matrix (if present)\n        self.distances = None  # type: Optional[Orange.misc.DistMatrix]\n        #: The effective distance matrix (is self.distances or computed from\n        #: self.data depending on input)\n        self._matrix = None      # type: Optional[Orange.misc.DistMatrix]\n        #: An bool mask (size == len(data)) indicating missing group/cluster\n        #: assignments\n        self._mask = None        # type: Optional[np.ndarray]\n        #: An array of cluster/group labels for instances with valid group\n        #: assignment\n        self._labels = None      # type: Optional[np.ndarray]\n        #: An array of silhouette scores for instances with valid group\n        #: assignment\n        self._silhouette = None  # type: Optional[np.ndarray]\n        self._silplot = None     # type: Optional[SilhouettePlot]\n\n        controllayout = self.controlArea.layout()\n        assert isinstance(controllayout, QVBoxLayout)\n        self._distances_gui_box = distbox = gui.widgetBox(\n            None, \"Distance\"\n        )\n        self._distances_gui_cb = gui.comboBox(\n            distbox, self, \"distance_idx\",\n            items=[name for name, _ in OWSilhouettePlot.Distances],\n            orientation=Qt.Horizontal, callback=self._invalidate_distances)\n        controllayout.addWidget(distbox)\n\n        box = gui.vBox(self.controlArea, \"Grouping\")\n        self.cluster_var_model = itemmodels.VariableListModel(\n            parent=self, placeholder=\"(None)\")\n        self.cluster_var_cb = gui.comboBox(\n            box, self, \"cluster_var\", contentsLength=14,\n            searchable=True, callback=self._invalidate_scores,\n            model=self.cluster_var_model\n        )\n        gui.checkBox(\n            box, self, \"group_by_cluster\", \"Show in groups\",\n            callback=self._replot)\n\n        box = gui.vBox(self.controlArea, \"Bars\")\n        gui.widgetLabel(box, \"Bar width:\")\n        gui.hSlider(\n            box, self, \"bar_size\", minValue=1, maxValue=10, step=1,\n            callback=self._update_bar_size)\n        gui.widgetLabel(box, \"Annotations:\")\n        self.annotation_var_model = itemmodels.VariableListModel(parent=self)\n        self.annotation_var_model[:] = [None]\n        self.annotation_cb = gui.comboBox(\n            box, self, \"annotation_var\", contentsLength=14,\n            callback=self._update_annotations,\n            model=self.annotation_var_model)\n        ibox = gui.indentedBox(box, 5)\n        self.ann_hidden_warning = warning = gui.widgetLabel(\n            ibox, \"(increase the width to show)\")\n        ibox.setFixedWidth(ibox.sizeHint().width())\n        warning.setVisible(False)\n\n        gui.rubber(self.controlArea)\n\n        gui.auto_send(self.buttonsArea, self, \"auto_commit\")\n\n        self.scene = GraphicsScene(self)\n        self.view = StyledGraphicsView(self.scene)\n        self.view.setRenderHint(QPainter.Antialiasing, True)\n        self.view.setAlignment(Qt.AlignTop | Qt.AlignLeft)\n        self.mainArea.layout().addWidget(self.view)\n\n        self.settingsAboutToBePacked.connect(self.pack_settings)\n\n    def sizeHint(self):\n        sh = self.controlArea.sizeHint()\n        return sh.expandedTo(QSize(600, 720))\n\n    def pack_settings(self):\n        if self.data and self._silplot is not None:\n            self.pending_selection = list(self._silplot.selection())\n        else:\n            self.pending_selection = None\n\n    @Inputs.data\n    @check_sql_input\n    def set_data(self, data: Union[Table, DistMatrix, None]):\n        \"\"\"\n        Set the input dataset or distance matrix.\n        \"\"\"\n        self.closeContext()\n        self.clear()\n        try:\n            if isinstance(data, Orange.misc.DistMatrix):\n                self._set_distances(data)\n            elif isinstance(data, Orange.data.Table):\n                self._set_table(data)\n            else:\n                self.distances = None\n                self.data = None\n        except InputValidationError as err:\n            self.Error.input_validation_error(err.message)\n            self.distances = None\n            self.data = None\n\n    def _set_table(self, data: Table):\n        self._setup_control_models(data.domain)\n        self.data = data\n        self.distances = None\n\n    def _set_distances(self, distances: DistMatrix):\n        if isinstance(distances.row_items, Orange.data.Table) and \\\n                distances.axis == 1:\n            data = distances.row_items\n        else:\n            raise ValidationError(\"Input matrix does not have associated data\")\n\n        if data is not None:\n            self._setup_control_models(data.domain)\n            self.distances = distances\n            self.data = data\n\n    def handleNewSignals(self):\n        if not self._is_empty():\n            self._update()\n            self._replot()\n            if self.pending_selection is not None and self._silplot is not None:\n                # If selection contains indices that are too large, the data\n                # file must had been modified, so we ignore selection\n                if max(self.pending_selection, default=-1) < len(self.data):\n                    self._silplot.setSelection(np.array(self.pending_selection))\n                self.pending_selection = None\n\n        # Disable/enable the Distances GUI controls if applicable\n        self._distances_gui_box.setEnabled(self.distances is None)\n\n        self.commit.now()\n\n    def _setup_control_models(self, domain: Domain):\n        groupvars = [\n            v for v in domain.variables + domain.metas\n            if v.is_discrete and len(v.values) >= 2]\n        if not groupvars:\n            raise NoGroupVariable()\n        self.cluster_var_model[:] = groupvars\n        if domain.class_var in groupvars:\n            self.cluster_var = domain.class_var\n        else:\n            self.cluster_var = groupvars[0]\n        annotvars = [var for var in domain.variables + domain.metas\n                     if var.is_string or var.is_discrete]\n        self.annotation_var_model[:] = [None] + annotvars\n        self.annotation_var = annotvars[0] if annotvars else None\n        self.openContext(Orange.data.Domain(groupvars))\n\n    def _is_empty(self) -> bool:\n        # Is empty (does not have any input).\n        return (self.data is None or len(self.data) == 0) \\\n               and self.distances is None\n\n    def clear(self):\n        \"\"\"\n        Clear the widget state.\n        \"\"\"\n        self.data = None\n        self.distances = None\n        self._matrix = None\n        self._mask = None\n        self._silhouette = None\n        self._labels = None\n        self.cluster_var_model[:] = []\n        self.annotation_var_model[:] = [None]\n        self._clear_scene()\n        self.Error.clear()\n        self.Warning.clear()\n\n    def _clear_scene(self):\n        # Clear the graphics scene and associated objects\n        self.scene.clear()\n        self.scene.setSceneRect(QRectF())\n        self.view.setSceneRect(QRectF())\n        self.view.setHeaderSceneRect(QRectF())\n        self.view.setFooterSceneRect(QRectF())\n        self._silplot = None\n\n    def _invalidate_distances(self):\n        # Invalidate the computed distance matrix and recompute the silhouette.\n        self._matrix = None\n        self._invalidate_scores()\n\n    def _invalidate_scores(self):\n        # Invalidate and recompute the current silhouette scores.\n        self._labels = self._silhouette = self._mask = None\n        self._update()\n        self._replot()\n        if self.data is not None:\n            self.commit.deferred()\n\n    def _ensure_matrix(self):\n        # ensure self._matrix is computed if necessary\n        if self._is_empty():\n            return\n        if self._matrix is None:\n            if self.distances is not None:\n                self._matrix = np.asarray(self.distances)\n            elif self.data is not None:\n                data = self.data\n                _, metric = self.Distances[self.distance_idx]\n                if not metric.supports_discrete and any(\n                        a.is_discrete for a in data.domain.attributes):\n                    self.Warning.ignoring_categorical()\n                    data = Orange.distance.remove_discrete_features(data)\n                try:\n                    self._matrix = np.asarray(metric(data))\n                except MemoryError:\n                    self.Error.memory_error()\n                    return\n                except ValueError as err:\n                    self.Error.value_error(str(err))\n                    return\n            else:\n                assert False, \"invalid state\"\n\n    def _update(self):\n        # Update/recompute the effective distances and scores as required.\n        self._clear_messages()\n        if self._is_empty():\n            self._reset_all()\n            return\n\n        self._ensure_matrix()\n        if self._matrix is None:\n            return\n\n        labels, _ = self.data.get_column_view(self.cluster_var)\n        labels = np.asarray(labels, dtype=float)\n        cluster_mask = np.isnan(labels)\n        dist_mask = np.isnan(self._matrix).all(axis=0)\n        mask = cluster_mask | dist_mask\n        labels = labels.astype(int)\n        labels = labels[~mask]\n\n        labels_unq = np.unique(labels)\n\n        if len(labels_unq) < 2:\n            self.Error.need_two_clusters()\n            labels = silhouette = mask = None\n        elif len(labels_unq) == len(labels):\n            self.Error.singleton_clusters_all()\n            labels = silhouette = mask = None\n        else:\n            silhouette = sklearn.metrics.silhouette_samples(\n                self._matrix[~mask, :][:, ~mask], labels, metric=\"precomputed\")\n        self._mask = mask\n        self._labels = labels\n        self._silhouette = silhouette\n\n        if mask is not None:\n            count_missing = np.count_nonzero(cluster_mask)\n            if count_missing:\n                self.Warning.missing_cluster_assignment(\n                    count_missing, s=\"s\" if count_missing > 1 else \"\")\n            count_nandist = np.count_nonzero(dist_mask)\n            if count_nandist:\n                self.Warning.nan_distances(\n                    count_nandist, s=\"s\" if count_nandist > 1 else \"\")\n\n    def _reset_all(self):\n        self._mask = None\n        self._silhouette = None\n        self._labels = None\n        self._matrix = None\n        self._clear_scene()\n\n    def _clear_messages(self):\n        self.Error.clear()\n        self.Warning.clear()\n\n    def _set_bar_height(self):\n        visible = self.bar_size >= 5\n        self._silplot.setBarHeight(self.bar_size)\n        self._silplot.setRowNamesVisible(visible)\n        self.ann_hidden_warning.setVisible(\n            not visible and self.annotation_var is not None)\n\n    def _replot(self):\n        # Clear and replot/initialize the scene\n        self._clear_scene()\n        if self._silhouette is not None and self._labels is not None:\n            self._silplot = silplot = SilhouettePlot()\n            self._set_bar_height()\n\n            if self.group_by_cluster:\n                silplot.setScores(\n                    self._silhouette, self._labels,\n                    self.cluster_var.values, self.cluster_var.colors)\n            else:\n                silplot.setScores(\n                    self._silhouette,\n                    np.zeros(len(self._silhouette), dtype=int),\n                    [\"\"], np.array([[63, 207, 207]])\n                )\n\n            self.scene.addItem(silplot)\n            self._update_annotations()\n            silplot.selectionChanged.connect(self.commit.deferred)\n            silplot.layout().activate()\n            self._update_scene_rect()\n            silplot.geometryChanged.connect(self._update_scene_rect)\n\n    def _update_bar_size(self):\n        if self._silplot is not None:\n            self._set_bar_height()\n\n    def _update_annotations(self):\n        annot_var = self.annotation_var\n        self.ann_hidden_warning.setVisible(\n            self.bar_size < 5 and annot_var is not None)\n\n        if self._silplot is not None:\n            if annot_var is not None:\n                column, _ = self.data.get_column_view(annot_var)\n                if self._mask is not None:\n                    assert column.shape == self._mask.shape\n                    # pylint: disable=invalid-unary-operand-type\n                    column = column[~self._mask]\n                self._silplot.setRowNames(\n                    [annot_var.str_val(value) for value in column])\n            else:\n                self._silplot.setRowNames(None)\n\n    def _update_scene_rect(self):\n        geom = self._silplot.geometry()\n        self.scene.setSceneRect(geom)\n        self.view.setSceneRect(geom)\n\n        header = self._silplot.topScaleItem()\n        footer = self._silplot.bottomScaleItem()\n\n        def extend_horizontal(rect):\n            # type: (QRectF) -> QRectF\n            rect = QRectF(rect)\n            rect.setLeft(geom.left())\n            rect.setRight(geom.right())\n            return rect\n\n        margin = 3\n        if header is not None:\n            self.view.setHeaderSceneRect(\n                extend_horizontal(header.geometry().adjusted(0, 0, 0, margin)))\n        if footer is not None:\n            self.view.setFooterSceneRect(\n                extend_horizontal(footer.geometry().adjusted(0, -margin, 0, 0)))\n\n    @gui.deferred\n    def commit(self):\n        \"\"\"\n        Commit/send the current selection to the output.\n        \"\"\"\n        selected = indices = data = None\n        if self.data is not None:\n            selectedmask = np.full(len(self.data), False, dtype=bool)\n            if self._silplot is not None:\n                indices = self._silplot.selection()\n                assert (np.diff(indices) > 0).all(), \"strictly increasing\"\n                if self._mask is not None:\n                    # pylint: disable=invalid-unary-operand-type\n                    indices = np.flatnonzero(~self._mask)[indices]\n                selectedmask[indices] = True\n\n            if self._mask is not None:\n                scores = np.full(shape=selectedmask.shape,\n                                 fill_value=np.nan)\n                # pylint: disable=invalid-unary-operand-type\n                scores[~self._mask] = self._silhouette\n            else:\n                scores = self._silhouette\n\n            domain = self.data.domain\n            proposed = \"Silhouette ({})\".format(escape(self.cluster_var.name))\n            names = [var.name for var in itertools.chain(domain.attributes,\n                                                         domain.class_vars,\n                                                         domain.metas)]\n            unique = get_unique_names(names, proposed)\n            silhouette_var = Orange.data.ContinuousVariable(unique)\n            domain = Orange.data.Domain(\n                domain.attributes,\n                domain.class_vars,\n                domain.metas + (silhouette_var, ))\n\n            if np.count_nonzero(selectedmask):\n                selected = self.data.from_table(\n                    domain, self.data, np.flatnonzero(selectedmask))\n\n            if selected is not None:\n                with selected.unlocked(selected.metas):\n                    selected[:, silhouette_var] = np.c_[scores[selectedmask]]\n\n            data = self.data.transform(domain)\n            with data.unlocked(data.metas):\n                data[:, silhouette_var] = np.c_[scores]\n\n        self.Outputs.selected_data.send(selected)\n        self.Outputs.annotated_data.send(create_annotated_table(data, indices))\n\n    def send_report(self):\n        if not len(self.cluster_var_model):\n            return\n\n        self.report_plot()\n        caption = \"Silhouette plot \" \\\n                  f\"({self.Distances[self.distance_idx][0]} distance), \" \\\n                  f\"clustered by '{self.cluster_var.name}'\"\n        if self.annotation_var and self._silplot.rowNamesVisible():\n            caption += f\", annotated with '{self.annotation_var.name}'\"\n        self.report_caption(caption)\n\n    def onDeleteWidget(self):\n        self.clear()\n        super().onDeleteWidget()\n\n    @classmethod\n    def migrate_context(cls, context, version):\n        values = context.values\n        if version < 2:\n            discrete_vars = (\n                name\n                for name, type_ in itertools.chain(\n                    context.attributes, context.class_vars, context.metas)\n                if type_ == 1)\n            index, _ = values.pop(\"cluster_var_idx\")\n            name = next(itertools.islice(discrete_vars, index, None)\n            )\n            values[\"cluster_var\"] = (name, 101)\n\n            index = values.pop(\"annotation_var_idx\")[0] - 1\n            if index == -1:\n                values[\"annotation_var\"] = None\n            else:\n                annot_vars = (\n                    (name, type_ + 100)\n                    for name, type_ in itertools.chain(\n                        context.attributes, context.class_vars, context.metas)\n                    if type_ in (1, 3))\n                values[\"annotation_var\"] = next(\n                    itertools.islice(annot_vars, index, None))",
                            "ast_type_str": "ClassDef",
                            "start_lineno": 60,
                            "end_lineno": 562,
                            "parent": null
                        },
                        {
                            "file_name": "Orange/widgets/visualize/owsilhouetteplot.py",
                            "type": "method",
                            "name": "OWSilhouettePlot._setup_control_models",
                            "content": "class OWSilhouettePlot(widget.OWWidget):\n    name = \"Silhouette Plot\"\n    description = \"Visually assess cluster quality and \" \\\n                  \"the degree of cluster membership.\"\n\n    icon = \"icons/SilhouettePlot.svg\"\n    priority = 300\n    keywords = []\n\n    class Inputs:\n        data = Input(\"Data\", (Orange.data.Table, Orange.misc.DistMatrix))\n\n    class Outputs:\n        selected_data = Output(\"Selected Data\", Orange.data.Table, default=True)\n        annotated_data = Output(ANNOTATED_DATA_SIGNAL_NAME, Orange.data.Table)\n\n    replaces = [\n        \"orangecontrib.prototypes.widgets.owsilhouetteplot.OWSilhouettePlot\",\n        \"Orange.widgets.unsupervised.owsilhouetteplot.OWSilhouettePlot\"\n    ]\n\n    settingsHandler = settings.DomainContextHandler()\n    settings_version = 2\n\n    #: Distance metric index\n    distance_idx = settings.Setting(0)\n    #: Group/cluster variable\n    cluster_var = settings.ContextSetting(None)\n    #: Annotation variable\n    annotation_var = settings.ContextSetting(None)\n    #: Group the (displayed) silhouettes by cluster\n    group_by_cluster = settings.Setting(True)\n    #: A fixed size for an instance bar\n    bar_size = settings.Setting(3)\n    #: Add silhouette scores to output data\n    auto_commit = settings.Setting(True)\n\n    pending_selection = settings.Setting(None, schema_only=True)\n\n    Distances = [(\"Euclidean\", Orange.distance.Euclidean),\n                 (\"Manhattan\", Orange.distance.Manhattan),\n                 (\"Cosine\", Orange.distance.Cosine)]\n\n    graph_name = \"scene\"\n\n    class Error(widget.OWWidget.Error):\n        need_two_clusters = Msg(\"Need at least two non-empty clusters\")\n        singleton_clusters_all = Msg(\"All clusters are singletons\")\n        memory_error = Msg(\"Not enough memory\")\n        value_error = Msg(\"Distances could not be computed: '{}'\")\n        input_validation_error = Msg(\"{}\")\n\n    class Warning(widget.OWWidget.Warning):\n        missing_cluster_assignment = Msg(\n            \"{} instance{s} omitted (missing cluster assignment)\")\n        nan_distances = Msg(\"{} instance{s} omitted (undefined distances)\")\n        ignoring_categorical = Msg(\"Ignoring categorical features\")\n\n    def __init__(self):\n        super().__init__()\n        #: The input data\n        self.data = None         # type: Optional[Orange.data.Table]\n        #: The input distance matrix (if present)\n        self.distances = None  # type: Optional[Orange.misc.DistMatrix]\n        #: The effective distance matrix (is self.distances or computed from\n        #: self.data depending on input)\n        self._matrix = None      # type: Optional[Orange.misc.DistMatrix]\n        #: An bool mask (size == len(data)) indicating missing group/cluster\n        #: assignments\n        self._mask = None        # type: Optional[np.ndarray]\n        #: An array of cluster/group labels for instances with valid group\n        #: assignment\n        self._labels = None      # type: Optional[np.ndarray]\n        #: An array of silhouette scores for instances with valid group\n        #: assignment\n        self._silhouette = None  # type: Optional[np.ndarray]\n        self._silplot = None     # type: Optional[SilhouettePlot]\n\n        controllayout = self.controlArea.layout()\n        assert isinstance(controllayout, QVBoxLayout)\n        self._distances_gui_box = distbox = gui.widgetBox(\n            None, \"Distance\"\n        )\n        self._distances_gui_cb = gui.comboBox(\n            distbox, self, \"distance_idx\",\n            items=[name for name, _ in OWSilhouettePlot.Distances],\n            orientation=Qt.Horizontal, callback=self._invalidate_distances)\n        controllayout.addWidget(distbox)\n\n        box = gui.vBox(self.controlArea, \"Grouping\")\n        self.cluster_var_model = itemmodels.VariableListModel(\n            parent=self, placeholder=\"(None)\")\n        self.cluster_var_cb = gui.comboBox(\n            box, self, \"cluster_var\", contentsLength=14,\n            searchable=True, callback=self._invalidate_scores,\n            model=self.cluster_var_model\n        )\n        gui.checkBox(\n            box, self, \"group_by_cluster\", \"Show in groups\",\n            callback=self._replot)\n\n        box = gui.vBox(self.controlArea, \"Bars\")\n        gui.widgetLabel(box, \"Bar width:\")\n        gui.hSlider(\n            box, self, \"bar_size\", minValue=1, maxValue=10, step=1,\n            callback=self._update_bar_size)\n        gui.widgetLabel(box, \"Annotations:\")\n        self.annotation_var_model = itemmodels.VariableListModel(parent=self)\n        self.annotation_var_model[:] = [None]\n        self.annotation_cb = gui.comboBox(\n            box, self, \"annotation_var\", contentsLength=14,\n            callback=self._update_annotations,\n            model=self.annotation_var_model)\n        ibox = gui.indentedBox(box, 5)\n        self.ann_hidden_warning = warning = gui.widgetLabel(\n            ibox, \"(increase the width to show)\")\n        ibox.setFixedWidth(ibox.sizeHint().width())\n        warning.setVisible(False)\n\n        gui.rubber(self.controlArea)\n\n        gui.auto_send(self.buttonsArea, self, \"auto_commit\")\n\n        self.scene = GraphicsScene(self)\n        self.view = StyledGraphicsView(self.scene)\n        self.view.setRenderHint(QPainter.Antialiasing, True)\n        self.view.setAlignment(Qt.AlignTop | Qt.AlignLeft)\n        self.mainArea.layout().addWidget(self.view)\n\n        self.settingsAboutToBePacked.connect(self.pack_settings)\n\n    def sizeHint(self):\n        sh = self.controlArea.sizeHint()\n        return sh.expandedTo(QSize(600, 720))\n\n    def pack_settings(self):\n        if self.data and self._silplot is not None:\n            self.pending_selection = list(self._silplot.selection())\n        else:\n            self.pending_selection = None\n\n    @Inputs.data\n    @check_sql_input\n    def set_data(self, data: Union[Table, DistMatrix, None]):\n        \"\"\"\n        Set the input dataset or distance matrix.\n        \"\"\"\n        self.closeContext()\n        self.clear()\n        try:\n            if isinstance(data, Orange.misc.DistMatrix):\n                self._set_distances(data)\n            elif isinstance(data, Orange.data.Table):\n                self._set_table(data)\n            else:\n                self.distances = None\n                self.data = None\n        except InputValidationError as err:\n            self.Error.input_validation_error(err.message)\n            self.distances = None\n            self.data = None\n\n    def _set_table(self, data: Table):\n        self._setup_control_models(data.domain)\n        self.data = data\n        self.distances = None\n\n    def _set_distances(self, distances: DistMatrix):\n        if isinstance(distances.row_items, Orange.data.Table) and \\\n                distances.axis == 1:\n            data = distances.row_items\n        else:\n            raise ValidationError(\"Input matrix does not have associated data\")\n\n        if data is not None:\n            self._setup_control_models(data.domain)\n            self.distances = distances\n            self.data = data\n\n    def handleNewSignals(self):\n        if not self._is_empty():\n            self._update()\n            self._replot()\n            if self.pending_selection is not None and self._silplot is not None:\n                # If selection contains indices that are too large, the data\n                # file must had been modified, so we ignore selection\n                if max(self.pending_selection, default=-1) < len(self.data):\n                    self._silplot.setSelection(np.array(self.pending_selection))\n                self.pending_selection = None\n\n        # Disable/enable the Distances GUI controls if applicable\n        self._distances_gui_box.setEnabled(self.distances is None)\n\n        self.commit.now()\n\n    def _setup_control_models(self, domain: Domain):\n        groupvars = [\n            v for v in domain.variables + domain.metas\n            if v.is_discrete and len(v.values) >= 2]\n        if not groupvars:\n            raise NoGroupVariable()\n        self.cluster_var_model[:] = groupvars\n        if domain.class_var in groupvars:\n            self.cluster_var = domain.class_var\n        else:\n            self.cluster_var = groupvars[0]\n        annotvars = [var for var in domain.variables + domain.metas\n                     if var.is_string or var.is_discrete]\n        self.annotation_var_model[:] = [None] + annotvars\n        self.annotation_var = annotvars[0] if annotvars else None\n        self.openContext(Orange.data.Domain(groupvars))\n\n    def _is_empty(self) -> bool:\n        # Is empty (does not have any input).\n        return (self.data is None or len(self.data) == 0) \\\n               and self.distances is None\n\n    def clear(self):\n        \"\"\"\n        Clear the widget state.\n        \"\"\"\n        self.data = None\n        self.distances = None\n        self._matrix = None\n        self._mask = None\n        self._silhouette = None\n        self._labels = None\n        self.cluster_var_model[:] = []\n        self.annotation_var_model[:] = [None]\n        self._clear_scene()\n        self.Error.clear()\n        self.Warning.clear()\n\n    def _clear_scene(self):\n        # Clear the graphics scene and associated objects\n        self.scene.clear()\n        self.scene.setSceneRect(QRectF())\n        self.view.setSceneRect(QRectF())\n        self.view.setHeaderSceneRect(QRectF())\n        self.view.setFooterSceneRect(QRectF())\n        self._silplot = None\n\n    def _invalidate_distances(self):\n        # Invalidate the computed distance matrix and recompute the silhouette.\n        self._matrix = None\n        self._invalidate_scores()\n\n    def _invalidate_scores(self):\n        # Invalidate and recompute the current silhouette scores.\n        self._labels = self._silhouette = self._mask = None\n        self._update()\n        self._replot()\n        if self.data is not None:\n            self.commit.deferred()\n\n    def _ensure_matrix(self):\n        # ensure self._matrix is computed if necessary\n        if self._is_empty():\n            return\n        if self._matrix is None:\n            if self.distances is not None:\n                self._matrix = np.asarray(self.distances)\n            elif self.data is not None:\n                data = self.data\n                _, metric = self.Distances[self.distance_idx]\n                if not metric.supports_discrete and any(\n                        a.is_discrete for a in data.domain.attributes):\n                    self.Warning.ignoring_categorical()\n                    data = Orange.distance.remove_discrete_features(data)\n                try:\n                    self._matrix = np.asarray(metric(data))\n                except MemoryError:\n                    self.Error.memory_error()\n                    return\n                except ValueError as err:\n                    self.Error.value_error(str(err))\n                    return\n            else:\n                assert False, \"invalid state\"\n\n    def _update(self):\n        # Update/recompute the effective distances and scores as required.\n        self._clear_messages()\n        if self._is_empty():\n            self._reset_all()\n            return\n\n        self._ensure_matrix()\n        if self._matrix is None:\n            return\n\n        labels, _ = self.data.get_column_view(self.cluster_var)\n        labels = np.asarray(labels, dtype=float)\n        cluster_mask = np.isnan(labels)\n        dist_mask = np.isnan(self._matrix).all(axis=0)\n        mask = cluster_mask | dist_mask\n        labels = labels.astype(int)\n        labels = labels[~mask]\n\n        labels_unq = np.unique(labels)\n\n        if len(labels_unq) < 2:\n            self.Error.need_two_clusters()\n            labels = silhouette = mask = None\n        elif len(labels_unq) == len(labels):\n            self.Error.singleton_clusters_all()\n            labels = silhouette = mask = None\n        else:\n            silhouette = sklearn.metrics.silhouette_samples(\n                self._matrix[~mask, :][:, ~mask], labels, metric=\"precomputed\")\n        self._mask = mask\n        self._labels = labels\n        self._silhouette = silhouette\n\n        if mask is not None:\n            count_missing = np.count_nonzero(cluster_mask)\n            if count_missing:\n                self.Warning.missing_cluster_assignment(\n                    count_missing, s=\"s\" if count_missing > 1 else \"\")\n            count_nandist = np.count_nonzero(dist_mask)\n            if count_nandist:\n                self.Warning.nan_distances(\n                    count_nandist, s=\"s\" if count_nandist > 1 else \"\")\n\n    def _reset_all(self):\n        self._mask = None\n        self._silhouette = None\n        self._labels = None\n        self._matrix = None\n        self._clear_scene()\n\n    def _clear_messages(self):\n        self.Error.clear()\n        self.Warning.clear()\n\n    def _set_bar_height(self):\n        visible = self.bar_size >= 5\n        self._silplot.setBarHeight(self.bar_size)\n        self._silplot.setRowNamesVisible(visible)\n        self.ann_hidden_warning.setVisible(\n            not visible and self.annotation_var is not None)\n\n    def _replot(self):\n        # Clear and replot/initialize the scene\n        self._clear_scene()\n        if self._silhouette is not None and self._labels is not None:\n            self._silplot = silplot = SilhouettePlot()\n            self._set_bar_height()\n\n            if self.group_by_cluster:\n                silplot.setScores(\n                    self._silhouette, self._labels,\n                    self.cluster_var.values, self.cluster_var.colors)\n            else:\n                silplot.setScores(\n                    self._silhouette,\n                    np.zeros(len(self._silhouette), dtype=int),\n                    [\"\"], np.array([[63, 207, 207]])\n                )\n\n            self.scene.addItem(silplot)\n            self._update_annotations()\n            silplot.selectionChanged.connect(self.commit.deferred)\n            silplot.layout().activate()\n            self._update_scene_rect()\n            silplot.geometryChanged.connect(self._update_scene_rect)\n\n    def _update_bar_size(self):\n        if self._silplot is not None:\n            self._set_bar_height()\n\n    def _update_annotations(self):\n        annot_var = self.annotation_var\n        self.ann_hidden_warning.setVisible(\n            self.bar_size < 5 and annot_var is not None)\n\n        if self._silplot is not None:\n            if annot_var is not None:\n                column, _ = self.data.get_column_view(annot_var)\n                if self._mask is not None:\n                    assert column.shape == self._mask.shape\n                    # pylint: disable=invalid-unary-operand-type\n                    column = column[~self._mask]\n                self._silplot.setRowNames(\n                    [annot_var.str_val(value) for value in column])\n            else:\n                self._silplot.setRowNames(None)\n\n    def _update_scene_rect(self):\n        geom = self._silplot.geometry()\n        self.scene.setSceneRect(geom)\n        self.view.setSceneRect(geom)\n\n        header = self._silplot.topScaleItem()\n        footer = self._silplot.bottomScaleItem()\n\n        def extend_horizontal(rect):\n            # type: (QRectF) -> QRectF\n            rect = QRectF(rect)\n            rect.setLeft(geom.left())\n            rect.setRight(geom.right())\n            return rect\n\n        margin = 3\n        if header is not None:\n            self.view.setHeaderSceneRect(\n                extend_horizontal(header.geometry().adjusted(0, 0, 0, margin)))\n        if footer is not None:\n            self.view.setFooterSceneRect(\n                extend_horizontal(footer.geometry().adjusted(0, -margin, 0, 0)))\n\n    @gui.deferred\n    def commit(self):\n        \"\"\"\n        Commit/send the current selection to the output.\n        \"\"\"\n        selected = indices = data = None\n        if self.data is not None:\n            selectedmask = np.full(len(self.data), False, dtype=bool)\n            if self._silplot is not None:\n                indices = self._silplot.selection()\n                assert (np.diff(indices) > 0).all(), \"strictly increasing\"\n                if self._mask is not None:\n                    # pylint: disable=invalid-unary-operand-type\n                    indices = np.flatnonzero(~self._mask)[indices]\n                selectedmask[indices] = True\n\n            if self._mask is not None:\n                scores = np.full(shape=selectedmask.shape,\n                                 fill_value=np.nan)\n                # pylint: disable=invalid-unary-operand-type\n                scores[~self._mask] = self._silhouette\n            else:\n                scores = self._silhouette\n\n            domain = self.data.domain\n            proposed = \"Silhouette ({})\".format(escape(self.cluster_var.name))\n            names = [var.name for var in itertools.chain(domain.attributes,\n                                                         domain.class_vars,\n                                                         domain.metas)]\n            unique = get_unique_names(names, proposed)\n            silhouette_var = Orange.data.ContinuousVariable(unique)\n            domain = Orange.data.Domain(\n                domain.attributes,\n                domain.class_vars,\n                domain.metas + (silhouette_var, ))\n\n            if np.count_nonzero(selectedmask):\n                selected = self.data.from_table(\n                    domain, self.data, np.flatnonzero(selectedmask))\n\n            if selected is not None:\n                with selected.unlocked(selected.metas):\n                    selected[:, silhouette_var] = np.c_[scores[selectedmask]]\n\n            data = self.data.transform(domain)\n            with data.unlocked(data.metas):\n                data[:, silhouette_var] = np.c_[scores]\n\n        self.Outputs.selected_data.send(selected)\n        self.Outputs.annotated_data.send(create_annotated_table(data, indices))\n\n    def send_report(self):\n        if not len(self.cluster_var_model):\n            return\n\n        self.report_plot()\n        caption = \"Silhouette plot \" \\\n                  f\"({self.Distances[self.distance_idx][0]} distance), \" \\\n                  f\"clustered by '{self.cluster_var.name}'\"\n        if self.annotation_var and self._silplot.rowNamesVisible():\n            caption += f\", annotated with '{self.annotation_var.name}'\"\n        self.report_caption(caption)\n\n    def onDeleteWidget(self):\n        self.clear()\n        super().onDeleteWidget()\n\n    @classmethod\n    def migrate_context(cls, context, version):\n        values = context.values\n        if version < 2:\n            discrete_vars = (\n                name\n                for name, type_ in itertools.chain(\n                    context.attributes, context.class_vars, context.metas)\n                if type_ == 1)\n            index, _ = values.pop(\"cluster_var_idx\")\n            name = next(itertools.islice(discrete_vars, index, None)\n            )\n            values[\"cluster_var\"] = (name, 101)\n\n            index = values.pop(\"annotation_var_idx\")[0] - 1\n            if index == -1:\n                values[\"annotation_var\"] = None\n            else:\n                annot_vars = (\n                    (name, type_ + 100)\n                    for name, type_ in itertools.chain(\n                        context.attributes, context.class_vars, context.metas)\n                    if type_ in (1, 3))\n                values[\"annotation_var\"] = next(\n                    itertools.islice(annot_vars, index, None))",
                            "ast_type_str": "ClassDef",
                            "start_lineno": 255,
                            "end_lineno": 270,
                            "parent": {
                                "file_name": "Orange/widgets/visualize/owsilhouetteplot.py",
                                "type": "class",
                                "name": "OWSilhouettePlot",
                                "content": "class OWSilhouettePlot(widget.OWWidget):\n    name = \"Silhouette Plot\"\n    description = \"Visually assess cluster quality and \" \\\n                  \"the degree of cluster membership.\"\n\n    icon = \"icons/SilhouettePlot.svg\"\n    priority = 300\n    keywords = []\n\n    class Inputs:\n        data = Input(\"Data\", (Orange.data.Table, Orange.misc.DistMatrix))\n\n    class Outputs:\n        selected_data = Output(\"Selected Data\", Orange.data.Table, default=True)\n        annotated_data = Output(ANNOTATED_DATA_SIGNAL_NAME, Orange.data.Table)\n\n    replaces = [\n        \"orangecontrib.prototypes.widgets.owsilhouetteplot.OWSilhouettePlot\",\n        \"Orange.widgets.unsupervised.owsilhouetteplot.OWSilhouettePlot\"\n    ]\n\n    settingsHandler = settings.DomainContextHandler()\n    settings_version = 2\n\n    #: Distance metric index\n    distance_idx = settings.Setting(0)\n    #: Group/cluster variable\n    cluster_var = settings.ContextSetting(None)\n    #: Annotation variable\n    annotation_var = settings.ContextSetting(None)\n    #: Group the (displayed) silhouettes by cluster\n    group_by_cluster = settings.Setting(True)\n    #: A fixed size for an instance bar\n    bar_size = settings.Setting(3)\n    #: Add silhouette scores to output data\n    auto_commit = settings.Setting(True)\n\n    pending_selection = settings.Setting(None, schema_only=True)\n\n    Distances = [(\"Euclidean\", Orange.distance.Euclidean),\n                 (\"Manhattan\", Orange.distance.Manhattan),\n                 (\"Cosine\", Orange.distance.Cosine)]\n\n    graph_name = \"scene\"\n\n    class Error(widget.OWWidget.Error):\n        need_two_clusters = Msg(\"Need at least two non-empty clusters\")\n        singleton_clusters_all = Msg(\"All clusters are singletons\")\n        memory_error = Msg(\"Not enough memory\")\n        value_error = Msg(\"Distances could not be computed: '{}'\")\n        input_validation_error = Msg(\"{}\")\n\n    class Warning(widget.OWWidget.Warning):\n        missing_cluster_assignment = Msg(\n            \"{} instance{s} omitted (missing cluster assignment)\")\n        nan_distances = Msg(\"{} instance{s} omitted (undefined distances)\")\n        ignoring_categorical = Msg(\"Ignoring categorical features\")\n\n    def __init__(self):\n        super().__init__()\n        #: The input data\n        self.data = None         # type: Optional[Orange.data.Table]\n        #: The input distance matrix (if present)\n        self.distances = None  # type: Optional[Orange.misc.DistMatrix]\n        #: The effective distance matrix (is self.distances or computed from\n        #: self.data depending on input)\n        self._matrix = None      # type: Optional[Orange.misc.DistMatrix]\n        #: An bool mask (size == len(data)) indicating missing group/cluster\n        #: assignments\n        self._mask = None        # type: Optional[np.ndarray]\n        #: An array of cluster/group labels for instances with valid group\n        #: assignment\n        self._labels = None      # type: Optional[np.ndarray]\n        #: An array of silhouette scores for instances with valid group\n        #: assignment\n        self._silhouette = None  # type: Optional[np.ndarray]\n        self._silplot = None     # type: Optional[SilhouettePlot]\n\n        controllayout = self.controlArea.layout()\n        assert isinstance(controllayout, QVBoxLayout)\n        self._distances_gui_box = distbox = gui.widgetBox(\n            None, \"Distance\"\n        )\n        self._distances_gui_cb = gui.comboBox(\n            distbox, self, \"distance_idx\",\n            items=[name for name, _ in OWSilhouettePlot.Distances],\n            orientation=Qt.Horizontal, callback=self._invalidate_distances)\n        controllayout.addWidget(distbox)\n\n        box = gui.vBox(self.controlArea, \"Grouping\")\n        self.cluster_var_model = itemmodels.VariableListModel(\n            parent=self, placeholder=\"(None)\")\n        self.cluster_var_cb = gui.comboBox(\n            box, self, \"cluster_var\", contentsLength=14,\n            searchable=True, callback=self._invalidate_scores,\n            model=self.cluster_var_model\n        )\n        gui.checkBox(\n            box, self, \"group_by_cluster\", \"Show in groups\",\n            callback=self._replot)\n\n        box = gui.vBox(self.controlArea, \"Bars\")\n        gui.widgetLabel(box, \"Bar width:\")\n        gui.hSlider(\n            box, self, \"bar_size\", minValue=1, maxValue=10, step=1,\n            callback=self._update_bar_size)\n        gui.widgetLabel(box, \"Annotations:\")\n        self.annotation_var_model = itemmodels.VariableListModel(parent=self)\n        self.annotation_var_model[:] = [None]\n        self.annotation_cb = gui.comboBox(\n            box, self, \"annotation_var\", contentsLength=14,\n            callback=self._update_annotations,\n            model=self.annotation_var_model)\n        ibox = gui.indentedBox(box, 5)\n        self.ann_hidden_warning = warning = gui.widgetLabel(\n            ibox, \"(increase the width to show)\")\n        ibox.setFixedWidth(ibox.sizeHint().width())\n        warning.setVisible(False)\n\n        gui.rubber(self.controlArea)\n\n        gui.auto_send(self.buttonsArea, self, \"auto_commit\")\n\n        self.scene = GraphicsScene(self)\n        self.view = StyledGraphicsView(self.scene)\n        self.view.setRenderHint(QPainter.Antialiasing, True)\n        self.view.setAlignment(Qt.AlignTop | Qt.AlignLeft)\n        self.mainArea.layout().addWidget(self.view)\n\n        self.settingsAboutToBePacked.connect(self.pack_settings)\n\n    def sizeHint(self):\n        sh = self.controlArea.sizeHint()\n        return sh.expandedTo(QSize(600, 720))\n\n    def pack_settings(self):\n        if self.data and self._silplot is not None:\n            self.pending_selection = list(self._silplot.selection())\n        else:\n            self.pending_selection = None\n\n    @Inputs.data\n    @check_sql_input\n    def set_data(self, data: Union[Table, DistMatrix, None]):\n        \"\"\"\n        Set the input dataset or distance matrix.\n        \"\"\"\n        self.closeContext()\n        self.clear()\n        try:\n            if isinstance(data, Orange.misc.DistMatrix):\n                self._set_distances(data)\n            elif isinstance(data, Orange.data.Table):\n                self._set_table(data)\n            else:\n                self.distances = None\n                self.data = None\n        except InputValidationError as err:\n            self.Error.input_validation_error(err.message)\n            self.distances = None\n            self.data = None\n\n    def _set_table(self, data: Table):\n        self._setup_control_models(data.domain)\n        self.data = data\n        self.distances = None\n\n    def _set_distances(self, distances: DistMatrix):\n        if isinstance(distances.row_items, Orange.data.Table) and \\\n                distances.axis == 1:\n            data = distances.row_items\n        else:\n            raise ValidationError(\"Input matrix does not have associated data\")\n\n        if data is not None:\n            self._setup_control_models(data.domain)\n            self.distances = distances\n            self.data = data\n\n    def handleNewSignals(self):\n        if not self._is_empty():\n            self._update()\n            self._replot()\n            if self.pending_selection is not None and self._silplot is not None:\n                # If selection contains indices that are too large, the data\n                # file must had been modified, so we ignore selection\n                if max(self.pending_selection, default=-1) < len(self.data):\n                    self._silplot.setSelection(np.array(self.pending_selection))\n                self.pending_selection = None\n\n        # Disable/enable the Distances GUI controls if applicable\n        self._distances_gui_box.setEnabled(self.distances is None)\n\n        self.commit.now()\n\n    def _setup_control_models(self, domain: Domain):\n        groupvars = [\n            v for v in domain.variables + domain.metas\n            if v.is_discrete and len(v.values) >= 2]\n        if not groupvars:\n            raise NoGroupVariable()\n        self.cluster_var_model[:] = groupvars\n        if domain.class_var in groupvars:\n            self.cluster_var = domain.class_var\n        else:\n            self.cluster_var = groupvars[0]\n        annotvars = [var for var in domain.variables + domain.metas\n                     if var.is_string or var.is_discrete]\n        self.annotation_var_model[:] = [None] + annotvars\n        self.annotation_var = annotvars[0] if annotvars else None\n        self.openContext(Orange.data.Domain(groupvars))\n\n    def _is_empty(self) -> bool:\n        # Is empty (does not have any input).\n        return (self.data is None or len(self.data) == 0) \\\n               and self.distances is None\n\n    def clear(self):\n        \"\"\"\n        Clear the widget state.\n        \"\"\"\n        self.data = None\n        self.distances = None\n        self._matrix = None\n        self._mask = None\n        self._silhouette = None\n        self._labels = None\n        self.cluster_var_model[:] = []\n        self.annotation_var_model[:] = [None]\n        self._clear_scene()\n        self.Error.clear()\n        self.Warning.clear()\n\n    def _clear_scene(self):\n        # Clear the graphics scene and associated objects\n        self.scene.clear()\n        self.scene.setSceneRect(QRectF())\n        self.view.setSceneRect(QRectF())\n        self.view.setHeaderSceneRect(QRectF())\n        self.view.setFooterSceneRect(QRectF())\n        self._silplot = None\n\n    def _invalidate_distances(self):\n        # Invalidate the computed distance matrix and recompute the silhouette.\n        self._matrix = None\n        self._invalidate_scores()\n\n    def _invalidate_scores(self):\n        # Invalidate and recompute the current silhouette scores.\n        self._labels = self._silhouette = self._mask = None\n        self._update()\n        self._replot()\n        if self.data is not None:\n            self.commit.deferred()\n\n    def _ensure_matrix(self):\n        # ensure self._matrix is computed if necessary\n        if self._is_empty():\n            return\n        if self._matrix is None:\n            if self.distances is not None:\n                self._matrix = np.asarray(self.distances)\n            elif self.data is not None:\n                data = self.data\n                _, metric = self.Distances[self.distance_idx]\n                if not metric.supports_discrete and any(\n                        a.is_discrete for a in data.domain.attributes):\n                    self.Warning.ignoring_categorical()\n                    data = Orange.distance.remove_discrete_features(data)\n                try:\n                    self._matrix = np.asarray(metric(data))\n                except MemoryError:\n                    self.Error.memory_error()\n                    return\n                except ValueError as err:\n                    self.Error.value_error(str(err))\n                    return\n            else:\n                assert False, \"invalid state\"\n\n    def _update(self):\n        # Update/recompute the effective distances and scores as required.\n        self._clear_messages()\n        if self._is_empty():\n            self._reset_all()\n            return\n\n        self._ensure_matrix()\n        if self._matrix is None:\n            return\n\n        labels, _ = self.data.get_column_view(self.cluster_var)\n        labels = np.asarray(labels, dtype=float)\n        cluster_mask = np.isnan(labels)\n        dist_mask = np.isnan(self._matrix).all(axis=0)\n        mask = cluster_mask | dist_mask\n        labels = labels.astype(int)\n        labels = labels[~mask]\n\n        labels_unq = np.unique(labels)\n\n        if len(labels_unq) < 2:\n            self.Error.need_two_clusters()\n            labels = silhouette = mask = None\n        elif len(labels_unq) == len(labels):\n            self.Error.singleton_clusters_all()\n            labels = silhouette = mask = None\n        else:\n            silhouette = sklearn.metrics.silhouette_samples(\n                self._matrix[~mask, :][:, ~mask], labels, metric=\"precomputed\")\n        self._mask = mask\n        self._labels = labels\n        self._silhouette = silhouette\n\n        if mask is not None:\n            count_missing = np.count_nonzero(cluster_mask)\n            if count_missing:\n                self.Warning.missing_cluster_assignment(\n                    count_missing, s=\"s\" if count_missing > 1 else \"\")\n            count_nandist = np.count_nonzero(dist_mask)\n            if count_nandist:\n                self.Warning.nan_distances(\n                    count_nandist, s=\"s\" if count_nandist > 1 else \"\")\n\n    def _reset_all(self):\n        self._mask = None\n        self._silhouette = None\n        self._labels = None\n        self._matrix = None\n        self._clear_scene()\n\n    def _clear_messages(self):\n        self.Error.clear()\n        self.Warning.clear()\n\n    def _set_bar_height(self):\n        visible = self.bar_size >= 5\n        self._silplot.setBarHeight(self.bar_size)\n        self._silplot.setRowNamesVisible(visible)\n        self.ann_hidden_warning.setVisible(\n            not visible and self.annotation_var is not None)\n\n    def _replot(self):\n        # Clear and replot/initialize the scene\n        self._clear_scene()\n        if self._silhouette is not None and self._labels is not None:\n            self._silplot = silplot = SilhouettePlot()\n            self._set_bar_height()\n\n            if self.group_by_cluster:\n                silplot.setScores(\n                    self._silhouette, self._labels,\n                    self.cluster_var.values, self.cluster_var.colors)\n            else:\n                silplot.setScores(\n                    self._silhouette,\n                    np.zeros(len(self._silhouette), dtype=int),\n                    [\"\"], np.array([[63, 207, 207]])\n                )\n\n            self.scene.addItem(silplot)\n            self._update_annotations()\n            silplot.selectionChanged.connect(self.commit.deferred)\n            silplot.layout().activate()\n            self._update_scene_rect()\n            silplot.geometryChanged.connect(self._update_scene_rect)\n\n    def _update_bar_size(self):\n        if self._silplot is not None:\n            self._set_bar_height()\n\n    def _update_annotations(self):\n        annot_var = self.annotation_var\n        self.ann_hidden_warning.setVisible(\n            self.bar_size < 5 and annot_var is not None)\n\n        if self._silplot is not None:\n            if annot_var is not None:\n                column, _ = self.data.get_column_view(annot_var)\n                if self._mask is not None:\n                    assert column.shape == self._mask.shape\n                    # pylint: disable=invalid-unary-operand-type\n                    column = column[~self._mask]\n                self._silplot.setRowNames(\n                    [annot_var.str_val(value) for value in column])\n            else:\n                self._silplot.setRowNames(None)\n\n    def _update_scene_rect(self):\n        geom = self._silplot.geometry()\n        self.scene.setSceneRect(geom)\n        self.view.setSceneRect(geom)\n\n        header = self._silplot.topScaleItem()\n        footer = self._silplot.bottomScaleItem()\n\n        def extend_horizontal(rect):\n            # type: (QRectF) -> QRectF\n            rect = QRectF(rect)\n            rect.setLeft(geom.left())\n            rect.setRight(geom.right())\n            return rect\n\n        margin = 3\n        if header is not None:\n            self.view.setHeaderSceneRect(\n                extend_horizontal(header.geometry().adjusted(0, 0, 0, margin)))\n        if footer is not None:\n            self.view.setFooterSceneRect(\n                extend_horizontal(footer.geometry().adjusted(0, -margin, 0, 0)))\n\n    @gui.deferred\n    def commit(self):\n        \"\"\"\n        Commit/send the current selection to the output.\n        \"\"\"\n        selected = indices = data = None\n        if self.data is not None:\n            selectedmask = np.full(len(self.data), False, dtype=bool)\n            if self._silplot is not None:\n                indices = self._silplot.selection()\n                assert (np.diff(indices) > 0).all(), \"strictly increasing\"\n                if self._mask is not None:\n                    # pylint: disable=invalid-unary-operand-type\n                    indices = np.flatnonzero(~self._mask)[indices]\n                selectedmask[indices] = True\n\n            if self._mask is not None:\n                scores = np.full(shape=selectedmask.shape,\n                                 fill_value=np.nan)\n                # pylint: disable=invalid-unary-operand-type\n                scores[~self._mask] = self._silhouette\n            else:\n                scores = self._silhouette\n\n            domain = self.data.domain\n            proposed = \"Silhouette ({})\".format(escape(self.cluster_var.name))\n            names = [var.name for var in itertools.chain(domain.attributes,\n                                                         domain.class_vars,\n                                                         domain.metas)]\n            unique = get_unique_names(names, proposed)\n            silhouette_var = Orange.data.ContinuousVariable(unique)\n            domain = Orange.data.Domain(\n                domain.attributes,\n                domain.class_vars,\n                domain.metas + (silhouette_var, ))\n\n            if np.count_nonzero(selectedmask):\n                selected = self.data.from_table(\n                    domain, self.data, np.flatnonzero(selectedmask))\n\n            if selected is not None:\n                with selected.unlocked(selected.metas):\n                    selected[:, silhouette_var] = np.c_[scores[selectedmask]]\n\n            data = self.data.transform(domain)\n            with data.unlocked(data.metas):\n                data[:, silhouette_var] = np.c_[scores]\n\n        self.Outputs.selected_data.send(selected)\n        self.Outputs.annotated_data.send(create_annotated_table(data, indices))\n\n    def send_report(self):\n        if not len(self.cluster_var_model):\n            return\n\n        self.report_plot()\n        caption = \"Silhouette plot \" \\\n                  f\"({self.Distances[self.distance_idx][0]} distance), \" \\\n                  f\"clustered by '{self.cluster_var.name}'\"\n        if self.annotation_var and self._silplot.rowNamesVisible():\n            caption += f\", annotated with '{self.annotation_var.name}'\"\n        self.report_caption(caption)\n\n    def onDeleteWidget(self):\n        self.clear()\n        super().onDeleteWidget()\n\n    @classmethod\n    def migrate_context(cls, context, version):\n        values = context.values\n        if version < 2:\n            discrete_vars = (\n                name\n                for name, type_ in itertools.chain(\n                    context.attributes, context.class_vars, context.metas)\n                if type_ == 1)\n            index, _ = values.pop(\"cluster_var_idx\")\n            name = next(itertools.islice(discrete_vars, index, None)\n            )\n            values[\"cluster_var\"] = (name, 101)\n\n            index = values.pop(\"annotation_var_idx\")[0] - 1\n            if index == -1:\n                values[\"annotation_var\"] = None\n            else:\n                annot_vars = (\n                    (name, type_ + 100)\n                    for name, type_ in itertools.chain(\n                        context.attributes, context.class_vars, context.metas)\n                    if type_ in (1, 3))\n                values[\"annotation_var\"] = next(\n                    itertools.islice(annot_vars, index, None))",
                                "ast_type_str": "ClassDef",
                                "start_lineno": 60,
                                "end_lineno": 562,
                                "parent": null
                            }
                        }
                    ],
                    "added_entities": [],
                    "deleted_entities": []
                },
                {
                    "descriptor": {
                        "old_range": {
                            "start": 539,
                            "length": 27
                        },
                        "new_range": {
                            "start": 539,
                            "length": 22
                        },
                        "section": "class OWSilhouettePlot(widget.OWWidget):"
                    },
                    "line_group": {
                        "all_lines": [
                            {
                                "content": "    def migrate_context(cls, context, version):",
                                "type": "context"
                            },
                            {
                                "content": "        values = context.values",
                                "type": "context"
                            },
                            {
                                "content": "        if version < 2:",
                                "type": "context"
                            },
                            {
                                "content": "            discrete_vars = (",
                                "type": "deleted"
                            },
                            {
                                "content": "                name",
                                "type": "deleted"
                            },
                            {
                                "content": "                for name, type_ in itertools.chain(",
                                "type": "deleted"
                            },
                            {
                                "content": "                    context.attributes, context.class_vars, context.metas)",
                                "type": "deleted"
                            },
                            {
                                "content": "                if type_ == 1)",
                                "type": "deleted"
                            },
                            {
                                "content": "            # contexts were constructed from Domain containing vars shown in",
                                "type": "added"
                            },
                            {
                                "content": "            # the list view, context.class_vars and context.metas were always",
                                "type": "added"
                            },
                            {
                                "content": "            # empty, and context.attributes contained discrete attributes",
                                "type": "added"
                            },
                            {
                                "content": "            index, _ = values.pop(\"cluster_var_idx\")",
                                "type": "context"
                            },
                            {
                                "content": "            name = next(itertools.islice(discrete_vars, index, None)",
                                "type": "deleted"
                            },
                            {
                                "content": "            )",
                                "type": "deleted"
                            },
                            {
                                "content": "            values[\"cluster_var\"] = (name, 101)",
                                "type": "deleted"
                            },
                            {
                                "content": "            values[\"cluster_var\"] = (context.attributes[index][0], 101)",
                                "type": "added"
                            },
                            {
                                "content": "",
                                "type": "context"
                            },
                            {
                                "content": "            index = values.pop(\"annotation_var_idx\")[0] - 1",
                                "type": "context"
                            },
                            {
                                "content": "            if index == -1:",
                                "type": "context"
                            },
                            {
                                "content": "                values[\"annotation_var\"] = None",
                                "type": "context"
                            },
                            {
                                "content": "            else:",
                                "type": "deleted"
                            },
                            {
                                "content": "                annot_vars = (",
                                "type": "deleted"
                            },
                            {
                                "content": "                    (name, type_ + 100)",
                                "type": "deleted"
                            },
                            {
                                "content": "                    for name, type_ in itertools.chain(",
                                "type": "deleted"
                            },
                            {
                                "content": "                        context.attributes, context.class_vars, context.metas)",
                                "type": "deleted"
                            },
                            {
                                "content": "                    if type_ in (1, 3))",
                                "type": "deleted"
                            },
                            {
                                "content": "                values[\"annotation_var\"] = next(",
                                "type": "deleted"
                            },
                            {
                                "content": "                    itertools.islice(annot_vars, index, None))",
                                "type": "deleted"
                            },
                            {
                                "content": "            elif index < len(context.attributes):",
                                "type": "added"
                            },
                            {
                                "content": "                name, _ = context.attributes[index]",
                                "type": "added"
                            },
                            {
                                "content": "                values[\"annotation_var\"] = (name, 101)",
                                "type": "added"
                            },
                            {
                                "content": "            # else we cannot migrate",
                                "type": "added"
                            },
                            {
                                "content": "            # Even this migration can be erroneous if metas contained a mixture",
                                "type": "added"
                            },
                            {
                                "content": "            # of discrete and string attributes; the latter were not stored in",
                                "type": "added"
                            },
                            {
                                "content": "            # context, so indices in context could have been wrong",
                                "type": "added"
                            },
                            {
                                "content": "",
                                "type": "context"
                            },
                            {
                                "content": "",
                                "type": "context"
                            },
                            {
                                "content": "class SelectAction(enum.IntEnum):",
                                "type": "context"
                            }
                        ]
                    },
                    "modified_entities": [
                        {
                            "file_name": "Orange/widgets/visualize/owsilhouetteplot.py",
                            "type": "class",
                            "name": "OWSilhouettePlot",
                            "content": "class OWSilhouettePlot(widget.OWWidget):\n    name = \"Silhouette Plot\"\n    description = \"Visually assess cluster quality and \" \\\n                  \"the degree of cluster membership.\"\n\n    icon = \"icons/SilhouettePlot.svg\"\n    priority = 300\n    keywords = []\n\n    class Inputs:\n        data = Input(\"Data\", (Orange.data.Table, Orange.misc.DistMatrix))\n\n    class Outputs:\n        selected_data = Output(\"Selected Data\", Orange.data.Table, default=True)\n        annotated_data = Output(ANNOTATED_DATA_SIGNAL_NAME, Orange.data.Table)\n\n    replaces = [\n        \"orangecontrib.prototypes.widgets.owsilhouetteplot.OWSilhouettePlot\",\n        \"Orange.widgets.unsupervised.owsilhouetteplot.OWSilhouettePlot\"\n    ]\n\n    settingsHandler = settings.DomainContextHandler()\n    settings_version = 2\n\n    #: Distance metric index\n    distance_idx = settings.Setting(0)\n    #: Group/cluster variable\n    cluster_var = settings.ContextSetting(None)\n    #: Annotation variable\n    annotation_var = settings.ContextSetting(None)\n    #: Group the (displayed) silhouettes by cluster\n    group_by_cluster = settings.Setting(True)\n    #: A fixed size for an instance bar\n    bar_size = settings.Setting(3)\n    #: Add silhouette scores to output data\n    auto_commit = settings.Setting(True)\n\n    pending_selection = settings.Setting(None, schema_only=True)\n\n    Distances = [(\"Euclidean\", Orange.distance.Euclidean),\n                 (\"Manhattan\", Orange.distance.Manhattan),\n                 (\"Cosine\", Orange.distance.Cosine)]\n\n    graph_name = \"scene\"\n\n    class Error(widget.OWWidget.Error):\n        need_two_clusters = Msg(\"Need at least two non-empty clusters\")\n        singleton_clusters_all = Msg(\"All clusters are singletons\")\n        memory_error = Msg(\"Not enough memory\")\n        value_error = Msg(\"Distances could not be computed: '{}'\")\n        input_validation_error = Msg(\"{}\")\n\n    class Warning(widget.OWWidget.Warning):\n        missing_cluster_assignment = Msg(\n            \"{} instance{s} omitted (missing cluster assignment)\")\n        nan_distances = Msg(\"{} instance{s} omitted (undefined distances)\")\n        ignoring_categorical = Msg(\"Ignoring categorical features\")\n\n    def __init__(self):\n        super().__init__()\n        #: The input data\n        self.data = None         # type: Optional[Orange.data.Table]\n        #: The input distance matrix (if present)\n        self.distances = None  # type: Optional[Orange.misc.DistMatrix]\n        #: The effective distance matrix (is self.distances or computed from\n        #: self.data depending on input)\n        self._matrix = None      # type: Optional[Orange.misc.DistMatrix]\n        #: An bool mask (size == len(data)) indicating missing group/cluster\n        #: assignments\n        self._mask = None        # type: Optional[np.ndarray]\n        #: An array of cluster/group labels for instances with valid group\n        #: assignment\n        self._labels = None      # type: Optional[np.ndarray]\n        #: An array of silhouette scores for instances with valid group\n        #: assignment\n        self._silhouette = None  # type: Optional[np.ndarray]\n        self._silplot = None     # type: Optional[SilhouettePlot]\n\n        controllayout = self.controlArea.layout()\n        assert isinstance(controllayout, QVBoxLayout)\n        self._distances_gui_box = distbox = gui.widgetBox(\n            None, \"Distance\"\n        )\n        self._distances_gui_cb = gui.comboBox(\n            distbox, self, \"distance_idx\",\n            items=[name for name, _ in OWSilhouettePlot.Distances],\n            orientation=Qt.Horizontal, callback=self._invalidate_distances)\n        controllayout.addWidget(distbox)\n\n        box = gui.vBox(self.controlArea, \"Grouping\")\n        self.cluster_var_model = itemmodels.VariableListModel(\n            parent=self, placeholder=\"(None)\")\n        self.cluster_var_cb = gui.comboBox(\n            box, self, \"cluster_var\", contentsLength=14,\n            searchable=True, callback=self._invalidate_scores,\n            model=self.cluster_var_model\n        )\n        gui.checkBox(\n            box, self, \"group_by_cluster\", \"Show in groups\",\n            callback=self._replot)\n\n        box = gui.vBox(self.controlArea, \"Bars\")\n        gui.widgetLabel(box, \"Bar width:\")\n        gui.hSlider(\n            box, self, \"bar_size\", minValue=1, maxValue=10, step=1,\n            callback=self._update_bar_size)\n        gui.widgetLabel(box, \"Annotations:\")\n        self.annotation_var_model = itemmodels.VariableListModel(parent=self)\n        self.annotation_var_model[:] = [None]\n        self.annotation_cb = gui.comboBox(\n            box, self, \"annotation_var\", contentsLength=14,\n            callback=self._update_annotations,\n            model=self.annotation_var_model)\n        ibox = gui.indentedBox(box, 5)\n        self.ann_hidden_warning = warning = gui.widgetLabel(\n            ibox, \"(increase the width to show)\")\n        ibox.setFixedWidth(ibox.sizeHint().width())\n        warning.setVisible(False)\n\n        gui.rubber(self.controlArea)\n\n        gui.auto_send(self.buttonsArea, self, \"auto_commit\")\n\n        self.scene = GraphicsScene(self)\n        self.view = StyledGraphicsView(self.scene)\n        self.view.setRenderHint(QPainter.Antialiasing, True)\n        self.view.setAlignment(Qt.AlignTop | Qt.AlignLeft)\n        self.mainArea.layout().addWidget(self.view)\n\n        self.settingsAboutToBePacked.connect(self.pack_settings)\n\n    def sizeHint(self):\n        sh = self.controlArea.sizeHint()\n        return sh.expandedTo(QSize(600, 720))\n\n    def pack_settings(self):\n        if self.data and self._silplot is not None:\n            self.pending_selection = list(self._silplot.selection())\n        else:\n            self.pending_selection = None\n\n    @Inputs.data\n    @check_sql_input\n    def set_data(self, data: Union[Table, DistMatrix, None]):\n        \"\"\"\n        Set the input dataset or distance matrix.\n        \"\"\"\n        self.closeContext()\n        self.clear()\n        try:\n            if isinstance(data, Orange.misc.DistMatrix):\n                self._set_distances(data)\n            elif isinstance(data, Orange.data.Table):\n                self._set_table(data)\n            else:\n                self.distances = None\n                self.data = None\n        except InputValidationError as err:\n            self.Error.input_validation_error(err.message)\n            self.distances = None\n            self.data = None\n\n    def _set_table(self, data: Table):\n        self._setup_control_models(data.domain)\n        self.data = data\n        self.distances = None\n\n    def _set_distances(self, distances: DistMatrix):\n        if isinstance(distances.row_items, Orange.data.Table) and \\\n                distances.axis == 1:\n            data = distances.row_items\n        else:\n            raise ValidationError(\"Input matrix does not have associated data\")\n\n        if data is not None:\n            self._setup_control_models(data.domain)\n            self.distances = distances\n            self.data = data\n\n    def handleNewSignals(self):\n        if not self._is_empty():\n            self._update()\n            self._replot()\n            if self.pending_selection is not None and self._silplot is not None:\n                # If selection contains indices that are too large, the data\n                # file must had been modified, so we ignore selection\n                if max(self.pending_selection, default=-1) < len(self.data):\n                    self._silplot.setSelection(np.array(self.pending_selection))\n                self.pending_selection = None\n\n        # Disable/enable the Distances GUI controls if applicable\n        self._distances_gui_box.setEnabled(self.distances is None)\n\n        self.commit.now()\n\n    def _setup_control_models(self, domain: Domain):\n        groupvars = [\n            v for v in domain.variables + domain.metas\n            if v.is_discrete and len(v.values) >= 2]\n        if not groupvars:\n            raise NoGroupVariable()\n        self.cluster_var_model[:] = groupvars\n        if domain.class_var in groupvars:\n            self.cluster_var = domain.class_var\n        else:\n            self.cluster_var = groupvars[0]\n        annotvars = [var for var in domain.variables + domain.metas\n                     if var.is_string or var.is_discrete]\n        self.annotation_var_model[:] = [None] + annotvars\n        self.annotation_var = annotvars[0] if annotvars else None\n        self.openContext(Orange.data.Domain(groupvars))\n\n    def _is_empty(self) -> bool:\n        # Is empty (does not have any input).\n        return (self.data is None or len(self.data) == 0) \\\n               and self.distances is None\n\n    def clear(self):\n        \"\"\"\n        Clear the widget state.\n        \"\"\"\n        self.data = None\n        self.distances = None\n        self._matrix = None\n        self._mask = None\n        self._silhouette = None\n        self._labels = None\n        self.cluster_var_model[:] = []\n        self.annotation_var_model[:] = [None]\n        self._clear_scene()\n        self.Error.clear()\n        self.Warning.clear()\n\n    def _clear_scene(self):\n        # Clear the graphics scene and associated objects\n        self.scene.clear()\n        self.scene.setSceneRect(QRectF())\n        self.view.setSceneRect(QRectF())\n        self.view.setHeaderSceneRect(QRectF())\n        self.view.setFooterSceneRect(QRectF())\n        self._silplot = None\n\n    def _invalidate_distances(self):\n        # Invalidate the computed distance matrix and recompute the silhouette.\n        self._matrix = None\n        self._invalidate_scores()\n\n    def _invalidate_scores(self):\n        # Invalidate and recompute the current silhouette scores.\n        self._labels = self._silhouette = self._mask = None\n        self._update()\n        self._replot()\n        if self.data is not None:\n            self.commit.deferred()\n\n    def _ensure_matrix(self):\n        # ensure self._matrix is computed if necessary\n        if self._is_empty():\n            return\n        if self._matrix is None:\n            if self.distances is not None:\n                self._matrix = np.asarray(self.distances)\n            elif self.data is not None:\n                data = self.data\n                _, metric = self.Distances[self.distance_idx]\n                if not metric.supports_discrete and any(\n                        a.is_discrete for a in data.domain.attributes):\n                    self.Warning.ignoring_categorical()\n                    data = Orange.distance.remove_discrete_features(data)\n                try:\n                    self._matrix = np.asarray(metric(data))\n                except MemoryError:\n                    self.Error.memory_error()\n                    return\n                except ValueError as err:\n                    self.Error.value_error(str(err))\n                    return\n            else:\n                assert False, \"invalid state\"\n\n    def _update(self):\n        # Update/recompute the effective distances and scores as required.\n        self._clear_messages()\n        if self._is_empty():\n            self._reset_all()\n            return\n\n        self._ensure_matrix()\n        if self._matrix is None:\n            return\n\n        labels, _ = self.data.get_column_view(self.cluster_var)\n        labels = np.asarray(labels, dtype=float)\n        cluster_mask = np.isnan(labels)\n        dist_mask = np.isnan(self._matrix).all(axis=0)\n        mask = cluster_mask | dist_mask\n        labels = labels.astype(int)\n        labels = labels[~mask]\n\n        labels_unq = np.unique(labels)\n\n        if len(labels_unq) < 2:\n            self.Error.need_two_clusters()\n            labels = silhouette = mask = None\n        elif len(labels_unq) == len(labels):\n            self.Error.singleton_clusters_all()\n            labels = silhouette = mask = None\n        else:\n            silhouette = sklearn.metrics.silhouette_samples(\n                self._matrix[~mask, :][:, ~mask], labels, metric=\"precomputed\")\n        self._mask = mask\n        self._labels = labels\n        self._silhouette = silhouette\n\n        if mask is not None:\n            count_missing = np.count_nonzero(cluster_mask)\n            if count_missing:\n                self.Warning.missing_cluster_assignment(\n                    count_missing, s=\"s\" if count_missing > 1 else \"\")\n            count_nandist = np.count_nonzero(dist_mask)\n            if count_nandist:\n                self.Warning.nan_distances(\n                    count_nandist, s=\"s\" if count_nandist > 1 else \"\")\n\n    def _reset_all(self):\n        self._mask = None\n        self._silhouette = None\n        self._labels = None\n        self._matrix = None\n        self._clear_scene()\n\n    def _clear_messages(self):\n        self.Error.clear()\n        self.Warning.clear()\n\n    def _set_bar_height(self):\n        visible = self.bar_size >= 5\n        self._silplot.setBarHeight(self.bar_size)\n        self._silplot.setRowNamesVisible(visible)\n        self.ann_hidden_warning.setVisible(\n            not visible and self.annotation_var is not None)\n\n    def _replot(self):\n        # Clear and replot/initialize the scene\n        self._clear_scene()\n        if self._silhouette is not None and self._labels is not None:\n            self._silplot = silplot = SilhouettePlot()\n            self._set_bar_height()\n\n            if self.group_by_cluster:\n                silplot.setScores(\n                    self._silhouette, self._labels,\n                    self.cluster_var.values, self.cluster_var.colors)\n            else:\n                silplot.setScores(\n                    self._silhouette,\n                    np.zeros(len(self._silhouette), dtype=int),\n                    [\"\"], np.array([[63, 207, 207]])\n                )\n\n            self.scene.addItem(silplot)\n            self._update_annotations()\n            silplot.selectionChanged.connect(self.commit.deferred)\n            silplot.layout().activate()\n            self._update_scene_rect()\n            silplot.geometryChanged.connect(self._update_scene_rect)\n\n    def _update_bar_size(self):\n        if self._silplot is not None:\n            self._set_bar_height()\n\n    def _update_annotations(self):\n        annot_var = self.annotation_var\n        self.ann_hidden_warning.setVisible(\n            self.bar_size < 5 and annot_var is not None)\n\n        if self._silplot is not None:\n            if annot_var is not None:\n                column, _ = self.data.get_column_view(annot_var)\n                if self._mask is not None:\n                    assert column.shape == self._mask.shape\n                    # pylint: disable=invalid-unary-operand-type\n                    column = column[~self._mask]\n                self._silplot.setRowNames(\n                    [annot_var.str_val(value) for value in column])\n            else:\n                self._silplot.setRowNames(None)\n\n    def _update_scene_rect(self):\n        geom = self._silplot.geometry()\n        self.scene.setSceneRect(geom)\n        self.view.setSceneRect(geom)\n\n        header = self._silplot.topScaleItem()\n        footer = self._silplot.bottomScaleItem()\n\n        def extend_horizontal(rect):\n            # type: (QRectF) -> QRectF\n            rect = QRectF(rect)\n            rect.setLeft(geom.left())\n            rect.setRight(geom.right())\n            return rect\n\n        margin = 3\n        if header is not None:\n            self.view.setHeaderSceneRect(\n                extend_horizontal(header.geometry().adjusted(0, 0, 0, margin)))\n        if footer is not None:\n            self.view.setFooterSceneRect(\n                extend_horizontal(footer.geometry().adjusted(0, -margin, 0, 0)))\n\n    @gui.deferred\n    def commit(self):\n        \"\"\"\n        Commit/send the current selection to the output.\n        \"\"\"\n        selected = indices = data = None\n        if self.data is not None:\n            selectedmask = np.full(len(self.data), False, dtype=bool)\n            if self._silplot is not None:\n                indices = self._silplot.selection()\n                assert (np.diff(indices) > 0).all(), \"strictly increasing\"\n                if self._mask is not None:\n                    # pylint: disable=invalid-unary-operand-type\n                    indices = np.flatnonzero(~self._mask)[indices]\n                selectedmask[indices] = True\n\n            if self._mask is not None:\n                scores = np.full(shape=selectedmask.shape,\n                                 fill_value=np.nan)\n                # pylint: disable=invalid-unary-operand-type\n                scores[~self._mask] = self._silhouette\n            else:\n                scores = self._silhouette\n\n            domain = self.data.domain\n            proposed = \"Silhouette ({})\".format(escape(self.cluster_var.name))\n            names = [var.name for var in itertools.chain(domain.attributes,\n                                                         domain.class_vars,\n                                                         domain.metas)]\n            unique = get_unique_names(names, proposed)\n            silhouette_var = Orange.data.ContinuousVariable(unique)\n            domain = Orange.data.Domain(\n                domain.attributes,\n                domain.class_vars,\n                domain.metas + (silhouette_var, ))\n\n            if np.count_nonzero(selectedmask):\n                selected = self.data.from_table(\n                    domain, self.data, np.flatnonzero(selectedmask))\n\n            if selected is not None:\n                with selected.unlocked(selected.metas):\n                    selected[:, silhouette_var] = np.c_[scores[selectedmask]]\n\n            data = self.data.transform(domain)\n            with data.unlocked(data.metas):\n                data[:, silhouette_var] = np.c_[scores]\n\n        self.Outputs.selected_data.send(selected)\n        self.Outputs.annotated_data.send(create_annotated_table(data, indices))\n\n    def send_report(self):\n        if not len(self.cluster_var_model):\n            return\n\n        self.report_plot()\n        caption = \"Silhouette plot \" \\\n                  f\"({self.Distances[self.distance_idx][0]} distance), \" \\\n                  f\"clustered by '{self.cluster_var.name}'\"\n        if self.annotation_var and self._silplot.rowNamesVisible():\n            caption += f\", annotated with '{self.annotation_var.name}'\"\n        self.report_caption(caption)\n\n    def onDeleteWidget(self):\n        self.clear()\n        super().onDeleteWidget()\n\n    @classmethod\n    def migrate_context(cls, context, version):\n        values = context.values\n        if version < 2:\n            discrete_vars = (\n                name\n                for name, type_ in itertools.chain(\n                    context.attributes, context.class_vars, context.metas)\n                if type_ == 1)\n            index, _ = values.pop(\"cluster_var_idx\")\n            name = next(itertools.islice(discrete_vars, index, None)\n            )\n            values[\"cluster_var\"] = (name, 101)\n\n            index = values.pop(\"annotation_var_idx\")[0] - 1\n            if index == -1:\n                values[\"annotation_var\"] = None\n            else:\n                annot_vars = (\n                    (name, type_ + 100)\n                    for name, type_ in itertools.chain(\n                        context.attributes, context.class_vars, context.metas)\n                    if type_ in (1, 3))\n                values[\"annotation_var\"] = next(\n                    itertools.islice(annot_vars, index, None))",
                            "ast_type_str": "ClassDef",
                            "start_lineno": 60,
                            "end_lineno": 562,
                            "parent": null
                        },
                        {
                            "file_name": "Orange/widgets/visualize/owsilhouetteplot.py",
                            "type": "method",
                            "name": "OWSilhouettePlot.migrate_context",
                            "content": "class OWSilhouettePlot(widget.OWWidget):\n    name = \"Silhouette Plot\"\n    description = \"Visually assess cluster quality and \" \\\n                  \"the degree of cluster membership.\"\n\n    icon = \"icons/SilhouettePlot.svg\"\n    priority = 300\n    keywords = []\n\n    class Inputs:\n        data = Input(\"Data\", (Orange.data.Table, Orange.misc.DistMatrix))\n\n    class Outputs:\n        selected_data = Output(\"Selected Data\", Orange.data.Table, default=True)\n        annotated_data = Output(ANNOTATED_DATA_SIGNAL_NAME, Orange.data.Table)\n\n    replaces = [\n        \"orangecontrib.prototypes.widgets.owsilhouetteplot.OWSilhouettePlot\",\n        \"Orange.widgets.unsupervised.owsilhouetteplot.OWSilhouettePlot\"\n    ]\n\n    settingsHandler = settings.DomainContextHandler()\n    settings_version = 2\n\n    #: Distance metric index\n    distance_idx = settings.Setting(0)\n    #: Group/cluster variable\n    cluster_var = settings.ContextSetting(None)\n    #: Annotation variable\n    annotation_var = settings.ContextSetting(None)\n    #: Group the (displayed) silhouettes by cluster\n    group_by_cluster = settings.Setting(True)\n    #: A fixed size for an instance bar\n    bar_size = settings.Setting(3)\n    #: Add silhouette scores to output data\n    auto_commit = settings.Setting(True)\n\n    pending_selection = settings.Setting(None, schema_only=True)\n\n    Distances = [(\"Euclidean\", Orange.distance.Euclidean),\n                 (\"Manhattan\", Orange.distance.Manhattan),\n                 (\"Cosine\", Orange.distance.Cosine)]\n\n    graph_name = \"scene\"\n\n    class Error(widget.OWWidget.Error):\n        need_two_clusters = Msg(\"Need at least two non-empty clusters\")\n        singleton_clusters_all = Msg(\"All clusters are singletons\")\n        memory_error = Msg(\"Not enough memory\")\n        value_error = Msg(\"Distances could not be computed: '{}'\")\n        input_validation_error = Msg(\"{}\")\n\n    class Warning(widget.OWWidget.Warning):\n        missing_cluster_assignment = Msg(\n            \"{} instance{s} omitted (missing cluster assignment)\")\n        nan_distances = Msg(\"{} instance{s} omitted (undefined distances)\")\n        ignoring_categorical = Msg(\"Ignoring categorical features\")\n\n    def __init__(self):\n        super().__init__()\n        #: The input data\n        self.data = None         # type: Optional[Orange.data.Table]\n        #: The input distance matrix (if present)\n        self.distances = None  # type: Optional[Orange.misc.DistMatrix]\n        #: The effective distance matrix (is self.distances or computed from\n        #: self.data depending on input)\n        self._matrix = None      # type: Optional[Orange.misc.DistMatrix]\n        #: An bool mask (size == len(data)) indicating missing group/cluster\n        #: assignments\n        self._mask = None        # type: Optional[np.ndarray]\n        #: An array of cluster/group labels for instances with valid group\n        #: assignment\n        self._labels = None      # type: Optional[np.ndarray]\n        #: An array of silhouette scores for instances with valid group\n        #: assignment\n        self._silhouette = None  # type: Optional[np.ndarray]\n        self._silplot = None     # type: Optional[SilhouettePlot]\n\n        controllayout = self.controlArea.layout()\n        assert isinstance(controllayout, QVBoxLayout)\n        self._distances_gui_box = distbox = gui.widgetBox(\n            None, \"Distance\"\n        )\n        self._distances_gui_cb = gui.comboBox(\n            distbox, self, \"distance_idx\",\n            items=[name for name, _ in OWSilhouettePlot.Distances],\n            orientation=Qt.Horizontal, callback=self._invalidate_distances)\n        controllayout.addWidget(distbox)\n\n        box = gui.vBox(self.controlArea, \"Grouping\")\n        self.cluster_var_model = itemmodels.VariableListModel(\n            parent=self, placeholder=\"(None)\")\n        self.cluster_var_cb = gui.comboBox(\n            box, self, \"cluster_var\", contentsLength=14,\n            searchable=True, callback=self._invalidate_scores,\n            model=self.cluster_var_model\n        )\n        gui.checkBox(\n            box, self, \"group_by_cluster\", \"Show in groups\",\n            callback=self._replot)\n\n        box = gui.vBox(self.controlArea, \"Bars\")\n        gui.widgetLabel(box, \"Bar width:\")\n        gui.hSlider(\n            box, self, \"bar_size\", minValue=1, maxValue=10, step=1,\n            callback=self._update_bar_size)\n        gui.widgetLabel(box, \"Annotations:\")\n        self.annotation_var_model = itemmodels.VariableListModel(parent=self)\n        self.annotation_var_model[:] = [None]\n        self.annotation_cb = gui.comboBox(\n            box, self, \"annotation_var\", contentsLength=14,\n            callback=self._update_annotations,\n            model=self.annotation_var_model)\n        ibox = gui.indentedBox(box, 5)\n        self.ann_hidden_warning = warning = gui.widgetLabel(\n            ibox, \"(increase the width to show)\")\n        ibox.setFixedWidth(ibox.sizeHint().width())\n        warning.setVisible(False)\n\n        gui.rubber(self.controlArea)\n\n        gui.auto_send(self.buttonsArea, self, \"auto_commit\")\n\n        self.scene = GraphicsScene(self)\n        self.view = StyledGraphicsView(self.scene)\n        self.view.setRenderHint(QPainter.Antialiasing, True)\n        self.view.setAlignment(Qt.AlignTop | Qt.AlignLeft)\n        self.mainArea.layout().addWidget(self.view)\n\n        self.settingsAboutToBePacked.connect(self.pack_settings)\n\n    def sizeHint(self):\n        sh = self.controlArea.sizeHint()\n        return sh.expandedTo(QSize(600, 720))\n\n    def pack_settings(self):\n        if self.data and self._silplot is not None:\n            self.pending_selection = list(self._silplot.selection())\n        else:\n            self.pending_selection = None\n\n    @Inputs.data\n    @check_sql_input\n    def set_data(self, data: Union[Table, DistMatrix, None]):\n        \"\"\"\n        Set the input dataset or distance matrix.\n        \"\"\"\n        self.closeContext()\n        self.clear()\n        try:\n            if isinstance(data, Orange.misc.DistMatrix):\n                self._set_distances(data)\n            elif isinstance(data, Orange.data.Table):\n                self._set_table(data)\n            else:\n                self.distances = None\n                self.data = None\n        except InputValidationError as err:\n            self.Error.input_validation_error(err.message)\n            self.distances = None\n            self.data = None\n\n    def _set_table(self, data: Table):\n        self._setup_control_models(data.domain)\n        self.data = data\n        self.distances = None\n\n    def _set_distances(self, distances: DistMatrix):\n        if isinstance(distances.row_items, Orange.data.Table) and \\\n                distances.axis == 1:\n            data = distances.row_items\n        else:\n            raise ValidationError(\"Input matrix does not have associated data\")\n\n        if data is not None:\n            self._setup_control_models(data.domain)\n            self.distances = distances\n            self.data = data\n\n    def handleNewSignals(self):\n        if not self._is_empty():\n            self._update()\n            self._replot()\n            if self.pending_selection is not None and self._silplot is not None:\n                # If selection contains indices that are too large, the data\n                # file must had been modified, so we ignore selection\n                if max(self.pending_selection, default=-1) < len(self.data):\n                    self._silplot.setSelection(np.array(self.pending_selection))\n                self.pending_selection = None\n\n        # Disable/enable the Distances GUI controls if applicable\n        self._distances_gui_box.setEnabled(self.distances is None)\n\n        self.commit.now()\n\n    def _setup_control_models(self, domain: Domain):\n        groupvars = [\n            v for v in domain.variables + domain.metas\n            if v.is_discrete and len(v.values) >= 2]\n        if not groupvars:\n            raise NoGroupVariable()\n        self.cluster_var_model[:] = groupvars\n        if domain.class_var in groupvars:\n            self.cluster_var = domain.class_var\n        else:\n            self.cluster_var = groupvars[0]\n        annotvars = [var for var in domain.variables + domain.metas\n                     if var.is_string or var.is_discrete]\n        self.annotation_var_model[:] = [None] + annotvars\n        self.annotation_var = annotvars[0] if annotvars else None\n        self.openContext(Orange.data.Domain(groupvars))\n\n    def _is_empty(self) -> bool:\n        # Is empty (does not have any input).\n        return (self.data is None or len(self.data) == 0) \\\n               and self.distances is None\n\n    def clear(self):\n        \"\"\"\n        Clear the widget state.\n        \"\"\"\n        self.data = None\n        self.distances = None\n        self._matrix = None\n        self._mask = None\n        self._silhouette = None\n        self._labels = None\n        self.cluster_var_model[:] = []\n        self.annotation_var_model[:] = [None]\n        self._clear_scene()\n        self.Error.clear()\n        self.Warning.clear()\n\n    def _clear_scene(self):\n        # Clear the graphics scene and associated objects\n        self.scene.clear()\n        self.scene.setSceneRect(QRectF())\n        self.view.setSceneRect(QRectF())\n        self.view.setHeaderSceneRect(QRectF())\n        self.view.setFooterSceneRect(QRectF())\n        self._silplot = None\n\n    def _invalidate_distances(self):\n        # Invalidate the computed distance matrix and recompute the silhouette.\n        self._matrix = None\n        self._invalidate_scores()\n\n    def _invalidate_scores(self):\n        # Invalidate and recompute the current silhouette scores.\n        self._labels = self._silhouette = self._mask = None\n        self._update()\n        self._replot()\n        if self.data is not None:\n            self.commit.deferred()\n\n    def _ensure_matrix(self):\n        # ensure self._matrix is computed if necessary\n        if self._is_empty():\n            return\n        if self._matrix is None:\n            if self.distances is not None:\n                self._matrix = np.asarray(self.distances)\n            elif self.data is not None:\n                data = self.data\n                _, metric = self.Distances[self.distance_idx]\n                if not metric.supports_discrete and any(\n                        a.is_discrete for a in data.domain.attributes):\n                    self.Warning.ignoring_categorical()\n                    data = Orange.distance.remove_discrete_features(data)\n                try:\n                    self._matrix = np.asarray(metric(data))\n                except MemoryError:\n                    self.Error.memory_error()\n                    return\n                except ValueError as err:\n                    self.Error.value_error(str(err))\n                    return\n            else:\n                assert False, \"invalid state\"\n\n    def _update(self):\n        # Update/recompute the effective distances and scores as required.\n        self._clear_messages()\n        if self._is_empty():\n            self._reset_all()\n            return\n\n        self._ensure_matrix()\n        if self._matrix is None:\n            return\n\n        labels, _ = self.data.get_column_view(self.cluster_var)\n        labels = np.asarray(labels, dtype=float)\n        cluster_mask = np.isnan(labels)\n        dist_mask = np.isnan(self._matrix).all(axis=0)\n        mask = cluster_mask | dist_mask\n        labels = labels.astype(int)\n        labels = labels[~mask]\n\n        labels_unq = np.unique(labels)\n\n        if len(labels_unq) < 2:\n            self.Error.need_two_clusters()\n            labels = silhouette = mask = None\n        elif len(labels_unq) == len(labels):\n            self.Error.singleton_clusters_all()\n            labels = silhouette = mask = None\n        else:\n            silhouette = sklearn.metrics.silhouette_samples(\n                self._matrix[~mask, :][:, ~mask], labels, metric=\"precomputed\")\n        self._mask = mask\n        self._labels = labels\n        self._silhouette = silhouette\n\n        if mask is not None:\n            count_missing = np.count_nonzero(cluster_mask)\n            if count_missing:\n                self.Warning.missing_cluster_assignment(\n                    count_missing, s=\"s\" if count_missing > 1 else \"\")\n            count_nandist = np.count_nonzero(dist_mask)\n            if count_nandist:\n                self.Warning.nan_distances(\n                    count_nandist, s=\"s\" if count_nandist > 1 else \"\")\n\n    def _reset_all(self):\n        self._mask = None\n        self._silhouette = None\n        self._labels = None\n        self._matrix = None\n        self._clear_scene()\n\n    def _clear_messages(self):\n        self.Error.clear()\n        self.Warning.clear()\n\n    def _set_bar_height(self):\n        visible = self.bar_size >= 5\n        self._silplot.setBarHeight(self.bar_size)\n        self._silplot.setRowNamesVisible(visible)\n        self.ann_hidden_warning.setVisible(\n            not visible and self.annotation_var is not None)\n\n    def _replot(self):\n        # Clear and replot/initialize the scene\n        self._clear_scene()\n        if self._silhouette is not None and self._labels is not None:\n            self._silplot = silplot = SilhouettePlot()\n            self._set_bar_height()\n\n            if self.group_by_cluster:\n                silplot.setScores(\n                    self._silhouette, self._labels,\n                    self.cluster_var.values, self.cluster_var.colors)\n            else:\n                silplot.setScores(\n                    self._silhouette,\n                    np.zeros(len(self._silhouette), dtype=int),\n                    [\"\"], np.array([[63, 207, 207]])\n                )\n\n            self.scene.addItem(silplot)\n            self._update_annotations()\n            silplot.selectionChanged.connect(self.commit.deferred)\n            silplot.layout().activate()\n            self._update_scene_rect()\n            silplot.geometryChanged.connect(self._update_scene_rect)\n\n    def _update_bar_size(self):\n        if self._silplot is not None:\n            self._set_bar_height()\n\n    def _update_annotations(self):\n        annot_var = self.annotation_var\n        self.ann_hidden_warning.setVisible(\n            self.bar_size < 5 and annot_var is not None)\n\n        if self._silplot is not None:\n            if annot_var is not None:\n                column, _ = self.data.get_column_view(annot_var)\n                if self._mask is not None:\n                    assert column.shape == self._mask.shape\n                    # pylint: disable=invalid-unary-operand-type\n                    column = column[~self._mask]\n                self._silplot.setRowNames(\n                    [annot_var.str_val(value) for value in column])\n            else:\n                self._silplot.setRowNames(None)\n\n    def _update_scene_rect(self):\n        geom = self._silplot.geometry()\n        self.scene.setSceneRect(geom)\n        self.view.setSceneRect(geom)\n\n        header = self._silplot.topScaleItem()\n        footer = self._silplot.bottomScaleItem()\n\n        def extend_horizontal(rect):\n            # type: (QRectF) -> QRectF\n            rect = QRectF(rect)\n            rect.setLeft(geom.left())\n            rect.setRight(geom.right())\n            return rect\n\n        margin = 3\n        if header is not None:\n            self.view.setHeaderSceneRect(\n                extend_horizontal(header.geometry().adjusted(0, 0, 0, margin)))\n        if footer is not None:\n            self.view.setFooterSceneRect(\n                extend_horizontal(footer.geometry().adjusted(0, -margin, 0, 0)))\n\n    @gui.deferred\n    def commit(self):\n        \"\"\"\n        Commit/send the current selection to the output.\n        \"\"\"\n        selected = indices = data = None\n        if self.data is not None:\n            selectedmask = np.full(len(self.data), False, dtype=bool)\n            if self._silplot is not None:\n                indices = self._silplot.selection()\n                assert (np.diff(indices) > 0).all(), \"strictly increasing\"\n                if self._mask is not None:\n                    # pylint: disable=invalid-unary-operand-type\n                    indices = np.flatnonzero(~self._mask)[indices]\n                selectedmask[indices] = True\n\n            if self._mask is not None:\n                scores = np.full(shape=selectedmask.shape,\n                                 fill_value=np.nan)\n                # pylint: disable=invalid-unary-operand-type\n                scores[~self._mask] = self._silhouette\n            else:\n                scores = self._silhouette\n\n            domain = self.data.domain\n            proposed = \"Silhouette ({})\".format(escape(self.cluster_var.name))\n            names = [var.name for var in itertools.chain(domain.attributes,\n                                                         domain.class_vars,\n                                                         domain.metas)]\n            unique = get_unique_names(names, proposed)\n            silhouette_var = Orange.data.ContinuousVariable(unique)\n            domain = Orange.data.Domain(\n                domain.attributes,\n                domain.class_vars,\n                domain.metas + (silhouette_var, ))\n\n            if np.count_nonzero(selectedmask):\n                selected = self.data.from_table(\n                    domain, self.data, np.flatnonzero(selectedmask))\n\n            if selected is not None:\n                with selected.unlocked(selected.metas):\n                    selected[:, silhouette_var] = np.c_[scores[selectedmask]]\n\n            data = self.data.transform(domain)\n            with data.unlocked(data.metas):\n                data[:, silhouette_var] = np.c_[scores]\n\n        self.Outputs.selected_data.send(selected)\n        self.Outputs.annotated_data.send(create_annotated_table(data, indices))\n\n    def send_report(self):\n        if not len(self.cluster_var_model):\n            return\n\n        self.report_plot()\n        caption = \"Silhouette plot \" \\\n                  f\"({self.Distances[self.distance_idx][0]} distance), \" \\\n                  f\"clustered by '{self.cluster_var.name}'\"\n        if self.annotation_var and self._silplot.rowNamesVisible():\n            caption += f\", annotated with '{self.annotation_var.name}'\"\n        self.report_caption(caption)\n\n    def onDeleteWidget(self):\n        self.clear()\n        super().onDeleteWidget()\n\n    @classmethod\n    def migrate_context(cls, context, version):\n        values = context.values\n        if version < 2:\n            discrete_vars = (\n                name\n                for name, type_ in itertools.chain(\n                    context.attributes, context.class_vars, context.metas)\n                if type_ == 1)\n            index, _ = values.pop(\"cluster_var_idx\")\n            name = next(itertools.islice(discrete_vars, index, None)\n            )\n            values[\"cluster_var\"] = (name, 101)\n\n            index = values.pop(\"annotation_var_idx\")[0] - 1\n            if index == -1:\n                values[\"annotation_var\"] = None\n            else:\n                annot_vars = (\n                    (name, type_ + 100)\n                    for name, type_ in itertools.chain(\n                        context.attributes, context.class_vars, context.metas)\n                    if type_ in (1, 3))\n                values[\"annotation_var\"] = next(\n                    itertools.islice(annot_vars, index, None))",
                            "ast_type_str": "ClassDef",
                            "start_lineno": 539,
                            "end_lineno": 562,
                            "parent": {
                                "file_name": "Orange/widgets/visualize/owsilhouetteplot.py",
                                "type": "class",
                                "name": "OWSilhouettePlot",
                                "content": "class OWSilhouettePlot(widget.OWWidget):\n    name = \"Silhouette Plot\"\n    description = \"Visually assess cluster quality and \" \\\n                  \"the degree of cluster membership.\"\n\n    icon = \"icons/SilhouettePlot.svg\"\n    priority = 300\n    keywords = []\n\n    class Inputs:\n        data = Input(\"Data\", (Orange.data.Table, Orange.misc.DistMatrix))\n\n    class Outputs:\n        selected_data = Output(\"Selected Data\", Orange.data.Table, default=True)\n        annotated_data = Output(ANNOTATED_DATA_SIGNAL_NAME, Orange.data.Table)\n\n    replaces = [\n        \"orangecontrib.prototypes.widgets.owsilhouetteplot.OWSilhouettePlot\",\n        \"Orange.widgets.unsupervised.owsilhouetteplot.OWSilhouettePlot\"\n    ]\n\n    settingsHandler = settings.DomainContextHandler()\n    settings_version = 2\n\n    #: Distance metric index\n    distance_idx = settings.Setting(0)\n    #: Group/cluster variable\n    cluster_var = settings.ContextSetting(None)\n    #: Annotation variable\n    annotation_var = settings.ContextSetting(None)\n    #: Group the (displayed) silhouettes by cluster\n    group_by_cluster = settings.Setting(True)\n    #: A fixed size for an instance bar\n    bar_size = settings.Setting(3)\n    #: Add silhouette scores to output data\n    auto_commit = settings.Setting(True)\n\n    pending_selection = settings.Setting(None, schema_only=True)\n\n    Distances = [(\"Euclidean\", Orange.distance.Euclidean),\n                 (\"Manhattan\", Orange.distance.Manhattan),\n                 (\"Cosine\", Orange.distance.Cosine)]\n\n    graph_name = \"scene\"\n\n    class Error(widget.OWWidget.Error):\n        need_two_clusters = Msg(\"Need at least two non-empty clusters\")\n        singleton_clusters_all = Msg(\"All clusters are singletons\")\n        memory_error = Msg(\"Not enough memory\")\n        value_error = Msg(\"Distances could not be computed: '{}'\")\n        input_validation_error = Msg(\"{}\")\n\n    class Warning(widget.OWWidget.Warning):\n        missing_cluster_assignment = Msg(\n            \"{} instance{s} omitted (missing cluster assignment)\")\n        nan_distances = Msg(\"{} instance{s} omitted (undefined distances)\")\n        ignoring_categorical = Msg(\"Ignoring categorical features\")\n\n    def __init__(self):\n        super().__init__()\n        #: The input data\n        self.data = None         # type: Optional[Orange.data.Table]\n        #: The input distance matrix (if present)\n        self.distances = None  # type: Optional[Orange.misc.DistMatrix]\n        #: The effective distance matrix (is self.distances or computed from\n        #: self.data depending on input)\n        self._matrix = None      # type: Optional[Orange.misc.DistMatrix]\n        #: An bool mask (size == len(data)) indicating missing group/cluster\n        #: assignments\n        self._mask = None        # type: Optional[np.ndarray]\n        #: An array of cluster/group labels for instances with valid group\n        #: assignment\n        self._labels = None      # type: Optional[np.ndarray]\n        #: An array of silhouette scores for instances with valid group\n        #: assignment\n        self._silhouette = None  # type: Optional[np.ndarray]\n        self._silplot = None     # type: Optional[SilhouettePlot]\n\n        controllayout = self.controlArea.layout()\n        assert isinstance(controllayout, QVBoxLayout)\n        self._distances_gui_box = distbox = gui.widgetBox(\n            None, \"Distance\"\n        )\n        self._distances_gui_cb = gui.comboBox(\n            distbox, self, \"distance_idx\",\n            items=[name for name, _ in OWSilhouettePlot.Distances],\n            orientation=Qt.Horizontal, callback=self._invalidate_distances)\n        controllayout.addWidget(distbox)\n\n        box = gui.vBox(self.controlArea, \"Grouping\")\n        self.cluster_var_model = itemmodels.VariableListModel(\n            parent=self, placeholder=\"(None)\")\n        self.cluster_var_cb = gui.comboBox(\n            box, self, \"cluster_var\", contentsLength=14,\n            searchable=True, callback=self._invalidate_scores,\n            model=self.cluster_var_model\n        )\n        gui.checkBox(\n            box, self, \"group_by_cluster\", \"Show in groups\",\n            callback=self._replot)\n\n        box = gui.vBox(self.controlArea, \"Bars\")\n        gui.widgetLabel(box, \"Bar width:\")\n        gui.hSlider(\n            box, self, \"bar_size\", minValue=1, maxValue=10, step=1,\n            callback=self._update_bar_size)\n        gui.widgetLabel(box, \"Annotations:\")\n        self.annotation_var_model = itemmodels.VariableListModel(parent=self)\n        self.annotation_var_model[:] = [None]\n        self.annotation_cb = gui.comboBox(\n            box, self, \"annotation_var\", contentsLength=14,\n            callback=self._update_annotations,\n            model=self.annotation_var_model)\n        ibox = gui.indentedBox(box, 5)\n        self.ann_hidden_warning = warning = gui.widgetLabel(\n            ibox, \"(increase the width to show)\")\n        ibox.setFixedWidth(ibox.sizeHint().width())\n        warning.setVisible(False)\n\n        gui.rubber(self.controlArea)\n\n        gui.auto_send(self.buttonsArea, self, \"auto_commit\")\n\n        self.scene = GraphicsScene(self)\n        self.view = StyledGraphicsView(self.scene)\n        self.view.setRenderHint(QPainter.Antialiasing, True)\n        self.view.setAlignment(Qt.AlignTop | Qt.AlignLeft)\n        self.mainArea.layout().addWidget(self.view)\n\n        self.settingsAboutToBePacked.connect(self.pack_settings)\n\n    def sizeHint(self):\n        sh = self.controlArea.sizeHint()\n        return sh.expandedTo(QSize(600, 720))\n\n    def pack_settings(self):\n        if self.data and self._silplot is not None:\n            self.pending_selection = list(self._silplot.selection())\n        else:\n            self.pending_selection = None\n\n    @Inputs.data\n    @check_sql_input\n    def set_data(self, data: Union[Table, DistMatrix, None]):\n        \"\"\"\n        Set the input dataset or distance matrix.\n        \"\"\"\n        self.closeContext()\n        self.clear()\n        try:\n            if isinstance(data, Orange.misc.DistMatrix):\n                self._set_distances(data)\n            elif isinstance(data, Orange.data.Table):\n                self._set_table(data)\n            else:\n                self.distances = None\n                self.data = None\n        except InputValidationError as err:\n            self.Error.input_validation_error(err.message)\n            self.distances = None\n            self.data = None\n\n    def _set_table(self, data: Table):\n        self._setup_control_models(data.domain)\n        self.data = data\n        self.distances = None\n\n    def _set_distances(self, distances: DistMatrix):\n        if isinstance(distances.row_items, Orange.data.Table) and \\\n                distances.axis == 1:\n            data = distances.row_items\n        else:\n            raise ValidationError(\"Input matrix does not have associated data\")\n\n        if data is not None:\n            self._setup_control_models(data.domain)\n            self.distances = distances\n            self.data = data\n\n    def handleNewSignals(self):\n        if not self._is_empty():\n            self._update()\n            self._replot()\n            if self.pending_selection is not None and self._silplot is not None:\n                # If selection contains indices that are too large, the data\n                # file must had been modified, so we ignore selection\n                if max(self.pending_selection, default=-1) < len(self.data):\n                    self._silplot.setSelection(np.array(self.pending_selection))\n                self.pending_selection = None\n\n        # Disable/enable the Distances GUI controls if applicable\n        self._distances_gui_box.setEnabled(self.distances is None)\n\n        self.commit.now()\n\n    def _setup_control_models(self, domain: Domain):\n        groupvars = [\n            v for v in domain.variables + domain.metas\n            if v.is_discrete and len(v.values) >= 2]\n        if not groupvars:\n            raise NoGroupVariable()\n        self.cluster_var_model[:] = groupvars\n        if domain.class_var in groupvars:\n            self.cluster_var = domain.class_var\n        else:\n            self.cluster_var = groupvars[0]\n        annotvars = [var for var in domain.variables + domain.metas\n                     if var.is_string or var.is_discrete]\n        self.annotation_var_model[:] = [None] + annotvars\n        self.annotation_var = annotvars[0] if annotvars else None\n        self.openContext(Orange.data.Domain(groupvars))\n\n    def _is_empty(self) -> bool:\n        # Is empty (does not have any input).\n        return (self.data is None or len(self.data) == 0) \\\n               and self.distances is None\n\n    def clear(self):\n        \"\"\"\n        Clear the widget state.\n        \"\"\"\n        self.data = None\n        self.distances = None\n        self._matrix = None\n        self._mask = None\n        self._silhouette = None\n        self._labels = None\n        self.cluster_var_model[:] = []\n        self.annotation_var_model[:] = [None]\n        self._clear_scene()\n        self.Error.clear()\n        self.Warning.clear()\n\n    def _clear_scene(self):\n        # Clear the graphics scene and associated objects\n        self.scene.clear()\n        self.scene.setSceneRect(QRectF())\n        self.view.setSceneRect(QRectF())\n        self.view.setHeaderSceneRect(QRectF())\n        self.view.setFooterSceneRect(QRectF())\n        self._silplot = None\n\n    def _invalidate_distances(self):\n        # Invalidate the computed distance matrix and recompute the silhouette.\n        self._matrix = None\n        self._invalidate_scores()\n\n    def _invalidate_scores(self):\n        # Invalidate and recompute the current silhouette scores.\n        self._labels = self._silhouette = self._mask = None\n        self._update()\n        self._replot()\n        if self.data is not None:\n            self.commit.deferred()\n\n    def _ensure_matrix(self):\n        # ensure self._matrix is computed if necessary\n        if self._is_empty():\n            return\n        if self._matrix is None:\n            if self.distances is not None:\n                self._matrix = np.asarray(self.distances)\n            elif self.data is not None:\n                data = self.data\n                _, metric = self.Distances[self.distance_idx]\n                if not metric.supports_discrete and any(\n                        a.is_discrete for a in data.domain.attributes):\n                    self.Warning.ignoring_categorical()\n                    data = Orange.distance.remove_discrete_features(data)\n                try:\n                    self._matrix = np.asarray(metric(data))\n                except MemoryError:\n                    self.Error.memory_error()\n                    return\n                except ValueError as err:\n                    self.Error.value_error(str(err))\n                    return\n            else:\n                assert False, \"invalid state\"\n\n    def _update(self):\n        # Update/recompute the effective distances and scores as required.\n        self._clear_messages()\n        if self._is_empty():\n            self._reset_all()\n            return\n\n        self._ensure_matrix()\n        if self._matrix is None:\n            return\n\n        labels, _ = self.data.get_column_view(self.cluster_var)\n        labels = np.asarray(labels, dtype=float)\n        cluster_mask = np.isnan(labels)\n        dist_mask = np.isnan(self._matrix).all(axis=0)\n        mask = cluster_mask | dist_mask\n        labels = labels.astype(int)\n        labels = labels[~mask]\n\n        labels_unq = np.unique(labels)\n\n        if len(labels_unq) < 2:\n            self.Error.need_two_clusters()\n            labels = silhouette = mask = None\n        elif len(labels_unq) == len(labels):\n            self.Error.singleton_clusters_all()\n            labels = silhouette = mask = None\n        else:\n            silhouette = sklearn.metrics.silhouette_samples(\n                self._matrix[~mask, :][:, ~mask], labels, metric=\"precomputed\")\n        self._mask = mask\n        self._labels = labels\n        self._silhouette = silhouette\n\n        if mask is not None:\n            count_missing = np.count_nonzero(cluster_mask)\n            if count_missing:\n                self.Warning.missing_cluster_assignment(\n                    count_missing, s=\"s\" if count_missing > 1 else \"\")\n            count_nandist = np.count_nonzero(dist_mask)\n            if count_nandist:\n                self.Warning.nan_distances(\n                    count_nandist, s=\"s\" if count_nandist > 1 else \"\")\n\n    def _reset_all(self):\n        self._mask = None\n        self._silhouette = None\n        self._labels = None\n        self._matrix = None\n        self._clear_scene()\n\n    def _clear_messages(self):\n        self.Error.clear()\n        self.Warning.clear()\n\n    def _set_bar_height(self):\n        visible = self.bar_size >= 5\n        self._silplot.setBarHeight(self.bar_size)\n        self._silplot.setRowNamesVisible(visible)\n        self.ann_hidden_warning.setVisible(\n            not visible and self.annotation_var is not None)\n\n    def _replot(self):\n        # Clear and replot/initialize the scene\n        self._clear_scene()\n        if self._silhouette is not None and self._labels is not None:\n            self._silplot = silplot = SilhouettePlot()\n            self._set_bar_height()\n\n            if self.group_by_cluster:\n                silplot.setScores(\n                    self._silhouette, self._labels,\n                    self.cluster_var.values, self.cluster_var.colors)\n            else:\n                silplot.setScores(\n                    self._silhouette,\n                    np.zeros(len(self._silhouette), dtype=int),\n                    [\"\"], np.array([[63, 207, 207]])\n                )\n\n            self.scene.addItem(silplot)\n            self._update_annotations()\n            silplot.selectionChanged.connect(self.commit.deferred)\n            silplot.layout().activate()\n            self._update_scene_rect()\n            silplot.geometryChanged.connect(self._update_scene_rect)\n\n    def _update_bar_size(self):\n        if self._silplot is not None:\n            self._set_bar_height()\n\n    def _update_annotations(self):\n        annot_var = self.annotation_var\n        self.ann_hidden_warning.setVisible(\n            self.bar_size < 5 and annot_var is not None)\n\n        if self._silplot is not None:\n            if annot_var is not None:\n                column, _ = self.data.get_column_view(annot_var)\n                if self._mask is not None:\n                    assert column.shape == self._mask.shape\n                    # pylint: disable=invalid-unary-operand-type\n                    column = column[~self._mask]\n                self._silplot.setRowNames(\n                    [annot_var.str_val(value) for value in column])\n            else:\n                self._silplot.setRowNames(None)\n\n    def _update_scene_rect(self):\n        geom = self._silplot.geometry()\n        self.scene.setSceneRect(geom)\n        self.view.setSceneRect(geom)\n\n        header = self._silplot.topScaleItem()\n        footer = self._silplot.bottomScaleItem()\n\n        def extend_horizontal(rect):\n            # type: (QRectF) -> QRectF\n            rect = QRectF(rect)\n            rect.setLeft(geom.left())\n            rect.setRight(geom.right())\n            return rect\n\n        margin = 3\n        if header is not None:\n            self.view.setHeaderSceneRect(\n                extend_horizontal(header.geometry().adjusted(0, 0, 0, margin)))\n        if footer is not None:\n            self.view.setFooterSceneRect(\n                extend_horizontal(footer.geometry().adjusted(0, -margin, 0, 0)))\n\n    @gui.deferred\n    def commit(self):\n        \"\"\"\n        Commit/send the current selection to the output.\n        \"\"\"\n        selected = indices = data = None\n        if self.data is not None:\n            selectedmask = np.full(len(self.data), False, dtype=bool)\n            if self._silplot is not None:\n                indices = self._silplot.selection()\n                assert (np.diff(indices) > 0).all(), \"strictly increasing\"\n                if self._mask is not None:\n                    # pylint: disable=invalid-unary-operand-type\n                    indices = np.flatnonzero(~self._mask)[indices]\n                selectedmask[indices] = True\n\n            if self._mask is not None:\n                scores = np.full(shape=selectedmask.shape,\n                                 fill_value=np.nan)\n                # pylint: disable=invalid-unary-operand-type\n                scores[~self._mask] = self._silhouette\n            else:\n                scores = self._silhouette\n\n            domain = self.data.domain\n            proposed = \"Silhouette ({})\".format(escape(self.cluster_var.name))\n            names = [var.name for var in itertools.chain(domain.attributes,\n                                                         domain.class_vars,\n                                                         domain.metas)]\n            unique = get_unique_names(names, proposed)\n            silhouette_var = Orange.data.ContinuousVariable(unique)\n            domain = Orange.data.Domain(\n                domain.attributes,\n                domain.class_vars,\n                domain.metas + (silhouette_var, ))\n\n            if np.count_nonzero(selectedmask):\n                selected = self.data.from_table(\n                    domain, self.data, np.flatnonzero(selectedmask))\n\n            if selected is not None:\n                with selected.unlocked(selected.metas):\n                    selected[:, silhouette_var] = np.c_[scores[selectedmask]]\n\n            data = self.data.transform(domain)\n            with data.unlocked(data.metas):\n                data[:, silhouette_var] = np.c_[scores]\n\n        self.Outputs.selected_data.send(selected)\n        self.Outputs.annotated_data.send(create_annotated_table(data, indices))\n\n    def send_report(self):\n        if not len(self.cluster_var_model):\n            return\n\n        self.report_plot()\n        caption = \"Silhouette plot \" \\\n                  f\"({self.Distances[self.distance_idx][0]} distance), \" \\\n                  f\"clustered by '{self.cluster_var.name}'\"\n        if self.annotation_var and self._silplot.rowNamesVisible():\n            caption += f\", annotated with '{self.annotation_var.name}'\"\n        self.report_caption(caption)\n\n    def onDeleteWidget(self):\n        self.clear()\n        super().onDeleteWidget()\n\n    @classmethod\n    def migrate_context(cls, context, version):\n        values = context.values\n        if version < 2:\n            discrete_vars = (\n                name\n                for name, type_ in itertools.chain(\n                    context.attributes, context.class_vars, context.metas)\n                if type_ == 1)\n            index, _ = values.pop(\"cluster_var_idx\")\n            name = next(itertools.islice(discrete_vars, index, None)\n            )\n            values[\"cluster_var\"] = (name, 101)\n\n            index = values.pop(\"annotation_var_idx\")[0] - 1\n            if index == -1:\n                values[\"annotation_var\"] = None\n            else:\n                annot_vars = (\n                    (name, type_ + 100)\n                    for name, type_ in itertools.chain(\n                        context.attributes, context.class_vars, context.metas)\n                    if type_ in (1, 3))\n                values[\"annotation_var\"] = next(\n                    itertools.islice(annot_vars, index, None))",
                                "ast_type_str": "ClassDef",
                                "start_lineno": 60,
                                "end_lineno": 562,
                                "parent": null
                            }
                        }
                    ],
                    "added_entities": [],
                    "deleted_entities": []
                }
            ]
        },
        {
            "old_file_content": "# pylint: disable=protected-access\n# Test methods with long descriptive names can omit docstrings\n# pylint: disable=missing-docstring\nimport random\nimport unittest\nfrom unittest.mock import Mock\n\nimport numpy as np\n\nfrom orangewidget.settings import Context\n\nimport Orange.distance\nfrom Orange.data import (\n    Table, Domain, ContinuousVariable, DiscreteVariable, StringVariable)\nfrom Orange.misc import DistMatrix\nfrom Orange.widgets.utils.annotated_data import ANNOTATED_DATA_SIGNAL_NAME\nfrom Orange.widgets.visualize.owsilhouetteplot import OWSilhouettePlot\nfrom Orange.widgets.tests.base import WidgetTest, WidgetOutputsTestMixin\nfrom Orange.widgets.tests.utils import possible_duplicate_table\n\n\nclass TestOWSilhouettePlot(WidgetTest, 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        cls.scorename = \"Silhouette ({})\".format(cls.data.domain.class_var.name)\n\n    def setUp(self):\n        self.widget = self.create_widget(OWSilhouettePlot,\n                                         stored_settings={\"auto_commit\": True})\n        self.widget = self.widget  # type: OWSilhouettePlot\n\n    def test_no_data(self):\n        \"\"\"Check that the widget doesn't crash on empty data\"\"\"\n        self.send_signal(self.widget.Inputs.data, self.data[:0])\n\n    def test_outputs_add_scores(self):\n        # check output when appending scores\n        self.send_signal(self.widget.Inputs.data, self.data)\n        selected_indices = self._select_data()\n        selected = self.get_output(self.widget.Outputs.selected_data)\n        annotated = self.get_output(self.widget.Outputs.annotated_data)\n        self.assertEqual(self.scorename, selected.domain.metas[0].name)\n        self.assertEqual(self.scorename, annotated.domain.metas[0].name)\n        np.testing.assert_array_equal(selected.X, self.data.X[selected_indices])\n\n    def _select_data(self):\n        random.seed(42)\n        points = random.sample(range(0, len(self.data)), 20)\n        self.widget._silplot.setSelection(points)\n        return sorted(points)\n\n    def test_insufficient_clusters(self):\n        iris = self.data\n        data_one_cluster = iris[:3]  # three instances Iris-setosa only\n        self.send_signal(self.widget.Inputs.data, data_one_cluster)\n        self.assertTrue(self.widget.Error.need_two_clusters.is_shown())\n\n        data_singletons = iris[[0, 50, 100]]\n        assert len(np.unique(data_singletons.Y)) == 3  # 3 instances 3 labels\n        self.send_signal(self.widget.Inputs.data, data_singletons)\n        self.assertTrue(self.widget.Error.singleton_clusters_all.is_shown())\n\n    def test_unknowns_in_labels(self):\n        data = self.data[[0, 1, 2, 50, 51, 52, 100, 101, 102]]\n        with data.unlocked(data.Y):\n            data.Y[::3] = np.nan\n        valid = ~np.isnan(data.Y.flatten())\n        self.send_signal(self.widget.Inputs.data, data)\n        output = self.get_output(ANNOTATED_DATA_SIGNAL_NAME)\n        scores = output[:, self.scorename].metas.flatten()\n        self.assertTrue(np.all(np.isnan(scores[::3])))\n        self.assertTrue(np.all(np.isfinite(scores[valid])))\n\n        # Run again on subset with known labels\n        data_1 = data[np.flatnonzero(valid)]\n        self.send_signal(self.widget.Inputs.data, data_1)\n        output_1 = self.get_output(ANNOTATED_DATA_SIGNAL_NAME)\n        scores_1 = output_1[:, self.scorename].metas.flatten()\n        self.assertTrue(np.all(np.isfinite(scores_1)))\n        # the scores must match\n        np.testing.assert_almost_equal(scores_1, scores[valid], decimal=12)\n\n    def test_nan_distances(self):\n        self.widget.distance_idx = 2\n        self.assertEqual(self.widget.Distances[self.widget.distance_idx][0],\n                         'Cosine')\n        data = self.data[[0, 1, 2, 50, 51, 52, 100, 101, 102]]\n        with data.unlocked(data.X):\n            data.X[::3] = 0\n        valid = np.any(data.X != 0, axis=1)\n        self.assertFalse(self.widget.Warning.nan_distances.is_shown())\n        self.send_signal(self.widget.Inputs.data, data)\n        self.assertTrue(np.isnan(self.widget._matrix).any())\n        self.assertTrue(self.widget.Warning.nan_distances.is_shown())\n        output = self.get_output(ANNOTATED_DATA_SIGNAL_NAME)\n        scores = output[:, self.scorename].metas.flatten()\n        self.assertTrue(np.all(np.isnan(scores[::3])))\n        self.assertTrue(np.all(np.isfinite(scores[valid])))\n\n    def test_ignore_categorical(self):\n        data = Table('heart_disease')\n        self.widget.distance_idx = 2\n        self.assertEqual(self.widget.Distances[self.widget.distance_idx][0],\n                         'Cosine')\n        self.assertFalse(self.widget.Warning.ignoring_categorical.is_shown())\n        self.send_signal(self.widget.Inputs.data, data)\n        self.assertTrue(self.widget.Warning.ignoring_categorical.is_shown())\n        output = self.get_output(ANNOTATED_DATA_SIGNAL_NAME)\n        self.assertEqual(len(output.domain.variables), len(data.domain.variables))\n        self.widget.distance_idx = 0\n        self.widget._update()\n        self.assertFalse(self.widget.Warning.ignoring_categorical.is_shown())\n\n    def test_meta_object_dtype(self):\n        # gh-1875: Test on mixed string/discrete metas\n        data = self.data[::5]\n        domain = Domain(data.domain.attributes,\n                        [],\n                        [data.domain[\"iris\"], StringVariable(\"S\")])\n        data = data.from_table(domain, data)\n        self.send_signal(self.widget.Inputs.data, data)\n\n    def test_memory_error(self):\n        \"\"\"\n        Handling memory error.\n        GH-2336\n        Handling value error as well. This value error is in a relation with memory error.\n        GH-2521\n        \"\"\"\n        for i, side_effect in enumerate([MemoryError, ValueError]):\n            data = Table(\"iris\")[::3]\n            self.send_signal(self.widget.Inputs.data, data)\n            self.assertFalse(self.widget.Error.memory_error.is_shown())\n            self.assertFalse(self.widget.Error.value_error.is_shown())\n            with unittest.mock.patch(\"numpy.asarray\", side_effect=side_effect):\n                self.widget._matrix = None\n                self.widget.data = data\n                self.widget._effective_data = data\n                self.widget._update()\n                self.assertTrue(self.widget.Error.memory_error.is_shown() != i)\n                self.assertTrue(self.widget.Error.value_error.is_shown() == i)\n\n    def test_bad_data_range(self):\n        \"\"\"\n        Silhouette Plot now sets axis range properly.\n        GH-2377\n        \"\"\"\n        nan = np.NaN\n        table = Table.from_list(\n            Domain(\n                [ContinuousVariable(\"a\"), ContinuousVariable(\"b\"), ContinuousVariable(\"c\")],\n                [DiscreteVariable(\"d\", values=(\"y\", \"n\"))]),\n            list(zip([4, nan, nan],\n                     [15, nan, nan],\n                     [16, nan, nan],\n                     \"nyy\"))\n        )\n        self.send_signal(self.widget.Inputs.data, table)\n\n    def test_saved_selection(self):\n        self.widget.settingsHandler.pack_data(self.widget)\n        # no assert here, just test is doesn't crash on empty\n\n        iris = Table(\"iris\")\n\n        self.send_signal(self.widget.Inputs.data, iris)\n        random.seed(42)\n        points = random.sample(range(0, len(self.data)), 20)\n        self.widget._silplot.setSelection(points)\n        settings = self.widget.settingsHandler.pack_data(self.widget)\n\n        w = self.create_widget(OWSilhouettePlot, stored_settings=settings)\n        self.send_signal(w.Inputs.data, iris, widget=w)\n        self.assertEqual(len(self.get_output(w.Outputs.selected_data)), 20)\n\n    def test_distance_input(self):\n        widget = self.widget\n        data = Table(\"heart_disease\")[::4]\n        matrix = Orange.distance.Euclidean(data)\n        self.send_signal(widget.Inputs.data, matrix, widget=widget)\n        self.assertIsNotNone(widget.distances)\n        self.assertIsNotNone(widget.data)\n        self.assertFalse(widget._distances_gui_box.isEnabled())\n\n        self.send_signal(widget.Inputs.data, data, widget=widget)\n        self.assertIsNone(widget.distances)\n        self.assertIsNotNone(widget.data)\n        self.assertTrue(widget._distances_gui_box.isEnabled())\n\n    def test_input_distance_no_data(self):\n        widget = self.widget\n        matrix = DistMatrix(\n            np.array([[0, 1, 1], [1, 0, 1], [1, 1, 0]]),\n            row_items=None\n        )\n        self.send_signal(widget.Inputs.data, matrix, widget=widget)\n        self.assertTrue(widget.Error.input_validation_error.is_shown())\n        self.assertIsNone(widget.data)\n        self.assertIsNone(widget.distances)\n        self.send_signal(widget.Inputs.data, None, widget=widget)\n        self.assertFalse(widget.Error.input_validation_error.is_shown())\n\n    def test_no_group_var(self):\n        widget = self.widget\n        data = Table(\"iris\")[::4]\n        data = data[:, data.domain.attributes]\n        matrix = Orange.distance.Euclidean(data)\n        self.send_signal(widget.Inputs.data, matrix, widget=widget)\n\n        self.assertTrue(widget.Error.input_validation_error.is_shown())\n        self.assertIsNone(widget.data)\n        self.assertIsNone(widget.distances)\n\n        self.send_signal(widget.Inputs.data, None, widget=widget)\n        self.assertFalse(widget.Error.input_validation_error.is_shown())\n\n    def test_unique_output_domain(self):\n        widget = self.widget\n        data = possible_duplicate_table('Silhouette (iris)')\n        matrix = Orange.distance.Euclidean(data)\n        self.send_signal(widget.Inputs.data, matrix, widget=widget)\n\n        output = self.get_output(self.widget.Outputs.annotated_data)\n        self.assertEqual(output.domain.metas[0].name, 'Silhouette (iris) (1)')\n\n    def test_report(self):\n        widget = self.widget\n        widget.report_plot = Mock()\n        widget.report_caption = Mock()\n\n        widget.send_report()\n        widget.report_plot.assert_not_called()\n        widget.report_caption.assert_not_called()\n\n        data = Table(\"zoo\")\n        self.send_signal(widget.Inputs.data, data)\n\n        widget.annotation_var = None\n        widget.send_report()\n        widget.report_plot.assert_called()\n        widget.report_caption.assert_called()\n        text = widget.report_caption.call_args[0][0]\n        self.assertIn(data.domain.class_var.name, text)\n        self.assertNotIn(\"nnotated\", text)\n\n        widget.annotation_var = data.domain.metas[0]\n        widget._silplot.rowNamesVisible = lambda: True\n        widget.send_report()\n        text = widget.report_caption.call_args[0][0]\n        self.assertIn(data.domain.class_var.name, text)\n        self.assertIn(\"nnotated\", text)\n        self.assertIn(data.domain.metas[0].name, text)\n\n    def test_migration(self):\n        enc_domain = dict(\n            attributes=(('foo', 1), ('bar', 1), ('baz', 2), ('bax', 1)),\n            class_vars=(('cfoo', 1), ),\n            metas=(('mbar', 3), ('mbaz', 1)))\n\n        context = Context(\n            values=dict(cluster_var_idx=(0, -2), annotation_var_idx=(0, -2)),\n            **enc_domain\n        )\n        OWSilhouettePlot.migrate_context(context, 1)\n        values = context.values\n        self.assertNotIn(\"cluster_var_idx\", values)\n        self.assertNotIn(\"annotation_var_idx\", values)\n        self.assertEqual(values[\"cluster_var\"], (\"foo\", 101))\n        self.assertEqual(values[\"annotation_var\"], None)\n\n        context = Context(\n            values=dict(cluster_var_idx=(4, -2), annotation_var_idx=(4, -2)),\n            **enc_domain\n        )\n        OWSilhouettePlot.migrate_context(context, 1)\n        self.assertNotIn(\"cluster_var_idx\", values)\n        self.assertNotIn(\"annotation_var_idx\", values)\n        self.assertEqual(context.values[\"cluster_var\"], (\"mbaz\", 101))\n        self.assertEqual(context.values[\"annotation_var\"], (\"cfoo\", 101))\n\n        context = Context(\n            values=dict(cluster_var_idx=(4, -2), annotation_var_idx=(5, -2)),\n            **enc_domain\n        )\n        OWSilhouettePlot.migrate_context(context, 1)\n        self.assertEqual(context.values[\"cluster_var\"], (\"mbaz\", 101))\n        self.assertEqual(context.values[\"annotation_var\"], (\"mbar\", 103))\n\n\nif __name__ == \"__main__\":\n    unittest.main()\n",
            "new_file_content": "# pylint: disable=protected-access\n# Test methods with long descriptive names can omit docstrings\n# pylint: disable=missing-docstring\nimport random\nimport unittest\nfrom unittest.mock import Mock\n\nimport numpy as np\n\nfrom orangewidget.settings import Context\n\nimport Orange.distance\nfrom Orange.data import (\n    Table, Domain, ContinuousVariable, DiscreteVariable, StringVariable)\nfrom Orange.misc import DistMatrix\nfrom Orange.widgets.utils.annotated_data import ANNOTATED_DATA_SIGNAL_NAME\nfrom Orange.widgets.visualize.owsilhouetteplot import OWSilhouettePlot\nfrom Orange.widgets.tests.base import WidgetTest, WidgetOutputsTestMixin\nfrom Orange.widgets.tests.utils import possible_duplicate_table\n\n\nclass TestOWSilhouettePlot(WidgetTest, 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        cls.scorename = \"Silhouette ({})\".format(cls.data.domain.class_var.name)\n\n    def setUp(self):\n        self.widget = self.create_widget(OWSilhouettePlot,\n                                         stored_settings={\"auto_commit\": True})\n        self.widget = self.widget  # type: OWSilhouettePlot\n\n    def test_no_data(self):\n        \"\"\"Check that the widget doesn't crash on empty data\"\"\"\n        self.send_signal(self.widget.Inputs.data, self.data[:0])\n\n    def test_outputs_add_scores(self):\n        # check output when appending scores\n        self.send_signal(self.widget.Inputs.data, self.data)\n        selected_indices = self._select_data()\n        selected = self.get_output(self.widget.Outputs.selected_data)\n        annotated = self.get_output(self.widget.Outputs.annotated_data)\n        self.assertEqual(self.scorename, selected.domain.metas[0].name)\n        self.assertEqual(self.scorename, annotated.domain.metas[0].name)\n        np.testing.assert_array_equal(selected.X, self.data.X[selected_indices])\n\n    def _select_data(self):\n        random.seed(42)\n        points = random.sample(range(0, len(self.data)), 20)\n        self.widget._silplot.setSelection(points)\n        return sorted(points)\n\n    def test_insufficient_clusters(self):\n        iris = self.data\n        data_one_cluster = iris[:3]  # three instances Iris-setosa only\n        self.send_signal(self.widget.Inputs.data, data_one_cluster)\n        self.assertTrue(self.widget.Error.need_two_clusters.is_shown())\n\n        data_singletons = iris[[0, 50, 100]]\n        assert len(np.unique(data_singletons.Y)) == 3  # 3 instances 3 labels\n        self.send_signal(self.widget.Inputs.data, data_singletons)\n        self.assertTrue(self.widget.Error.singleton_clusters_all.is_shown())\n\n    def test_unknowns_in_labels(self):\n        data = self.data[[0, 1, 2, 50, 51, 52, 100, 101, 102]]\n        with data.unlocked(data.Y):\n            data.Y[::3] = np.nan\n        valid = ~np.isnan(data.Y.flatten())\n        self.send_signal(self.widget.Inputs.data, data)\n        output = self.get_output(ANNOTATED_DATA_SIGNAL_NAME)\n        scores = output[:, self.scorename].metas.flatten()\n        self.assertTrue(np.all(np.isnan(scores[::3])))\n        self.assertTrue(np.all(np.isfinite(scores[valid])))\n\n        # Run again on subset with known labels\n        data_1 = data[np.flatnonzero(valid)]\n        self.send_signal(self.widget.Inputs.data, data_1)\n        output_1 = self.get_output(ANNOTATED_DATA_SIGNAL_NAME)\n        scores_1 = output_1[:, self.scorename].metas.flatten()\n        self.assertTrue(np.all(np.isfinite(scores_1)))\n        # the scores must match\n        np.testing.assert_almost_equal(scores_1, scores[valid], decimal=12)\n\n    def test_nan_distances(self):\n        self.widget.distance_idx = 2\n        self.assertEqual(self.widget.Distances[self.widget.distance_idx][0],\n                         'Cosine')\n        data = self.data[[0, 1, 2, 50, 51, 52, 100, 101, 102]]\n        with data.unlocked(data.X):\n            data.X[::3] = 0\n        valid = np.any(data.X != 0, axis=1)\n        self.assertFalse(self.widget.Warning.nan_distances.is_shown())\n        self.send_signal(self.widget.Inputs.data, data)\n        self.assertTrue(np.isnan(self.widget._matrix).any())\n        self.assertTrue(self.widget.Warning.nan_distances.is_shown())\n        output = self.get_output(ANNOTATED_DATA_SIGNAL_NAME)\n        scores = output[:, self.scorename].metas.flatten()\n        self.assertTrue(np.all(np.isnan(scores[::3])))\n        self.assertTrue(np.all(np.isfinite(scores[valid])))\n\n    def test_ignore_categorical(self):\n        data = Table('heart_disease')\n        self.widget.distance_idx = 2\n        self.assertEqual(self.widget.Distances[self.widget.distance_idx][0],\n                         'Cosine')\n        self.assertFalse(self.widget.Warning.ignoring_categorical.is_shown())\n        self.send_signal(self.widget.Inputs.data, data)\n        self.assertTrue(self.widget.Warning.ignoring_categorical.is_shown())\n        output = self.get_output(ANNOTATED_DATA_SIGNAL_NAME)\n        self.assertEqual(len(output.domain.variables), len(data.domain.variables))\n        self.widget.distance_idx = 0\n        self.widget._update()\n        self.assertFalse(self.widget.Warning.ignoring_categorical.is_shown())\n\n    def test_meta_object_dtype(self):\n        # gh-1875: Test on mixed string/discrete metas\n        data = self.data[::5]\n        domain = Domain(data.domain.attributes,\n                        [],\n                        [data.domain[\"iris\"], StringVariable(\"S\")])\n        data = data.from_table(domain, data)\n        self.send_signal(self.widget.Inputs.data, data)\n\n    def test_memory_error(self):\n        \"\"\"\n        Handling memory error.\n        GH-2336\n        Handling value error as well. This value error is in a relation with memory error.\n        GH-2521\n        \"\"\"\n        for i, side_effect in enumerate([MemoryError, ValueError]):\n            data = Table(\"iris\")[::3]\n            self.send_signal(self.widget.Inputs.data, data)\n            self.assertFalse(self.widget.Error.memory_error.is_shown())\n            self.assertFalse(self.widget.Error.value_error.is_shown())\n            with unittest.mock.patch(\"numpy.asarray\", side_effect=side_effect):\n                self.widget._matrix = None\n                self.widget.data = data\n                self.widget._effective_data = data\n                self.widget._update()\n                self.assertTrue(self.widget.Error.memory_error.is_shown() != i)\n                self.assertTrue(self.widget.Error.value_error.is_shown() == i)\n\n    def test_bad_data_range(self):\n        \"\"\"\n        Silhouette Plot now sets axis range properly.\n        GH-2377\n        \"\"\"\n        nan = np.NaN\n        table = Table.from_list(\n            Domain(\n                [ContinuousVariable(\"a\"), ContinuousVariable(\"b\"), ContinuousVariable(\"c\")],\n                [DiscreteVariable(\"d\", values=(\"y\", \"n\"))]),\n            list(zip([4, nan, nan],\n                     [15, nan, nan],\n                     [16, nan, nan],\n                     \"nyy\"))\n        )\n        self.send_signal(self.widget.Inputs.data, table)\n\n    def test_saved_selection(self):\n        self.widget.settingsHandler.pack_data(self.widget)\n        # no assert here, just test is doesn't crash on empty\n\n        iris = Table(\"iris\")\n\n        self.send_signal(self.widget.Inputs.data, iris)\n        random.seed(42)\n        points = random.sample(range(0, len(self.data)), 20)\n        self.widget._silplot.setSelection(points)\n        settings = self.widget.settingsHandler.pack_data(self.widget)\n\n        w = self.create_widget(OWSilhouettePlot, stored_settings=settings)\n        self.send_signal(w.Inputs.data, iris, widget=w)\n        self.assertEqual(len(self.get_output(w.Outputs.selected_data)), 20)\n\n    def test_distance_input(self):\n        widget = self.widget\n        data = Table(\"heart_disease\")[::4]\n        matrix = Orange.distance.Euclidean(data)\n        self.send_signal(widget.Inputs.data, matrix, widget=widget)\n        self.assertIsNotNone(widget.distances)\n        self.assertIsNotNone(widget.data)\n        self.assertFalse(widget._distances_gui_box.isEnabled())\n\n        self.send_signal(widget.Inputs.data, data, widget=widget)\n        self.assertIsNone(widget.distances)\n        self.assertIsNotNone(widget.data)\n        self.assertTrue(widget._distances_gui_box.isEnabled())\n\n    def test_input_distance_no_data(self):\n        widget = self.widget\n        matrix = DistMatrix(\n            np.array([[0, 1, 1], [1, 0, 1], [1, 1, 0]]),\n            row_items=None\n        )\n        self.send_signal(widget.Inputs.data, matrix, widget=widget)\n        self.assertTrue(widget.Error.input_validation_error.is_shown())\n        self.assertIsNone(widget.data)\n        self.assertIsNone(widget.distances)\n        self.send_signal(widget.Inputs.data, None, widget=widget)\n        self.assertFalse(widget.Error.input_validation_error.is_shown())\n\n    def test_no_group_var(self):\n        widget = self.widget\n        data = Table(\"iris\")[::4]\n        data = data[:, data.domain.attributes]\n        matrix = Orange.distance.Euclidean(data)\n        self.send_signal(widget.Inputs.data, matrix, widget=widget)\n\n        self.assertTrue(widget.Error.input_validation_error.is_shown())\n        self.assertIsNone(widget.data)\n        self.assertIsNone(widget.distances)\n\n        self.send_signal(widget.Inputs.data, None, widget=widget)\n        self.assertFalse(widget.Error.input_validation_error.is_shown())\n\n    def test_unique_output_domain(self):\n        widget = self.widget\n        data = possible_duplicate_table('Silhouette (iris)')\n        matrix = Orange.distance.Euclidean(data)\n        self.send_signal(widget.Inputs.data, matrix, widget=widget)\n\n        output = self.get_output(self.widget.Outputs.annotated_data)\n        self.assertEqual(output.domain.metas[0].name, 'Silhouette (iris) (1)')\n\n    def test_report(self):\n        widget = self.widget\n        widget.report_plot = Mock()\n        widget.report_caption = Mock()\n\n        widget.send_report()\n        widget.report_plot.assert_not_called()\n        widget.report_caption.assert_not_called()\n\n        data = Table(\"zoo\")\n        self.send_signal(widget.Inputs.data, data)\n\n        widget.annotation_var = None\n        widget.send_report()\n        widget.report_plot.assert_called()\n        widget.report_caption.assert_called()\n        text = widget.report_caption.call_args[0][0]\n        self.assertIn(data.domain.class_var.name, text)\n        self.assertNotIn(\"nnotated\", text)\n\n        widget.annotation_var = data.domain.metas[0]\n        widget._silplot.rowNamesVisible = lambda: True\n        widget.send_report()\n        text = widget.report_caption.call_args[0][0]\n        self.assertIn(data.domain.class_var.name, text)\n        self.assertIn(\"nnotated\", text)\n        self.assertIn(data.domain.metas[0].name, text)\n\n    def test_migration(self):\n        enc_domain = dict(\n            attributes=(('foo', 1), ('bar', 1), ('baz', 1), ('bax', 1),\n                        ('cfoo', 1), ('mbaz', 1)))\n\n        # No annotation\n        context = Context(\n            values=dict(cluster_var_idx=(0, -2), annotation_var_idx=(0, -2)),\n            **enc_domain\n        )\n        OWSilhouettePlot.migrate_context(context, 1)\n        values = context.values\n        self.assertNotIn(\"cluster_var_idx\", values)\n        self.assertNotIn(\"annotation_var_idx\", values)\n        self.assertEqual(values[\"cluster_var\"], (\"foo\", 101))\n        self.assertEqual(values[\"annotation_var\"], None)\n\n        # We have annotation\n        context = Context(\n            values=dict(cluster_var_idx=(2, -2), annotation_var_idx=(4, -2)),\n            **enc_domain\n        )\n        OWSilhouettePlot.migrate_context(context, 1)\n        self.assertNotIn(\"cluster_var_idx\", values)\n        self.assertNotIn(\"annotation_var_idx\", values)\n        self.assertEqual(context.values[\"cluster_var\"], (\"baz\", 101))\n        self.assertEqual(context.values[\"annotation_var\"], (\"bax\", 101))\n\n        # We thought was had annotation, but the index is wrong due to\n        # incorrect domain\n        context = Context(\n            values=dict(cluster_var_idx=(4, -2), annotation_var_idx=(7, -2)),\n            **enc_domain\n        )\n        OWSilhouettePlot.migrate_context(context, 1)\n        self.assertEqual(context.values[\"cluster_var\"], (\"cfoo\", 101))\n        self.assertNotIn(\"annotation_var_idx\", values)\n\n\nif __name__ == \"__main__\":\n    unittest.main()\n",
            "header": {
                "file": {
                    "path": "Orange/widgets/visualize/tests/test_owsilhouetteplot.py"
                },
                "misc_line": null
            },
            "index_line": {
                "old_commit_hash": "192b30e8e",
                "new_commit_hash": "81daceca1",
                "mode": "100644"
            },
            "is_binary_file": false,
            "binary_line": null,
            "minus_file": {
                "path": "a/Orange/widgets/visualize/tests/test_owsilhouetteplot.py"
            },
            "plus_file": {
                "path": "b/Orange/widgets/visualize/tests/test_owsilhouetteplot.py"
            },
            "hunks": [
                {
                    "descriptor": {
                        "old_range": {
                            "start": 259,
                            "length": 10
                        },
                        "new_range": {
                            "start": 259,
                            "length": 10
                        },
                        "section": "class TestOWSilhouettePlot(WidgetTest, WidgetOutputsTestMixin):"
                    },
                    "line_group": {
                        "all_lines": [
                            {
                                "content": "",
                                "type": "context"
                            },
                            {
                                "content": "    def test_migration(self):",
                                "type": "context"
                            },
                            {
                                "content": "        enc_domain = dict(",
                                "type": "context"
                            },
                            {
                                "content": "            attributes=(('foo', 1), ('bar', 1), ('baz', 2), ('bax', 1)),",
                                "type": "deleted"
                            },
                            {
                                "content": "            class_vars=(('cfoo', 1), ),",
                                "type": "deleted"
                            },
                            {
                                "content": "            metas=(('mbar', 3), ('mbaz', 1)))",
                                "type": "deleted"
                            },
                            {
                                "content": "            attributes=(('foo', 1), ('bar', 1), ('baz', 1), ('bax', 1),",
                                "type": "added"
                            },
                            {
                                "content": "                        ('cfoo', 1), ('mbaz', 1)))",
                                "type": "added"
                            },
                            {
                                "content": "",
                                "type": "context"
                            },
                            {
                                "content": "        # No annotation",
                                "type": "added"
                            },
                            {
                                "content": "        context = Context(",
                                "type": "context"
                            },
                            {
                                "content": "            values=dict(cluster_var_idx=(0, -2), annotation_var_idx=(0, -2)),",
                                "type": "context"
                            },
                            {
                                "content": "            **enc_domain",
                                "type": "context"
                            }
                        ]
                    },
                    "modified_entities": [
                        {
                            "file_name": "Orange/widgets/visualize/tests/test_owsilhouetteplot.py",
                            "type": "method",
                            "name": "TestOWSilhouettePlot.test_migration",
                            "content": "class TestOWSilhouettePlot(WidgetTest, 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        cls.scorename = \"Silhouette ({})\".format(cls.data.domain.class_var.name)\n\n    def setUp(self):\n        self.widget = self.create_widget(OWSilhouettePlot,\n                                         stored_settings={\"auto_commit\": True})\n        self.widget = self.widget  # type: OWSilhouettePlot\n\n    def test_no_data(self):\n        \"\"\"Check that the widget doesn't crash on empty data\"\"\"\n        self.send_signal(self.widget.Inputs.data, self.data[:0])\n\n    def test_outputs_add_scores(self):\n        # check output when appending scores\n        self.send_signal(self.widget.Inputs.data, self.data)\n        selected_indices = self._select_data()\n        selected = self.get_output(self.widget.Outputs.selected_data)\n        annotated = self.get_output(self.widget.Outputs.annotated_data)\n        self.assertEqual(self.scorename, selected.domain.metas[0].name)\n        self.assertEqual(self.scorename, annotated.domain.metas[0].name)\n        np.testing.assert_array_equal(selected.X, self.data.X[selected_indices])\n\n    def _select_data(self):\n        random.seed(42)\n        points = random.sample(range(0, len(self.data)), 20)\n        self.widget._silplot.setSelection(points)\n        return sorted(points)\n\n    def test_insufficient_clusters(self):\n        iris = self.data\n        data_one_cluster = iris[:3]  # three instances Iris-setosa only\n        self.send_signal(self.widget.Inputs.data, data_one_cluster)\n        self.assertTrue(self.widget.Error.need_two_clusters.is_shown())\n\n        data_singletons = iris[[0, 50, 100]]\n        assert len(np.unique(data_singletons.Y)) == 3  # 3 instances 3 labels\n        self.send_signal(self.widget.Inputs.data, data_singletons)\n        self.assertTrue(self.widget.Error.singleton_clusters_all.is_shown())\n\n    def test_unknowns_in_labels(self):\n        data = self.data[[0, 1, 2, 50, 51, 52, 100, 101, 102]]\n        with data.unlocked(data.Y):\n            data.Y[::3] = np.nan\n        valid = ~np.isnan(data.Y.flatten())\n        self.send_signal(self.widget.Inputs.data, data)\n        output = self.get_output(ANNOTATED_DATA_SIGNAL_NAME)\n        scores = output[:, self.scorename].metas.flatten()\n        self.assertTrue(np.all(np.isnan(scores[::3])))\n        self.assertTrue(np.all(np.isfinite(scores[valid])))\n\n        # Run again on subset with known labels\n        data_1 = data[np.flatnonzero(valid)]\n        self.send_signal(self.widget.Inputs.data, data_1)\n        output_1 = self.get_output(ANNOTATED_DATA_SIGNAL_NAME)\n        scores_1 = output_1[:, self.scorename].metas.flatten()\n        self.assertTrue(np.all(np.isfinite(scores_1)))\n        # the scores must match\n        np.testing.assert_almost_equal(scores_1, scores[valid], decimal=12)\n\n    def test_nan_distances(self):\n        self.widget.distance_idx = 2\n        self.assertEqual(self.widget.Distances[self.widget.distance_idx][0],\n                         'Cosine')\n        data = self.data[[0, 1, 2, 50, 51, 52, 100, 101, 102]]\n        with data.unlocked(data.X):\n            data.X[::3] = 0\n        valid = np.any(data.X != 0, axis=1)\n        self.assertFalse(self.widget.Warning.nan_distances.is_shown())\n        self.send_signal(self.widget.Inputs.data, data)\n        self.assertTrue(np.isnan(self.widget._matrix).any())\n        self.assertTrue(self.widget.Warning.nan_distances.is_shown())\n        output = self.get_output(ANNOTATED_DATA_SIGNAL_NAME)\n        scores = output[:, self.scorename].metas.flatten()\n        self.assertTrue(np.all(np.isnan(scores[::3])))\n        self.assertTrue(np.all(np.isfinite(scores[valid])))\n\n    def test_ignore_categorical(self):\n        data = Table('heart_disease')\n        self.widget.distance_idx = 2\n        self.assertEqual(self.widget.Distances[self.widget.distance_idx][0],\n                         'Cosine')\n        self.assertFalse(self.widget.Warning.ignoring_categorical.is_shown())\n        self.send_signal(self.widget.Inputs.data, data)\n        self.assertTrue(self.widget.Warning.ignoring_categorical.is_shown())\n        output = self.get_output(ANNOTATED_DATA_SIGNAL_NAME)\n        self.assertEqual(len(output.domain.variables), len(data.domain.variables))\n        self.widget.distance_idx = 0\n        self.widget._update()\n        self.assertFalse(self.widget.Warning.ignoring_categorical.is_shown())\n\n    def test_meta_object_dtype(self):\n        # gh-1875: Test on mixed string/discrete metas\n        data = self.data[::5]\n        domain = Domain(data.domain.attributes,\n                        [],\n                        [data.domain[\"iris\"], StringVariable(\"S\")])\n        data = data.from_table(domain, data)\n        self.send_signal(self.widget.Inputs.data, data)\n\n    def test_memory_error(self):\n        \"\"\"\n        Handling memory error.\n        GH-2336\n        Handling value error as well. This value error is in a relation with memory error.\n        GH-2521\n        \"\"\"\n        for i, side_effect in enumerate([MemoryError, ValueError]):\n            data = Table(\"iris\")[::3]\n            self.send_signal(self.widget.Inputs.data, data)\n            self.assertFalse(self.widget.Error.memory_error.is_shown())\n            self.assertFalse(self.widget.Error.value_error.is_shown())\n            with unittest.mock.patch(\"numpy.asarray\", side_effect=side_effect):\n                self.widget._matrix = None\n                self.widget.data = data\n                self.widget._effective_data = data\n                self.widget._update()\n                self.assertTrue(self.widget.Error.memory_error.is_shown() != i)\n                self.assertTrue(self.widget.Error.value_error.is_shown() == i)\n\n    def test_bad_data_range(self):\n        \"\"\"\n        Silhouette Plot now sets axis range properly.\n        GH-2377\n        \"\"\"\n        nan = np.NaN\n        table = Table.from_list(\n            Domain(\n                [ContinuousVariable(\"a\"), ContinuousVariable(\"b\"), ContinuousVariable(\"c\")],\n                [DiscreteVariable(\"d\", values=(\"y\", \"n\"))]),\n            list(zip([4, nan, nan],\n                     [15, nan, nan],\n                     [16, nan, nan],\n                     \"nyy\"))\n        )\n        self.send_signal(self.widget.Inputs.data, table)\n\n    def test_saved_selection(self):\n        self.widget.settingsHandler.pack_data(self.widget)\n        # no assert here, just test is doesn't crash on empty\n\n        iris = Table(\"iris\")\n\n        self.send_signal(self.widget.Inputs.data, iris)\n        random.seed(42)\n        points = random.sample(range(0, len(self.data)), 20)\n        self.widget._silplot.setSelection(points)\n        settings = self.widget.settingsHandler.pack_data(self.widget)\n\n        w = self.create_widget(OWSilhouettePlot, stored_settings=settings)\n        self.send_signal(w.Inputs.data, iris, widget=w)\n        self.assertEqual(len(self.get_output(w.Outputs.selected_data)), 20)\n\n    def test_distance_input(self):\n        widget = self.widget\n        data = Table(\"heart_disease\")[::4]\n        matrix = Orange.distance.Euclidean(data)\n        self.send_signal(widget.Inputs.data, matrix, widget=widget)\n        self.assertIsNotNone(widget.distances)\n        self.assertIsNotNone(widget.data)\n        self.assertFalse(widget._distances_gui_box.isEnabled())\n\n        self.send_signal(widget.Inputs.data, data, widget=widget)\n        self.assertIsNone(widget.distances)\n        self.assertIsNotNone(widget.data)\n        self.assertTrue(widget._distances_gui_box.isEnabled())\n\n    def test_input_distance_no_data(self):\n        widget = self.widget\n        matrix = DistMatrix(\n            np.array([[0, 1, 1], [1, 0, 1], [1, 1, 0]]),\n            row_items=None\n        )\n        self.send_signal(widget.Inputs.data, matrix, widget=widget)\n        self.assertTrue(widget.Error.input_validation_error.is_shown())\n        self.assertIsNone(widget.data)\n        self.assertIsNone(widget.distances)\n        self.send_signal(widget.Inputs.data, None, widget=widget)\n        self.assertFalse(widget.Error.input_validation_error.is_shown())\n\n    def test_no_group_var(self):\n        widget = self.widget\n        data = Table(\"iris\")[::4]\n        data = data[:, data.domain.attributes]\n        matrix = Orange.distance.Euclidean(data)\n        self.send_signal(widget.Inputs.data, matrix, widget=widget)\n\n        self.assertTrue(widget.Error.input_validation_error.is_shown())\n        self.assertIsNone(widget.data)\n        self.assertIsNone(widget.distances)\n\n        self.send_signal(widget.Inputs.data, None, widget=widget)\n        self.assertFalse(widget.Error.input_validation_error.is_shown())\n\n    def test_unique_output_domain(self):\n        widget = self.widget\n        data = possible_duplicate_table('Silhouette (iris)')\n        matrix = Orange.distance.Euclidean(data)\n        self.send_signal(widget.Inputs.data, matrix, widget=widget)\n\n        output = self.get_output(self.widget.Outputs.annotated_data)\n        self.assertEqual(output.domain.metas[0].name, 'Silhouette (iris) (1)')\n\n    def test_report(self):\n        widget = self.widget\n        widget.report_plot = Mock()\n        widget.report_caption = Mock()\n\n        widget.send_report()\n        widget.report_plot.assert_not_called()\n        widget.report_caption.assert_not_called()\n\n        data = Table(\"zoo\")\n        self.send_signal(widget.Inputs.data, data)\n\n        widget.annotation_var = None\n        widget.send_report()\n        widget.report_plot.assert_called()\n        widget.report_caption.assert_called()\n        text = widget.report_caption.call_args[0][0]\n        self.assertIn(data.domain.class_var.name, text)\n        self.assertNotIn(\"nnotated\", text)\n\n        widget.annotation_var = data.domain.metas[0]\n        widget._silplot.rowNamesVisible = lambda: True\n        widget.send_report()\n        text = widget.report_caption.call_args[0][0]\n        self.assertIn(data.domain.class_var.name, text)\n        self.assertIn(\"nnotated\", text)\n        self.assertIn(data.domain.metas[0].name, text)\n\n    def test_migration(self):\n        enc_domain = dict(\n            attributes=(('foo', 1), ('bar', 1), ('baz', 2), ('bax', 1)),\n            class_vars=(('cfoo', 1), ),\n            metas=(('mbar', 3), ('mbaz', 1)))\n\n        context = Context(\n            values=dict(cluster_var_idx=(0, -2), annotation_var_idx=(0, -2)),\n            **enc_domain\n        )\n        OWSilhouettePlot.migrate_context(context, 1)\n        values = context.values\n        self.assertNotIn(\"cluster_var_idx\", values)\n        self.assertNotIn(\"annotation_var_idx\", values)\n        self.assertEqual(values[\"cluster_var\"], (\"foo\", 101))\n        self.assertEqual(values[\"annotation_var\"], None)\n\n        context = Context(\n            values=dict(cluster_var_idx=(4, -2), annotation_var_idx=(4, -2)),\n            **enc_domain\n        )\n        OWSilhouettePlot.migrate_context(context, 1)\n        self.assertNotIn(\"cluster_var_idx\", values)\n        self.assertNotIn(\"annotation_var_idx\", values)\n        self.assertEqual(context.values[\"cluster_var\"], (\"mbaz\", 101))\n        self.assertEqual(context.values[\"annotation_var\"], (\"cfoo\", 101))\n\n        context = Context(\n            values=dict(cluster_var_idx=(4, -2), annotation_var_idx=(5, -2)),\n            **enc_domain\n        )\n        OWSilhouettePlot.migrate_context(context, 1)\n        self.assertEqual(context.values[\"cluster_var\"], (\"mbaz\", 101))\n        self.assertEqual(context.values[\"annotation_var\"], (\"mbar\", 103))",
                            "ast_type_str": "ClassDef",
                            "start_lineno": 260,
                            "end_lineno": 293,
                            "parent": {
                                "file_name": "Orange/widgets/visualize/tests/test_owsilhouetteplot.py",
                                "type": "class",
                                "name": "TestOWSilhouettePlot",
                                "content": "class TestOWSilhouettePlot(WidgetTest, 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        cls.scorename = \"Silhouette ({})\".format(cls.data.domain.class_var.name)\n\n    def setUp(self):\n        self.widget = self.create_widget(OWSilhouettePlot,\n                                         stored_settings={\"auto_commit\": True})\n        self.widget = self.widget  # type: OWSilhouettePlot\n\n    def test_no_data(self):\n        \"\"\"Check that the widget doesn't crash on empty data\"\"\"\n        self.send_signal(self.widget.Inputs.data, self.data[:0])\n\n    def test_outputs_add_scores(self):\n        # check output when appending scores\n        self.send_signal(self.widget.Inputs.data, self.data)\n        selected_indices = self._select_data()\n        selected = self.get_output(self.widget.Outputs.selected_data)\n        annotated = self.get_output(self.widget.Outputs.annotated_data)\n        self.assertEqual(self.scorename, selected.domain.metas[0].name)\n        self.assertEqual(self.scorename, annotated.domain.metas[0].name)\n        np.testing.assert_array_equal(selected.X, self.data.X[selected_indices])\n\n    def _select_data(self):\n        random.seed(42)\n        points = random.sample(range(0, len(self.data)), 20)\n        self.widget._silplot.setSelection(points)\n        return sorted(points)\n\n    def test_insufficient_clusters(self):\n        iris = self.data\n        data_one_cluster = iris[:3]  # three instances Iris-setosa only\n        self.send_signal(self.widget.Inputs.data, data_one_cluster)\n        self.assertTrue(self.widget.Error.need_two_clusters.is_shown())\n\n        data_singletons = iris[[0, 50, 100]]\n        assert len(np.unique(data_singletons.Y)) == 3  # 3 instances 3 labels\n        self.send_signal(self.widget.Inputs.data, data_singletons)\n        self.assertTrue(self.widget.Error.singleton_clusters_all.is_shown())\n\n    def test_unknowns_in_labels(self):\n        data = self.data[[0, 1, 2, 50, 51, 52, 100, 101, 102]]\n        with data.unlocked(data.Y):\n            data.Y[::3] = np.nan\n        valid = ~np.isnan(data.Y.flatten())\n        self.send_signal(self.widget.Inputs.data, data)\n        output = self.get_output(ANNOTATED_DATA_SIGNAL_NAME)\n        scores = output[:, self.scorename].metas.flatten()\n        self.assertTrue(np.all(np.isnan(scores[::3])))\n        self.assertTrue(np.all(np.isfinite(scores[valid])))\n\n        # Run again on subset with known labels\n        data_1 = data[np.flatnonzero(valid)]\n        self.send_signal(self.widget.Inputs.data, data_1)\n        output_1 = self.get_output(ANNOTATED_DATA_SIGNAL_NAME)\n        scores_1 = output_1[:, self.scorename].metas.flatten()\n        self.assertTrue(np.all(np.isfinite(scores_1)))\n        # the scores must match\n        np.testing.assert_almost_equal(scores_1, scores[valid], decimal=12)\n\n    def test_nan_distances(self):\n        self.widget.distance_idx = 2\n        self.assertEqual(self.widget.Distances[self.widget.distance_idx][0],\n                         'Cosine')\n        data = self.data[[0, 1, 2, 50, 51, 52, 100, 101, 102]]\n        with data.unlocked(data.X):\n            data.X[::3] = 0\n        valid = np.any(data.X != 0, axis=1)\n        self.assertFalse(self.widget.Warning.nan_distances.is_shown())\n        self.send_signal(self.widget.Inputs.data, data)\n        self.assertTrue(np.isnan(self.widget._matrix).any())\n        self.assertTrue(self.widget.Warning.nan_distances.is_shown())\n        output = self.get_output(ANNOTATED_DATA_SIGNAL_NAME)\n        scores = output[:, self.scorename].metas.flatten()\n        self.assertTrue(np.all(np.isnan(scores[::3])))\n        self.assertTrue(np.all(np.isfinite(scores[valid])))\n\n    def test_ignore_categorical(self):\n        data = Table('heart_disease')\n        self.widget.distance_idx = 2\n        self.assertEqual(self.widget.Distances[self.widget.distance_idx][0],\n                         'Cosine')\n        self.assertFalse(self.widget.Warning.ignoring_categorical.is_shown())\n        self.send_signal(self.widget.Inputs.data, data)\n        self.assertTrue(self.widget.Warning.ignoring_categorical.is_shown())\n        output = self.get_output(ANNOTATED_DATA_SIGNAL_NAME)\n        self.assertEqual(len(output.domain.variables), len(data.domain.variables))\n        self.widget.distance_idx = 0\n        self.widget._update()\n        self.assertFalse(self.widget.Warning.ignoring_categorical.is_shown())\n\n    def test_meta_object_dtype(self):\n        # gh-1875: Test on mixed string/discrete metas\n        data = self.data[::5]\n        domain = Domain(data.domain.attributes,\n                        [],\n                        [data.domain[\"iris\"], StringVariable(\"S\")])\n        data = data.from_table(domain, data)\n        self.send_signal(self.widget.Inputs.data, data)\n\n    def test_memory_error(self):\n        \"\"\"\n        Handling memory error.\n        GH-2336\n        Handling value error as well. This value error is in a relation with memory error.\n        GH-2521\n        \"\"\"\n        for i, side_effect in enumerate([MemoryError, ValueError]):\n            data = Table(\"iris\")[::3]\n            self.send_signal(self.widget.Inputs.data, data)\n            self.assertFalse(self.widget.Error.memory_error.is_shown())\n            self.assertFalse(self.widget.Error.value_error.is_shown())\n            with unittest.mock.patch(\"numpy.asarray\", side_effect=side_effect):\n                self.widget._matrix = None\n                self.widget.data = data\n                self.widget._effective_data = data\n                self.widget._update()\n                self.assertTrue(self.widget.Error.memory_error.is_shown() != i)\n                self.assertTrue(self.widget.Error.value_error.is_shown() == i)\n\n    def test_bad_data_range(self):\n        \"\"\"\n        Silhouette Plot now sets axis range properly.\n        GH-2377\n        \"\"\"\n        nan = np.NaN\n        table = Table.from_list(\n            Domain(\n                [ContinuousVariable(\"a\"), ContinuousVariable(\"b\"), ContinuousVariable(\"c\")],\n                [DiscreteVariable(\"d\", values=(\"y\", \"n\"))]),\n            list(zip([4, nan, nan],\n                     [15, nan, nan],\n                     [16, nan, nan],\n                     \"nyy\"))\n        )\n        self.send_signal(self.widget.Inputs.data, table)\n\n    def test_saved_selection(self):\n        self.widget.settingsHandler.pack_data(self.widget)\n        # no assert here, just test is doesn't crash on empty\n\n        iris = Table(\"iris\")\n\n        self.send_signal(self.widget.Inputs.data, iris)\n        random.seed(42)\n        points = random.sample(range(0, len(self.data)), 20)\n        self.widget._silplot.setSelection(points)\n        settings = self.widget.settingsHandler.pack_data(self.widget)\n\n        w = self.create_widget(OWSilhouettePlot, stored_settings=settings)\n        self.send_signal(w.Inputs.data, iris, widget=w)\n        self.assertEqual(len(self.get_output(w.Outputs.selected_data)), 20)\n\n    def test_distance_input(self):\n        widget = self.widget\n        data = Table(\"heart_disease\")[::4]\n        matrix = Orange.distance.Euclidean(data)\n        self.send_signal(widget.Inputs.data, matrix, widget=widget)\n        self.assertIsNotNone(widget.distances)\n        self.assertIsNotNone(widget.data)\n        self.assertFalse(widget._distances_gui_box.isEnabled())\n\n        self.send_signal(widget.Inputs.data, data, widget=widget)\n        self.assertIsNone(widget.distances)\n        self.assertIsNotNone(widget.data)\n        self.assertTrue(widget._distances_gui_box.isEnabled())\n\n    def test_input_distance_no_data(self):\n        widget = self.widget\n        matrix = DistMatrix(\n            np.array([[0, 1, 1], [1, 0, 1], [1, 1, 0]]),\n            row_items=None\n        )\n        self.send_signal(widget.Inputs.data, matrix, widget=widget)\n        self.assertTrue(widget.Error.input_validation_error.is_shown())\n        self.assertIsNone(widget.data)\n        self.assertIsNone(widget.distances)\n        self.send_signal(widget.Inputs.data, None, widget=widget)\n        self.assertFalse(widget.Error.input_validation_error.is_shown())\n\n    def test_no_group_var(self):\n        widget = self.widget\n        data = Table(\"iris\")[::4]\n        data = data[:, data.domain.attributes]\n        matrix = Orange.distance.Euclidean(data)\n        self.send_signal(widget.Inputs.data, matrix, widget=widget)\n\n        self.assertTrue(widget.Error.input_validation_error.is_shown())\n        self.assertIsNone(widget.data)\n        self.assertIsNone(widget.distances)\n\n        self.send_signal(widget.Inputs.data, None, widget=widget)\n        self.assertFalse(widget.Error.input_validation_error.is_shown())\n\n    def test_unique_output_domain(self):\n        widget = self.widget\n        data = possible_duplicate_table('Silhouette (iris)')\n        matrix = Orange.distance.Euclidean(data)\n        self.send_signal(widget.Inputs.data, matrix, widget=widget)\n\n        output = self.get_output(self.widget.Outputs.annotated_data)\n        self.assertEqual(output.domain.metas[0].name, 'Silhouette (iris) (1)')\n\n    def test_report(self):\n        widget = self.widget\n        widget.report_plot = Mock()\n        widget.report_caption = Mock()\n\n        widget.send_report()\n        widget.report_plot.assert_not_called()\n        widget.report_caption.assert_not_called()\n\n        data = Table(\"zoo\")\n        self.send_signal(widget.Inputs.data, data)\n\n        widget.annotation_var = None\n        widget.send_report()\n        widget.report_plot.assert_called()\n        widget.report_caption.assert_called()\n        text = widget.report_caption.call_args[0][0]\n        self.assertIn(data.domain.class_var.name, text)\n        self.assertNotIn(\"nnotated\", text)\n\n        widget.annotation_var = data.domain.metas[0]\n        widget._silplot.rowNamesVisible = lambda: True\n        widget.send_report()\n        text = widget.report_caption.call_args[0][0]\n        self.assertIn(data.domain.class_var.name, text)\n        self.assertIn(\"nnotated\", text)\n        self.assertIn(data.domain.metas[0].name, text)\n\n    def test_migration(self):\n        enc_domain = dict(\n            attributes=(('foo', 1), ('bar', 1), ('baz', 2), ('bax', 1)),\n            class_vars=(('cfoo', 1), ),\n            metas=(('mbar', 3), ('mbaz', 1)))\n\n        context = Context(\n            values=dict(cluster_var_idx=(0, -2), annotation_var_idx=(0, -2)),\n            **enc_domain\n        )\n        OWSilhouettePlot.migrate_context(context, 1)\n        values = context.values\n        self.assertNotIn(\"cluster_var_idx\", values)\n        self.assertNotIn(\"annotation_var_idx\", values)\n        self.assertEqual(values[\"cluster_var\"], (\"foo\", 101))\n        self.assertEqual(values[\"annotation_var\"], None)\n\n        context = Context(\n            values=dict(cluster_var_idx=(4, -2), annotation_var_idx=(4, -2)),\n            **enc_domain\n        )\n        OWSilhouettePlot.migrate_context(context, 1)\n        self.assertNotIn(\"cluster_var_idx\", values)\n        self.assertNotIn(\"annotation_var_idx\", values)\n        self.assertEqual(context.values[\"cluster_var\"], (\"mbaz\", 101))\n        self.assertEqual(context.values[\"annotation_var\"], (\"cfoo\", 101))\n\n        context = Context(\n            values=dict(cluster_var_idx=(4, -2), annotation_var_idx=(5, -2)),\n            **enc_domain\n        )\n        OWSilhouettePlot.migrate_context(context, 1)\n        self.assertEqual(context.values[\"cluster_var\"], (\"mbaz\", 101))\n        self.assertEqual(context.values[\"annotation_var\"], (\"mbar\", 103))",
                                "ast_type_str": "ClassDef",
                                "start_lineno": 22,
                                "end_lineno": 293,
                                "parent": null
                            }
                        },
                        {
                            "file_name": "Orange/widgets/visualize/tests/test_owsilhouetteplot.py",
                            "type": "class",
                            "name": "TestOWSilhouettePlot",
                            "content": "class TestOWSilhouettePlot(WidgetTest, 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        cls.scorename = \"Silhouette ({})\".format(cls.data.domain.class_var.name)\n\n    def setUp(self):\n        self.widget = self.create_widget(OWSilhouettePlot,\n                                         stored_settings={\"auto_commit\": True})\n        self.widget = self.widget  # type: OWSilhouettePlot\n\n    def test_no_data(self):\n        \"\"\"Check that the widget doesn't crash on empty data\"\"\"\n        self.send_signal(self.widget.Inputs.data, self.data[:0])\n\n    def test_outputs_add_scores(self):\n        # check output when appending scores\n        self.send_signal(self.widget.Inputs.data, self.data)\n        selected_indices = self._select_data()\n        selected = self.get_output(self.widget.Outputs.selected_data)\n        annotated = self.get_output(self.widget.Outputs.annotated_data)\n        self.assertEqual(self.scorename, selected.domain.metas[0].name)\n        self.assertEqual(self.scorename, annotated.domain.metas[0].name)\n        np.testing.assert_array_equal(selected.X, self.data.X[selected_indices])\n\n    def _select_data(self):\n        random.seed(42)\n        points = random.sample(range(0, len(self.data)), 20)\n        self.widget._silplot.setSelection(points)\n        return sorted(points)\n\n    def test_insufficient_clusters(self):\n        iris = self.data\n        data_one_cluster = iris[:3]  # three instances Iris-setosa only\n        self.send_signal(self.widget.Inputs.data, data_one_cluster)\n        self.assertTrue(self.widget.Error.need_two_clusters.is_shown())\n\n        data_singletons = iris[[0, 50, 100]]\n        assert len(np.unique(data_singletons.Y)) == 3  # 3 instances 3 labels\n        self.send_signal(self.widget.Inputs.data, data_singletons)\n        self.assertTrue(self.widget.Error.singleton_clusters_all.is_shown())\n\n    def test_unknowns_in_labels(self):\n        data = self.data[[0, 1, 2, 50, 51, 52, 100, 101, 102]]\n        with data.unlocked(data.Y):\n            data.Y[::3] = np.nan\n        valid = ~np.isnan(data.Y.flatten())\n        self.send_signal(self.widget.Inputs.data, data)\n        output = self.get_output(ANNOTATED_DATA_SIGNAL_NAME)\n        scores = output[:, self.scorename].metas.flatten()\n        self.assertTrue(np.all(np.isnan(scores[::3])))\n        self.assertTrue(np.all(np.isfinite(scores[valid])))\n\n        # Run again on subset with known labels\n        data_1 = data[np.flatnonzero(valid)]\n        self.send_signal(self.widget.Inputs.data, data_1)\n        output_1 = self.get_output(ANNOTATED_DATA_SIGNAL_NAME)\n        scores_1 = output_1[:, self.scorename].metas.flatten()\n        self.assertTrue(np.all(np.isfinite(scores_1)))\n        # the scores must match\n        np.testing.assert_almost_equal(scores_1, scores[valid], decimal=12)\n\n    def test_nan_distances(self):\n        self.widget.distance_idx = 2\n        self.assertEqual(self.widget.Distances[self.widget.distance_idx][0],\n                         'Cosine')\n        data = self.data[[0, 1, 2, 50, 51, 52, 100, 101, 102]]\n        with data.unlocked(data.X):\n            data.X[::3] = 0\n        valid = np.any(data.X != 0, axis=1)\n        self.assertFalse(self.widget.Warning.nan_distances.is_shown())\n        self.send_signal(self.widget.Inputs.data, data)\n        self.assertTrue(np.isnan(self.widget._matrix).any())\n        self.assertTrue(self.widget.Warning.nan_distances.is_shown())\n        output = self.get_output(ANNOTATED_DATA_SIGNAL_NAME)\n        scores = output[:, self.scorename].metas.flatten()\n        self.assertTrue(np.all(np.isnan(scores[::3])))\n        self.assertTrue(np.all(np.isfinite(scores[valid])))\n\n    def test_ignore_categorical(self):\n        data = Table('heart_disease')\n        self.widget.distance_idx = 2\n        self.assertEqual(self.widget.Distances[self.widget.distance_idx][0],\n                         'Cosine')\n        self.assertFalse(self.widget.Warning.ignoring_categorical.is_shown())\n        self.send_signal(self.widget.Inputs.data, data)\n        self.assertTrue(self.widget.Warning.ignoring_categorical.is_shown())\n        output = self.get_output(ANNOTATED_DATA_SIGNAL_NAME)\n        self.assertEqual(len(output.domain.variables), len(data.domain.variables))\n        self.widget.distance_idx = 0\n        self.widget._update()\n        self.assertFalse(self.widget.Warning.ignoring_categorical.is_shown())\n\n    def test_meta_object_dtype(self):\n        # gh-1875: Test on mixed string/discrete metas\n        data = self.data[::5]\n        domain = Domain(data.domain.attributes,\n                        [],\n                        [data.domain[\"iris\"], StringVariable(\"S\")])\n        data = data.from_table(domain, data)\n        self.send_signal(self.widget.Inputs.data, data)\n\n    def test_memory_error(self):\n        \"\"\"\n        Handling memory error.\n        GH-2336\n        Handling value error as well. This value error is in a relation with memory error.\n        GH-2521\n        \"\"\"\n        for i, side_effect in enumerate([MemoryError, ValueError]):\n            data = Table(\"iris\")[::3]\n            self.send_signal(self.widget.Inputs.data, data)\n            self.assertFalse(self.widget.Error.memory_error.is_shown())\n            self.assertFalse(self.widget.Error.value_error.is_shown())\n            with unittest.mock.patch(\"numpy.asarray\", side_effect=side_effect):\n                self.widget._matrix = None\n                self.widget.data = data\n                self.widget._effective_data = data\n                self.widget._update()\n                self.assertTrue(self.widget.Error.memory_error.is_shown() != i)\n                self.assertTrue(self.widget.Error.value_error.is_shown() == i)\n\n    def test_bad_data_range(self):\n        \"\"\"\n        Silhouette Plot now sets axis range properly.\n        GH-2377\n        \"\"\"\n        nan = np.NaN\n        table = Table.from_list(\n            Domain(\n                [ContinuousVariable(\"a\"), ContinuousVariable(\"b\"), ContinuousVariable(\"c\")],\n                [DiscreteVariable(\"d\", values=(\"y\", \"n\"))]),\n            list(zip([4, nan, nan],\n                     [15, nan, nan],\n                     [16, nan, nan],\n                     \"nyy\"))\n        )\n        self.send_signal(self.widget.Inputs.data, table)\n\n    def test_saved_selection(self):\n        self.widget.settingsHandler.pack_data(self.widget)\n        # no assert here, just test is doesn't crash on empty\n\n        iris = Table(\"iris\")\n\n        self.send_signal(self.widget.Inputs.data, iris)\n        random.seed(42)\n        points = random.sample(range(0, len(self.data)), 20)\n        self.widget._silplot.setSelection(points)\n        settings = self.widget.settingsHandler.pack_data(self.widget)\n\n        w = self.create_widget(OWSilhouettePlot, stored_settings=settings)\n        self.send_signal(w.Inputs.data, iris, widget=w)\n        self.assertEqual(len(self.get_output(w.Outputs.selected_data)), 20)\n\n    def test_distance_input(self):\n        widget = self.widget\n        data = Table(\"heart_disease\")[::4]\n        matrix = Orange.distance.Euclidean(data)\n        self.send_signal(widget.Inputs.data, matrix, widget=widget)\n        self.assertIsNotNone(widget.distances)\n        self.assertIsNotNone(widget.data)\n        self.assertFalse(widget._distances_gui_box.isEnabled())\n\n        self.send_signal(widget.Inputs.data, data, widget=widget)\n        self.assertIsNone(widget.distances)\n        self.assertIsNotNone(widget.data)\n        self.assertTrue(widget._distances_gui_box.isEnabled())\n\n    def test_input_distance_no_data(self):\n        widget = self.widget\n        matrix = DistMatrix(\n            np.array([[0, 1, 1], [1, 0, 1], [1, 1, 0]]),\n            row_items=None\n        )\n        self.send_signal(widget.Inputs.data, matrix, widget=widget)\n        self.assertTrue(widget.Error.input_validation_error.is_shown())\n        self.assertIsNone(widget.data)\n        self.assertIsNone(widget.distances)\n        self.send_signal(widget.Inputs.data, None, widget=widget)\n        self.assertFalse(widget.Error.input_validation_error.is_shown())\n\n    def test_no_group_var(self):\n        widget = self.widget\n        data = Table(\"iris\")[::4]\n        data = data[:, data.domain.attributes]\n        matrix = Orange.distance.Euclidean(data)\n        self.send_signal(widget.Inputs.data, matrix, widget=widget)\n\n        self.assertTrue(widget.Error.input_validation_error.is_shown())\n        self.assertIsNone(widget.data)\n        self.assertIsNone(widget.distances)\n\n        self.send_signal(widget.Inputs.data, None, widget=widget)\n        self.assertFalse(widget.Error.input_validation_error.is_shown())\n\n    def test_unique_output_domain(self):\n        widget = self.widget\n        data = possible_duplicate_table('Silhouette (iris)')\n        matrix = Orange.distance.Euclidean(data)\n        self.send_signal(widget.Inputs.data, matrix, widget=widget)\n\n        output = self.get_output(self.widget.Outputs.annotated_data)\n        self.assertEqual(output.domain.metas[0].name, 'Silhouette (iris) (1)')\n\n    def test_report(self):\n        widget = self.widget\n        widget.report_plot = Mock()\n        widget.report_caption = Mock()\n\n        widget.send_report()\n        widget.report_plot.assert_not_called()\n        widget.report_caption.assert_not_called()\n\n        data = Table(\"zoo\")\n        self.send_signal(widget.Inputs.data, data)\n\n        widget.annotation_var = None\n        widget.send_report()\n        widget.report_plot.assert_called()\n        widget.report_caption.assert_called()\n        text = widget.report_caption.call_args[0][0]\n        self.assertIn(data.domain.class_var.name, text)\n        self.assertNotIn(\"nnotated\", text)\n\n        widget.annotation_var = data.domain.metas[0]\n        widget._silplot.rowNamesVisible = lambda: True\n        widget.send_report()\n        text = widget.report_caption.call_args[0][0]\n        self.assertIn(data.domain.class_var.name, text)\n        self.assertIn(\"nnotated\", text)\n        self.assertIn(data.domain.metas[0].name, text)\n\n    def test_migration(self):\n        enc_domain = dict(\n            attributes=(('foo', 1), ('bar', 1), ('baz', 2), ('bax', 1)),\n            class_vars=(('cfoo', 1), ),\n            metas=(('mbar', 3), ('mbaz', 1)))\n\n        context = Context(\n            values=dict(cluster_var_idx=(0, -2), annotation_var_idx=(0, -2)),\n            **enc_domain\n        )\n        OWSilhouettePlot.migrate_context(context, 1)\n        values = context.values\n        self.assertNotIn(\"cluster_var_idx\", values)\n        self.assertNotIn(\"annotation_var_idx\", values)\n        self.assertEqual(values[\"cluster_var\"], (\"foo\", 101))\n        self.assertEqual(values[\"annotation_var\"], None)\n\n        context = Context(\n            values=dict(cluster_var_idx=(4, -2), annotation_var_idx=(4, -2)),\n            **enc_domain\n        )\n        OWSilhouettePlot.migrate_context(context, 1)\n        self.assertNotIn(\"cluster_var_idx\", values)\n        self.assertNotIn(\"annotation_var_idx\", values)\n        self.assertEqual(context.values[\"cluster_var\"], (\"mbaz\", 101))\n        self.assertEqual(context.values[\"annotation_var\"], (\"cfoo\", 101))\n\n        context = Context(\n            values=dict(cluster_var_idx=(4, -2), annotation_var_idx=(5, -2)),\n            **enc_domain\n        )\n        OWSilhouettePlot.migrate_context(context, 1)\n        self.assertEqual(context.values[\"cluster_var\"], (\"mbaz\", 101))\n        self.assertEqual(context.values[\"annotation_var\"], (\"mbar\", 103))",
                            "ast_type_str": "ClassDef",
                            "start_lineno": 22,
                            "end_lineno": 293,
                            "parent": null
                        }
                    ],
                    "added_entities": [],
                    "deleted_entities": []
                },
                {
                    "descriptor": {
                        "old_range": {
                            "start": 274,
                            "length": 23
                        },
                        "new_range": {
                            "start": 274,
                            "length": 26
                        },
                        "section": "class TestOWSilhouettePlot(WidgetTest, WidgetOutputsTestMixin):"
                    },
                    "line_group": {
                        "all_lines": [
                            {
                                "content": "        self.assertEqual(values[\"cluster_var\"], (\"foo\", 101))",
                                "type": "context"
                            },
                            {
                                "content": "        self.assertEqual(values[\"annotation_var\"], None)",
                                "type": "context"
                            },
                            {
                                "content": "",
                                "type": "context"
                            },
                            {
                                "content": "        # We have annotation",
                                "type": "added"
                            },
                            {
                                "content": "        context = Context(",
                                "type": "context"
                            },
                            {
                                "content": "            values=dict(cluster_var_idx=(4, -2), annotation_var_idx=(4, -2)),",
                                "type": "deleted"
                            },
                            {
                                "content": "            values=dict(cluster_var_idx=(2, -2), annotation_var_idx=(4, -2)),",
                                "type": "added"
                            },
                            {
                                "content": "            **enc_domain",
                                "type": "context"
                            },
                            {
                                "content": "        )",
                                "type": "context"
                            },
                            {
                                "content": "        OWSilhouettePlot.migrate_context(context, 1)",
                                "type": "context"
                            },
                            {
                                "content": "        self.assertNotIn(\"cluster_var_idx\", values)",
                                "type": "context"
                            },
                            {
                                "content": "        self.assertNotIn(\"annotation_var_idx\", values)",
                                "type": "context"
                            },
                            {
                                "content": "        self.assertEqual(context.values[\"cluster_var\"], (\"mbaz\", 101))",
                                "type": "deleted"
                            },
                            {
                                "content": "        self.assertEqual(context.values[\"annotation_var\"], (\"cfoo\", 101))",
                                "type": "deleted"
                            },
                            {
                                "content": "        self.assertEqual(context.values[\"cluster_var\"], (\"baz\", 101))",
                                "type": "added"
                            },
                            {
                                "content": "        self.assertEqual(context.values[\"annotation_var\"], (\"bax\", 101))",
                                "type": "added"
                            },
                            {
                                "content": "",
                                "type": "context"
                            },
                            {
                                "content": "        # We thought was had annotation, but the index is wrong due to",
                                "type": "added"
                            },
                            {
                                "content": "        # incorrect domain",
                                "type": "added"
                            },
                            {
                                "content": "        context = Context(",
                                "type": "context"
                            },
                            {
                                "content": "            values=dict(cluster_var_idx=(4, -2), annotation_var_idx=(5, -2)),",
                                "type": "deleted"
                            },
                            {
                                "content": "            values=dict(cluster_var_idx=(4, -2), annotation_var_idx=(7, -2)),",
                                "type": "added"
                            },
                            {
                                "content": "            **enc_domain",
                                "type": "context"
                            },
                            {
                                "content": "        )",
                                "type": "context"
                            },
                            {
                                "content": "        OWSilhouettePlot.migrate_context(context, 1)",
                                "type": "context"
                            },
                            {
                                "content": "        self.assertEqual(context.values[\"cluster_var\"], (\"mbaz\", 101))",
                                "type": "deleted"
                            },
                            {
                                "content": "        self.assertEqual(context.values[\"annotation_var\"], (\"mbar\", 103))",
                                "type": "deleted"
                            },
                            {
                                "content": "        self.assertEqual(context.values[\"cluster_var\"], (\"cfoo\", 101))",
                                "type": "added"
                            },
                            {
                                "content": "        self.assertNotIn(\"annotation_var_idx\", values)",
                                "type": "added"
                            },
                            {
                                "content": "",
                                "type": "context"
                            },
                            {
                                "content": "",
                                "type": "context"
                            },
                            {
                                "content": "if __name__ == \"__main__\":",
                                "type": "context"
                            },
                            {
                                "content": "",
                                "type": "context"
                            }
                        ]
                    },
                    "modified_entities": [
                        {
                            "file_name": "Orange/widgets/visualize/tests/test_owsilhouetteplot.py",
                            "type": "method",
                            "name": "TestOWSilhouettePlot.test_migration",
                            "content": "class TestOWSilhouettePlot(WidgetTest, 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        cls.scorename = \"Silhouette ({})\".format(cls.data.domain.class_var.name)\n\n    def setUp(self):\n        self.widget = self.create_widget(OWSilhouettePlot,\n                                         stored_settings={\"auto_commit\": True})\n        self.widget = self.widget  # type: OWSilhouettePlot\n\n    def test_no_data(self):\n        \"\"\"Check that the widget doesn't crash on empty data\"\"\"\n        self.send_signal(self.widget.Inputs.data, self.data[:0])\n\n    def test_outputs_add_scores(self):\n        # check output when appending scores\n        self.send_signal(self.widget.Inputs.data, self.data)\n        selected_indices = self._select_data()\n        selected = self.get_output(self.widget.Outputs.selected_data)\n        annotated = self.get_output(self.widget.Outputs.annotated_data)\n        self.assertEqual(self.scorename, selected.domain.metas[0].name)\n        self.assertEqual(self.scorename, annotated.domain.metas[0].name)\n        np.testing.assert_array_equal(selected.X, self.data.X[selected_indices])\n\n    def _select_data(self):\n        random.seed(42)\n        points = random.sample(range(0, len(self.data)), 20)\n        self.widget._silplot.setSelection(points)\n        return sorted(points)\n\n    def test_insufficient_clusters(self):\n        iris = self.data\n        data_one_cluster = iris[:3]  # three instances Iris-setosa only\n        self.send_signal(self.widget.Inputs.data, data_one_cluster)\n        self.assertTrue(self.widget.Error.need_two_clusters.is_shown())\n\n        data_singletons = iris[[0, 50, 100]]\n        assert len(np.unique(data_singletons.Y)) == 3  # 3 instances 3 labels\n        self.send_signal(self.widget.Inputs.data, data_singletons)\n        self.assertTrue(self.widget.Error.singleton_clusters_all.is_shown())\n\n    def test_unknowns_in_labels(self):\n        data = self.data[[0, 1, 2, 50, 51, 52, 100, 101, 102]]\n        with data.unlocked(data.Y):\n            data.Y[::3] = np.nan\n        valid = ~np.isnan(data.Y.flatten())\n        self.send_signal(self.widget.Inputs.data, data)\n        output = self.get_output(ANNOTATED_DATA_SIGNAL_NAME)\n        scores = output[:, self.scorename].metas.flatten()\n        self.assertTrue(np.all(np.isnan(scores[::3])))\n        self.assertTrue(np.all(np.isfinite(scores[valid])))\n\n        # Run again on subset with known labels\n        data_1 = data[np.flatnonzero(valid)]\n        self.send_signal(self.widget.Inputs.data, data_1)\n        output_1 = self.get_output(ANNOTATED_DATA_SIGNAL_NAME)\n        scores_1 = output_1[:, self.scorename].metas.flatten()\n        self.assertTrue(np.all(np.isfinite(scores_1)))\n        # the scores must match\n        np.testing.assert_almost_equal(scores_1, scores[valid], decimal=12)\n\n    def test_nan_distances(self):\n        self.widget.distance_idx = 2\n        self.assertEqual(self.widget.Distances[self.widget.distance_idx][0],\n                         'Cosine')\n        data = self.data[[0, 1, 2, 50, 51, 52, 100, 101, 102]]\n        with data.unlocked(data.X):\n            data.X[::3] = 0\n        valid = np.any(data.X != 0, axis=1)\n        self.assertFalse(self.widget.Warning.nan_distances.is_shown())\n        self.send_signal(self.widget.Inputs.data, data)\n        self.assertTrue(np.isnan(self.widget._matrix).any())\n        self.assertTrue(self.widget.Warning.nan_distances.is_shown())\n        output = self.get_output(ANNOTATED_DATA_SIGNAL_NAME)\n        scores = output[:, self.scorename].metas.flatten()\n        self.assertTrue(np.all(np.isnan(scores[::3])))\n        self.assertTrue(np.all(np.isfinite(scores[valid])))\n\n    def test_ignore_categorical(self):\n        data = Table('heart_disease')\n        self.widget.distance_idx = 2\n        self.assertEqual(self.widget.Distances[self.widget.distance_idx][0],\n                         'Cosine')\n        self.assertFalse(self.widget.Warning.ignoring_categorical.is_shown())\n        self.send_signal(self.widget.Inputs.data, data)\n        self.assertTrue(self.widget.Warning.ignoring_categorical.is_shown())\n        output = self.get_output(ANNOTATED_DATA_SIGNAL_NAME)\n        self.assertEqual(len(output.domain.variables), len(data.domain.variables))\n        self.widget.distance_idx = 0\n        self.widget._update()\n        self.assertFalse(self.widget.Warning.ignoring_categorical.is_shown())\n\n    def test_meta_object_dtype(self):\n        # gh-1875: Test on mixed string/discrete metas\n        data = self.data[::5]\n        domain = Domain(data.domain.attributes,\n                        [],\n                        [data.domain[\"iris\"], StringVariable(\"S\")])\n        data = data.from_table(domain, data)\n        self.send_signal(self.widget.Inputs.data, data)\n\n    def test_memory_error(self):\n        \"\"\"\n        Handling memory error.\n        GH-2336\n        Handling value error as well. This value error is in a relation with memory error.\n        GH-2521\n        \"\"\"\n        for i, side_effect in enumerate([MemoryError, ValueError]):\n            data = Table(\"iris\")[::3]\n            self.send_signal(self.widget.Inputs.data, data)\n            self.assertFalse(self.widget.Error.memory_error.is_shown())\n            self.assertFalse(self.widget.Error.value_error.is_shown())\n            with unittest.mock.patch(\"numpy.asarray\", side_effect=side_effect):\n                self.widget._matrix = None\n                self.widget.data = data\n                self.widget._effective_data = data\n                self.widget._update()\n                self.assertTrue(self.widget.Error.memory_error.is_shown() != i)\n                self.assertTrue(self.widget.Error.value_error.is_shown() == i)\n\n    def test_bad_data_range(self):\n        \"\"\"\n        Silhouette Plot now sets axis range properly.\n        GH-2377\n        \"\"\"\n        nan = np.NaN\n        table = Table.from_list(\n            Domain(\n                [ContinuousVariable(\"a\"), ContinuousVariable(\"b\"), ContinuousVariable(\"c\")],\n                [DiscreteVariable(\"d\", values=(\"y\", \"n\"))]),\n            list(zip([4, nan, nan],\n                     [15, nan, nan],\n                     [16, nan, nan],\n                     \"nyy\"))\n        )\n        self.send_signal(self.widget.Inputs.data, table)\n\n    def test_saved_selection(self):\n        self.widget.settingsHandler.pack_data(self.widget)\n        # no assert here, just test is doesn't crash on empty\n\n        iris = Table(\"iris\")\n\n        self.send_signal(self.widget.Inputs.data, iris)\n        random.seed(42)\n        points = random.sample(range(0, len(self.data)), 20)\n        self.widget._silplot.setSelection(points)\n        settings = self.widget.settingsHandler.pack_data(self.widget)\n\n        w = self.create_widget(OWSilhouettePlot, stored_settings=settings)\n        self.send_signal(w.Inputs.data, iris, widget=w)\n        self.assertEqual(len(self.get_output(w.Outputs.selected_data)), 20)\n\n    def test_distance_input(self):\n        widget = self.widget\n        data = Table(\"heart_disease\")[::4]\n        matrix = Orange.distance.Euclidean(data)\n        self.send_signal(widget.Inputs.data, matrix, widget=widget)\n        self.assertIsNotNone(widget.distances)\n        self.assertIsNotNone(widget.data)\n        self.assertFalse(widget._distances_gui_box.isEnabled())\n\n        self.send_signal(widget.Inputs.data, data, widget=widget)\n        self.assertIsNone(widget.distances)\n        self.assertIsNotNone(widget.data)\n        self.assertTrue(widget._distances_gui_box.isEnabled())\n\n    def test_input_distance_no_data(self):\n        widget = self.widget\n        matrix = DistMatrix(\n            np.array([[0, 1, 1], [1, 0, 1], [1, 1, 0]]),\n            row_items=None\n        )\n        self.send_signal(widget.Inputs.data, matrix, widget=widget)\n        self.assertTrue(widget.Error.input_validation_error.is_shown())\n        self.assertIsNone(widget.data)\n        self.assertIsNone(widget.distances)\n        self.send_signal(widget.Inputs.data, None, widget=widget)\n        self.assertFalse(widget.Error.input_validation_error.is_shown())\n\n    def test_no_group_var(self):\n        widget = self.widget\n        data = Table(\"iris\")[::4]\n        data = data[:, data.domain.attributes]\n        matrix = Orange.distance.Euclidean(data)\n        self.send_signal(widget.Inputs.data, matrix, widget=widget)\n\n        self.assertTrue(widget.Error.input_validation_error.is_shown())\n        self.assertIsNone(widget.data)\n        self.assertIsNone(widget.distances)\n\n        self.send_signal(widget.Inputs.data, None, widget=widget)\n        self.assertFalse(widget.Error.input_validation_error.is_shown())\n\n    def test_unique_output_domain(self):\n        widget = self.widget\n        data = possible_duplicate_table('Silhouette (iris)')\n        matrix = Orange.distance.Euclidean(data)\n        self.send_signal(widget.Inputs.data, matrix, widget=widget)\n\n        output = self.get_output(self.widget.Outputs.annotated_data)\n        self.assertEqual(output.domain.metas[0].name, 'Silhouette (iris) (1)')\n\n    def test_report(self):\n        widget = self.widget\n        widget.report_plot = Mock()\n        widget.report_caption = Mock()\n\n        widget.send_report()\n        widget.report_plot.assert_not_called()\n        widget.report_caption.assert_not_called()\n\n        data = Table(\"zoo\")\n        self.send_signal(widget.Inputs.data, data)\n\n        widget.annotation_var = None\n        widget.send_report()\n        widget.report_plot.assert_called()\n        widget.report_caption.assert_called()\n        text = widget.report_caption.call_args[0][0]\n        self.assertIn(data.domain.class_var.name, text)\n        self.assertNotIn(\"nnotated\", text)\n\n        widget.annotation_var = data.domain.metas[0]\n        widget._silplot.rowNamesVisible = lambda: True\n        widget.send_report()\n        text = widget.report_caption.call_args[0][0]\n        self.assertIn(data.domain.class_var.name, text)\n        self.assertIn(\"nnotated\", text)\n        self.assertIn(data.domain.metas[0].name, text)\n\n    def test_migration(self):\n        enc_domain = dict(\n            attributes=(('foo', 1), ('bar', 1), ('baz', 2), ('bax', 1)),\n            class_vars=(('cfoo', 1), ),\n            metas=(('mbar', 3), ('mbaz', 1)))\n\n        context = Context(\n            values=dict(cluster_var_idx=(0, -2), annotation_var_idx=(0, -2)),\n            **enc_domain\n        )\n        OWSilhouettePlot.migrate_context(context, 1)\n        values = context.values\n        self.assertNotIn(\"cluster_var_idx\", values)\n        self.assertNotIn(\"annotation_var_idx\", values)\n        self.assertEqual(values[\"cluster_var\"], (\"foo\", 101))\n        self.assertEqual(values[\"annotation_var\"], None)\n\n        context = Context(\n            values=dict(cluster_var_idx=(4, -2), annotation_var_idx=(4, -2)),\n            **enc_domain\n        )\n        OWSilhouettePlot.migrate_context(context, 1)\n        self.assertNotIn(\"cluster_var_idx\", values)\n        self.assertNotIn(\"annotation_var_idx\", values)\n        self.assertEqual(context.values[\"cluster_var\"], (\"mbaz\", 101))\n        self.assertEqual(context.values[\"annotation_var\"], (\"cfoo\", 101))\n\n        context = Context(\n            values=dict(cluster_var_idx=(4, -2), annotation_var_idx=(5, -2)),\n            **enc_domain\n        )\n        OWSilhouettePlot.migrate_context(context, 1)\n        self.assertEqual(context.values[\"cluster_var\"], (\"mbaz\", 101))\n        self.assertEqual(context.values[\"annotation_var\"], (\"mbar\", 103))",
                            "ast_type_str": "ClassDef",
                            "start_lineno": 260,
                            "end_lineno": 293,
                            "parent": {
                                "file_name": "Orange/widgets/visualize/tests/test_owsilhouetteplot.py",
                                "type": "class",
                                "name": "TestOWSilhouettePlot",
                                "content": "class TestOWSilhouettePlot(WidgetTest, 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        cls.scorename = \"Silhouette ({})\".format(cls.data.domain.class_var.name)\n\n    def setUp(self):\n        self.widget = self.create_widget(OWSilhouettePlot,\n                                         stored_settings={\"auto_commit\": True})\n        self.widget = self.widget  # type: OWSilhouettePlot\n\n    def test_no_data(self):\n        \"\"\"Check that the widget doesn't crash on empty data\"\"\"\n        self.send_signal(self.widget.Inputs.data, self.data[:0])\n\n    def test_outputs_add_scores(self):\n        # check output when appending scores\n        self.send_signal(self.widget.Inputs.data, self.data)\n        selected_indices = self._select_data()\n        selected = self.get_output(self.widget.Outputs.selected_data)\n        annotated = self.get_output(self.widget.Outputs.annotated_data)\n        self.assertEqual(self.scorename, selected.domain.metas[0].name)\n        self.assertEqual(self.scorename, annotated.domain.metas[0].name)\n        np.testing.assert_array_equal(selected.X, self.data.X[selected_indices])\n\n    def _select_data(self):\n        random.seed(42)\n        points = random.sample(range(0, len(self.data)), 20)\n        self.widget._silplot.setSelection(points)\n        return sorted(points)\n\n    def test_insufficient_clusters(self):\n        iris = self.data\n        data_one_cluster = iris[:3]  # three instances Iris-setosa only\n        self.send_signal(self.widget.Inputs.data, data_one_cluster)\n        self.assertTrue(self.widget.Error.need_two_clusters.is_shown())\n\n        data_singletons = iris[[0, 50, 100]]\n        assert len(np.unique(data_singletons.Y)) == 3  # 3 instances 3 labels\n        self.send_signal(self.widget.Inputs.data, data_singletons)\n        self.assertTrue(self.widget.Error.singleton_clusters_all.is_shown())\n\n    def test_unknowns_in_labels(self):\n        data = self.data[[0, 1, 2, 50, 51, 52, 100, 101, 102]]\n        with data.unlocked(data.Y):\n            data.Y[::3] = np.nan\n        valid = ~np.isnan(data.Y.flatten())\n        self.send_signal(self.widget.Inputs.data, data)\n        output = self.get_output(ANNOTATED_DATA_SIGNAL_NAME)\n        scores = output[:, self.scorename].metas.flatten()\n        self.assertTrue(np.all(np.isnan(scores[::3])))\n        self.assertTrue(np.all(np.isfinite(scores[valid])))\n\n        # Run again on subset with known labels\n        data_1 = data[np.flatnonzero(valid)]\n        self.send_signal(self.widget.Inputs.data, data_1)\n        output_1 = self.get_output(ANNOTATED_DATA_SIGNAL_NAME)\n        scores_1 = output_1[:, self.scorename].metas.flatten()\n        self.assertTrue(np.all(np.isfinite(scores_1)))\n        # the scores must match\n        np.testing.assert_almost_equal(scores_1, scores[valid], decimal=12)\n\n    def test_nan_distances(self):\n        self.widget.distance_idx = 2\n        self.assertEqual(self.widget.Distances[self.widget.distance_idx][0],\n                         'Cosine')\n        data = self.data[[0, 1, 2, 50, 51, 52, 100, 101, 102]]\n        with data.unlocked(data.X):\n            data.X[::3] = 0\n        valid = np.any(data.X != 0, axis=1)\n        self.assertFalse(self.widget.Warning.nan_distances.is_shown())\n        self.send_signal(self.widget.Inputs.data, data)\n        self.assertTrue(np.isnan(self.widget._matrix).any())\n        self.assertTrue(self.widget.Warning.nan_distances.is_shown())\n        output = self.get_output(ANNOTATED_DATA_SIGNAL_NAME)\n        scores = output[:, self.scorename].metas.flatten()\n        self.assertTrue(np.all(np.isnan(scores[::3])))\n        self.assertTrue(np.all(np.isfinite(scores[valid])))\n\n    def test_ignore_categorical(self):\n        data = Table('heart_disease')\n        self.widget.distance_idx = 2\n        self.assertEqual(self.widget.Distances[self.widget.distance_idx][0],\n                         'Cosine')\n        self.assertFalse(self.widget.Warning.ignoring_categorical.is_shown())\n        self.send_signal(self.widget.Inputs.data, data)\n        self.assertTrue(self.widget.Warning.ignoring_categorical.is_shown())\n        output = self.get_output(ANNOTATED_DATA_SIGNAL_NAME)\n        self.assertEqual(len(output.domain.variables), len(data.domain.variables))\n        self.widget.distance_idx = 0\n        self.widget._update()\n        self.assertFalse(self.widget.Warning.ignoring_categorical.is_shown())\n\n    def test_meta_object_dtype(self):\n        # gh-1875: Test on mixed string/discrete metas\n        data = self.data[::5]\n        domain = Domain(data.domain.attributes,\n                        [],\n                        [data.domain[\"iris\"], StringVariable(\"S\")])\n        data = data.from_table(domain, data)\n        self.send_signal(self.widget.Inputs.data, data)\n\n    def test_memory_error(self):\n        \"\"\"\n        Handling memory error.\n        GH-2336\n        Handling value error as well. This value error is in a relation with memory error.\n        GH-2521\n        \"\"\"\n        for i, side_effect in enumerate([MemoryError, ValueError]):\n            data = Table(\"iris\")[::3]\n            self.send_signal(self.widget.Inputs.data, data)\n            self.assertFalse(self.widget.Error.memory_error.is_shown())\n            self.assertFalse(self.widget.Error.value_error.is_shown())\n            with unittest.mock.patch(\"numpy.asarray\", side_effect=side_effect):\n                self.widget._matrix = None\n                self.widget.data = data\n                self.widget._effective_data = data\n                self.widget._update()\n                self.assertTrue(self.widget.Error.memory_error.is_shown() != i)\n                self.assertTrue(self.widget.Error.value_error.is_shown() == i)\n\n    def test_bad_data_range(self):\n        \"\"\"\n        Silhouette Plot now sets axis range properly.\n        GH-2377\n        \"\"\"\n        nan = np.NaN\n        table = Table.from_list(\n            Domain(\n                [ContinuousVariable(\"a\"), ContinuousVariable(\"b\"), ContinuousVariable(\"c\")],\n                [DiscreteVariable(\"d\", values=(\"y\", \"n\"))]),\n            list(zip([4, nan, nan],\n                     [15, nan, nan],\n                     [16, nan, nan],\n                     \"nyy\"))\n        )\n        self.send_signal(self.widget.Inputs.data, table)\n\n    def test_saved_selection(self):\n        self.widget.settingsHandler.pack_data(self.widget)\n        # no assert here, just test is doesn't crash on empty\n\n        iris = Table(\"iris\")\n\n        self.send_signal(self.widget.Inputs.data, iris)\n        random.seed(42)\n        points = random.sample(range(0, len(self.data)), 20)\n        self.widget._silplot.setSelection(points)\n        settings = self.widget.settingsHandler.pack_data(self.widget)\n\n        w = self.create_widget(OWSilhouettePlot, stored_settings=settings)\n        self.send_signal(w.Inputs.data, iris, widget=w)\n        self.assertEqual(len(self.get_output(w.Outputs.selected_data)), 20)\n\n    def test_distance_input(self):\n        widget = self.widget\n        data = Table(\"heart_disease\")[::4]\n        matrix = Orange.distance.Euclidean(data)\n        self.send_signal(widget.Inputs.data, matrix, widget=widget)\n        self.assertIsNotNone(widget.distances)\n        self.assertIsNotNone(widget.data)\n        self.assertFalse(widget._distances_gui_box.isEnabled())\n\n        self.send_signal(widget.Inputs.data, data, widget=widget)\n        self.assertIsNone(widget.distances)\n        self.assertIsNotNone(widget.data)\n        self.assertTrue(widget._distances_gui_box.isEnabled())\n\n    def test_input_distance_no_data(self):\n        widget = self.widget\n        matrix = DistMatrix(\n            np.array([[0, 1, 1], [1, 0, 1], [1, 1, 0]]),\n            row_items=None\n        )\n        self.send_signal(widget.Inputs.data, matrix, widget=widget)\n        self.assertTrue(widget.Error.input_validation_error.is_shown())\n        self.assertIsNone(widget.data)\n        self.assertIsNone(widget.distances)\n        self.send_signal(widget.Inputs.data, None, widget=widget)\n        self.assertFalse(widget.Error.input_validation_error.is_shown())\n\n    def test_no_group_var(self):\n        widget = self.widget\n        data = Table(\"iris\")[::4]\n        data = data[:, data.domain.attributes]\n        matrix = Orange.distance.Euclidean(data)\n        self.send_signal(widget.Inputs.data, matrix, widget=widget)\n\n        self.assertTrue(widget.Error.input_validation_error.is_shown())\n        self.assertIsNone(widget.data)\n        self.assertIsNone(widget.distances)\n\n        self.send_signal(widget.Inputs.data, None, widget=widget)\n        self.assertFalse(widget.Error.input_validation_error.is_shown())\n\n    def test_unique_output_domain(self):\n        widget = self.widget\n        data = possible_duplicate_table('Silhouette (iris)')\n        matrix = Orange.distance.Euclidean(data)\n        self.send_signal(widget.Inputs.data, matrix, widget=widget)\n\n        output = self.get_output(self.widget.Outputs.annotated_data)\n        self.assertEqual(output.domain.metas[0].name, 'Silhouette (iris) (1)')\n\n    def test_report(self):\n        widget = self.widget\n        widget.report_plot = Mock()\n        widget.report_caption = Mock()\n\n        widget.send_report()\n        widget.report_plot.assert_not_called()\n        widget.report_caption.assert_not_called()\n\n        data = Table(\"zoo\")\n        self.send_signal(widget.Inputs.data, data)\n\n        widget.annotation_var = None\n        widget.send_report()\n        widget.report_plot.assert_called()\n        widget.report_caption.assert_called()\n        text = widget.report_caption.call_args[0][0]\n        self.assertIn(data.domain.class_var.name, text)\n        self.assertNotIn(\"nnotated\", text)\n\n        widget.annotation_var = data.domain.metas[0]\n        widget._silplot.rowNamesVisible = lambda: True\n        widget.send_report()\n        text = widget.report_caption.call_args[0][0]\n        self.assertIn(data.domain.class_var.name, text)\n        self.assertIn(\"nnotated\", text)\n        self.assertIn(data.domain.metas[0].name, text)\n\n    def test_migration(self):\n        enc_domain = dict(\n            attributes=(('foo', 1), ('bar', 1), ('baz', 2), ('bax', 1)),\n            class_vars=(('cfoo', 1), ),\n            metas=(('mbar', 3), ('mbaz', 1)))\n\n        context = Context(\n            values=dict(cluster_var_idx=(0, -2), annotation_var_idx=(0, -2)),\n            **enc_domain\n        )\n        OWSilhouettePlot.migrate_context(context, 1)\n        values = context.values\n        self.assertNotIn(\"cluster_var_idx\", values)\n        self.assertNotIn(\"annotation_var_idx\", values)\n        self.assertEqual(values[\"cluster_var\"], (\"foo\", 101))\n        self.assertEqual(values[\"annotation_var\"], None)\n\n        context = Context(\n            values=dict(cluster_var_idx=(4, -2), annotation_var_idx=(4, -2)),\n            **enc_domain\n        )\n        OWSilhouettePlot.migrate_context(context, 1)\n        self.assertNotIn(\"cluster_var_idx\", values)\n        self.assertNotIn(\"annotation_var_idx\", values)\n        self.assertEqual(context.values[\"cluster_var\"], (\"mbaz\", 101))\n        self.assertEqual(context.values[\"annotation_var\"], (\"cfoo\", 101))\n\n        context = Context(\n            values=dict(cluster_var_idx=(4, -2), annotation_var_idx=(5, -2)),\n            **enc_domain\n        )\n        OWSilhouettePlot.migrate_context(context, 1)\n        self.assertEqual(context.values[\"cluster_var\"], (\"mbaz\", 101))\n        self.assertEqual(context.values[\"annotation_var\"], (\"mbar\", 103))",
                                "ast_type_str": "ClassDef",
                                "start_lineno": 22,
                                "end_lineno": 293,
                                "parent": null
                            }
                        },
                        {
                            "file_name": "Orange/widgets/visualize/tests/test_owsilhouetteplot.py",
                            "type": "class",
                            "name": "TestOWSilhouettePlot",
                            "content": "class TestOWSilhouettePlot(WidgetTest, 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        cls.scorename = \"Silhouette ({})\".format(cls.data.domain.class_var.name)\n\n    def setUp(self):\n        self.widget = self.create_widget(OWSilhouettePlot,\n                                         stored_settings={\"auto_commit\": True})\n        self.widget = self.widget  # type: OWSilhouettePlot\n\n    def test_no_data(self):\n        \"\"\"Check that the widget doesn't crash on empty data\"\"\"\n        self.send_signal(self.widget.Inputs.data, self.data[:0])\n\n    def test_outputs_add_scores(self):\n        # check output when appending scores\n        self.send_signal(self.widget.Inputs.data, self.data)\n        selected_indices = self._select_data()\n        selected = self.get_output(self.widget.Outputs.selected_data)\n        annotated = self.get_output(self.widget.Outputs.annotated_data)\n        self.assertEqual(self.scorename, selected.domain.metas[0].name)\n        self.assertEqual(self.scorename, annotated.domain.metas[0].name)\n        np.testing.assert_array_equal(selected.X, self.data.X[selected_indices])\n\n    def _select_data(self):\n        random.seed(42)\n        points = random.sample(range(0, len(self.data)), 20)\n        self.widget._silplot.setSelection(points)\n        return sorted(points)\n\n    def test_insufficient_clusters(self):\n        iris = self.data\n        data_one_cluster = iris[:3]  # three instances Iris-setosa only\n        self.send_signal(self.widget.Inputs.data, data_one_cluster)\n        self.assertTrue(self.widget.Error.need_two_clusters.is_shown())\n\n        data_singletons = iris[[0, 50, 100]]\n        assert len(np.unique(data_singletons.Y)) == 3  # 3 instances 3 labels\n        self.send_signal(self.widget.Inputs.data, data_singletons)\n        self.assertTrue(self.widget.Error.singleton_clusters_all.is_shown())\n\n    def test_unknowns_in_labels(self):\n        data = self.data[[0, 1, 2, 50, 51, 52, 100, 101, 102]]\n        with data.unlocked(data.Y):\n            data.Y[::3] = np.nan\n        valid = ~np.isnan(data.Y.flatten())\n        self.send_signal(self.widget.Inputs.data, data)\n        output = self.get_output(ANNOTATED_DATA_SIGNAL_NAME)\n        scores = output[:, self.scorename].metas.flatten()\n        self.assertTrue(np.all(np.isnan(scores[::3])))\n        self.assertTrue(np.all(np.isfinite(scores[valid])))\n\n        # Run again on subset with known labels\n        data_1 = data[np.flatnonzero(valid)]\n        self.send_signal(self.widget.Inputs.data, data_1)\n        output_1 = self.get_output(ANNOTATED_DATA_SIGNAL_NAME)\n        scores_1 = output_1[:, self.scorename].metas.flatten()\n        self.assertTrue(np.all(np.isfinite(scores_1)))\n        # the scores must match\n        np.testing.assert_almost_equal(scores_1, scores[valid], decimal=12)\n\n    def test_nan_distances(self):\n        self.widget.distance_idx = 2\n        self.assertEqual(self.widget.Distances[self.widget.distance_idx][0],\n                         'Cosine')\n        data = self.data[[0, 1, 2, 50, 51, 52, 100, 101, 102]]\n        with data.unlocked(data.X):\n            data.X[::3] = 0\n        valid = np.any(data.X != 0, axis=1)\n        self.assertFalse(self.widget.Warning.nan_distances.is_shown())\n        self.send_signal(self.widget.Inputs.data, data)\n        self.assertTrue(np.isnan(self.widget._matrix).any())\n        self.assertTrue(self.widget.Warning.nan_distances.is_shown())\n        output = self.get_output(ANNOTATED_DATA_SIGNAL_NAME)\n        scores = output[:, self.scorename].metas.flatten()\n        self.assertTrue(np.all(np.isnan(scores[::3])))\n        self.assertTrue(np.all(np.isfinite(scores[valid])))\n\n    def test_ignore_categorical(self):\n        data = Table('heart_disease')\n        self.widget.distance_idx = 2\n        self.assertEqual(self.widget.Distances[self.widget.distance_idx][0],\n                         'Cosine')\n        self.assertFalse(self.widget.Warning.ignoring_categorical.is_shown())\n        self.send_signal(self.widget.Inputs.data, data)\n        self.assertTrue(self.widget.Warning.ignoring_categorical.is_shown())\n        output = self.get_output(ANNOTATED_DATA_SIGNAL_NAME)\n        self.assertEqual(len(output.domain.variables), len(data.domain.variables))\n        self.widget.distance_idx = 0\n        self.widget._update()\n        self.assertFalse(self.widget.Warning.ignoring_categorical.is_shown())\n\n    def test_meta_object_dtype(self):\n        # gh-1875: Test on mixed string/discrete metas\n        data = self.data[::5]\n        domain = Domain(data.domain.attributes,\n                        [],\n                        [data.domain[\"iris\"], StringVariable(\"S\")])\n        data = data.from_table(domain, data)\n        self.send_signal(self.widget.Inputs.data, data)\n\n    def test_memory_error(self):\n        \"\"\"\n        Handling memory error.\n        GH-2336\n        Handling value error as well. This value error is in a relation with memory error.\n        GH-2521\n        \"\"\"\n        for i, side_effect in enumerate([MemoryError, ValueError]):\n            data = Table(\"iris\")[::3]\n            self.send_signal(self.widget.Inputs.data, data)\n            self.assertFalse(self.widget.Error.memory_error.is_shown())\n            self.assertFalse(self.widget.Error.value_error.is_shown())\n            with unittest.mock.patch(\"numpy.asarray\", side_effect=side_effect):\n                self.widget._matrix = None\n                self.widget.data = data\n                self.widget._effective_data = data\n                self.widget._update()\n                self.assertTrue(self.widget.Error.memory_error.is_shown() != i)\n                self.assertTrue(self.widget.Error.value_error.is_shown() == i)\n\n    def test_bad_data_range(self):\n        \"\"\"\n        Silhouette Plot now sets axis range properly.\n        GH-2377\n        \"\"\"\n        nan = np.NaN\n        table = Table.from_list(\n            Domain(\n                [ContinuousVariable(\"a\"), ContinuousVariable(\"b\"), ContinuousVariable(\"c\")],\n                [DiscreteVariable(\"d\", values=(\"y\", \"n\"))]),\n            list(zip([4, nan, nan],\n                     [15, nan, nan],\n                     [16, nan, nan],\n                     \"nyy\"))\n        )\n        self.send_signal(self.widget.Inputs.data, table)\n\n    def test_saved_selection(self):\n        self.widget.settingsHandler.pack_data(self.widget)\n        # no assert here, just test is doesn't crash on empty\n\n        iris = Table(\"iris\")\n\n        self.send_signal(self.widget.Inputs.data, iris)\n        random.seed(42)\n        points = random.sample(range(0, len(self.data)), 20)\n        self.widget._silplot.setSelection(points)\n        settings = self.widget.settingsHandler.pack_data(self.widget)\n\n        w = self.create_widget(OWSilhouettePlot, stored_settings=settings)\n        self.send_signal(w.Inputs.data, iris, widget=w)\n        self.assertEqual(len(self.get_output(w.Outputs.selected_data)), 20)\n\n    def test_distance_input(self):\n        widget = self.widget\n        data = Table(\"heart_disease\")[::4]\n        matrix = Orange.distance.Euclidean(data)\n        self.send_signal(widget.Inputs.data, matrix, widget=widget)\n        self.assertIsNotNone(widget.distances)\n        self.assertIsNotNone(widget.data)\n        self.assertFalse(widget._distances_gui_box.isEnabled())\n\n        self.send_signal(widget.Inputs.data, data, widget=widget)\n        self.assertIsNone(widget.distances)\n        self.assertIsNotNone(widget.data)\n        self.assertTrue(widget._distances_gui_box.isEnabled())\n\n    def test_input_distance_no_data(self):\n        widget = self.widget\n        matrix = DistMatrix(\n            np.array([[0, 1, 1], [1, 0, 1], [1, 1, 0]]),\n            row_items=None\n        )\n        self.send_signal(widget.Inputs.data, matrix, widget=widget)\n        self.assertTrue(widget.Error.input_validation_error.is_shown())\n        self.assertIsNone(widget.data)\n        self.assertIsNone(widget.distances)\n        self.send_signal(widget.Inputs.data, None, widget=widget)\n        self.assertFalse(widget.Error.input_validation_error.is_shown())\n\n    def test_no_group_var(self):\n        widget = self.widget\n        data = Table(\"iris\")[::4]\n        data = data[:, data.domain.attributes]\n        matrix = Orange.distance.Euclidean(data)\n        self.send_signal(widget.Inputs.data, matrix, widget=widget)\n\n        self.assertTrue(widget.Error.input_validation_error.is_shown())\n        self.assertIsNone(widget.data)\n        self.assertIsNone(widget.distances)\n\n        self.send_signal(widget.Inputs.data, None, widget=widget)\n        self.assertFalse(widget.Error.input_validation_error.is_shown())\n\n    def test_unique_output_domain(self):\n        widget = self.widget\n        data = possible_duplicate_table('Silhouette (iris)')\n        matrix = Orange.distance.Euclidean(data)\n        self.send_signal(widget.Inputs.data, matrix, widget=widget)\n\n        output = self.get_output(self.widget.Outputs.annotated_data)\n        self.assertEqual(output.domain.metas[0].name, 'Silhouette (iris) (1)')\n\n    def test_report(self):\n        widget = self.widget\n        widget.report_plot = Mock()\n        widget.report_caption = Mock()\n\n        widget.send_report()\n        widget.report_plot.assert_not_called()\n        widget.report_caption.assert_not_called()\n\n        data = Table(\"zoo\")\n        self.send_signal(widget.Inputs.data, data)\n\n        widget.annotation_var = None\n        widget.send_report()\n        widget.report_plot.assert_called()\n        widget.report_caption.assert_called()\n        text = widget.report_caption.call_args[0][0]\n        self.assertIn(data.domain.class_var.name, text)\n        self.assertNotIn(\"nnotated\", text)\n\n        widget.annotation_var = data.domain.metas[0]\n        widget._silplot.rowNamesVisible = lambda: True\n        widget.send_report()\n        text = widget.report_caption.call_args[0][0]\n        self.assertIn(data.domain.class_var.name, text)\n        self.assertIn(\"nnotated\", text)\n        self.assertIn(data.domain.metas[0].name, text)\n\n    def test_migration(self):\n        enc_domain = dict(\n            attributes=(('foo', 1), ('bar', 1), ('baz', 2), ('bax', 1)),\n            class_vars=(('cfoo', 1), ),\n            metas=(('mbar', 3), ('mbaz', 1)))\n\n        context = Context(\n            values=dict(cluster_var_idx=(0, -2), annotation_var_idx=(0, -2)),\n            **enc_domain\n        )\n        OWSilhouettePlot.migrate_context(context, 1)\n        values = context.values\n        self.assertNotIn(\"cluster_var_idx\", values)\n        self.assertNotIn(\"annotation_var_idx\", values)\n        self.assertEqual(values[\"cluster_var\"], (\"foo\", 101))\n        self.assertEqual(values[\"annotation_var\"], None)\n\n        context = Context(\n            values=dict(cluster_var_idx=(4, -2), annotation_var_idx=(4, -2)),\n            **enc_domain\n        )\n        OWSilhouettePlot.migrate_context(context, 1)\n        self.assertNotIn(\"cluster_var_idx\", values)\n        self.assertNotIn(\"annotation_var_idx\", values)\n        self.assertEqual(context.values[\"cluster_var\"], (\"mbaz\", 101))\n        self.assertEqual(context.values[\"annotation_var\"], (\"cfoo\", 101))\n\n        context = Context(\n            values=dict(cluster_var_idx=(4, -2), annotation_var_idx=(5, -2)),\n            **enc_domain\n        )\n        OWSilhouettePlot.migrate_context(context, 1)\n        self.assertEqual(context.values[\"cluster_var\"], (\"mbaz\", 101))\n        self.assertEqual(context.values[\"annotation_var\"], (\"mbar\", 103))",
                            "ast_type_str": "ClassDef",
                            "start_lineno": 22,
                            "end_lineno": 293,
                            "parent": null
                        }
                    ],
                    "added_entities": [],
                    "deleted_entities": []
                }
            ]
        }
    ],
    "old_commit_hash": "3617003b27d75572ebbb11a136b3c079829e9bb1^",
    "new_commit_hash": "3617003b27d75572ebbb11a136b3c079829e9bb1",
    "commit_message": "Silhouette: Fix context migration for broken contexts",
    "commit_date": "2022-05-13T15:12:30+02:00",
    "metadata": {}
}
{
    "repo_name": "orange3",
    "new_commit_hash": "3617003b27d75572ebbb11a136b3c079829e9bb1",
    "test_file_codes": [
        "# pylint: disable=protected-access\n# Test methods with long descriptive names can omit docstrings\n# pylint: disable=missing-docstring\nimport random\nimport unittest\nfrom unittest.mock import Mock\n\nimport numpy as np\n\nfrom orangewidget.settings import Context\n\nimport Orange.distance\nfrom Orange.data import (\n    Table, Domain, ContinuousVariable, DiscreteVariable, StringVariable)\nfrom Orange.misc import DistMatrix\nfrom Orange.widgets.utils.annotated_data import ANNOTATED_DATA_SIGNAL_NAME\nfrom Orange.widgets.visualize.owsilhouetteplot import OWSilhouettePlot\nfrom Orange.widgets.tests.base import WidgetTest, WidgetOutputsTestMixin\nfrom Orange.widgets.tests.utils import possible_duplicate_table\n\n\nclass TestOWSilhouettePlot(WidgetTest, 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        cls.scorename = \"Silhouette ({})\".format(cls.data.domain.class_var.name)\n\n    def setUp(self):\n        self.widget = self.create_widget(OWSilhouettePlot,\n                                         stored_settings={\"auto_commit\": True})\n        self.widget = self.widget  # type: OWSilhouettePlot\n\n    def test_no_data(self):\n        \"\"\"Check that the widget doesn't crash on empty data\"\"\"\n        self.send_signal(self.widget.Inputs.data, self.data[:0])\n\n    def test_outputs_add_scores(self):\n        # check output when appending scores\n        self.send_signal(self.widget.Inputs.data, self.data)\n        selected_indices = self._select_data()\n        selected = self.get_output(self.widget.Outputs.selected_data)\n        annotated = self.get_output(self.widget.Outputs.annotated_data)\n        self.assertEqual(self.scorename, selected.domain.metas[0].name)\n        self.assertEqual(self.scorename, annotated.domain.metas[0].name)\n        np.testing.assert_array_equal(selected.X, self.data.X[selected_indices])\n\n    def _select_data(self):\n        random.seed(42)\n        points = random.sample(range(0, len(self.data)), 20)\n        self.widget._silplot.setSelection(points)\n        return sorted(points)\n\n    def test_insufficient_clusters(self):\n        iris = self.data\n        data_one_cluster = iris[:3]  # three instances Iris-setosa only\n        self.send_signal(self.widget.Inputs.data, data_one_cluster)\n        self.assertTrue(self.widget.Error.need_two_clusters.is_shown())\n\n        data_singletons = iris[[0, 50, 100]]\n        assert len(np.unique(data_singletons.Y)) == 3  # 3 instances 3 labels\n        self.send_signal(self.widget.Inputs.data, data_singletons)\n        self.assertTrue(self.widget.Error.singleton_clusters_all.is_shown())\n\n    def test_unknowns_in_labels(self):\n        data = self.data[[0, 1, 2, 50, 51, 52, 100, 101, 102]]\n        with data.unlocked(data.Y):\n            data.Y[::3] = np.nan\n        valid = ~np.isnan(data.Y.flatten())\n        self.send_signal(self.widget.Inputs.data, data)\n        output = self.get_output(ANNOTATED_DATA_SIGNAL_NAME)\n        scores = output[:, self.scorename].metas.flatten()\n        self.assertTrue(np.all(np.isnan(scores[::3])))\n        self.assertTrue(np.all(np.isfinite(scores[valid])))\n\n        # Run again on subset with known labels\n        data_1 = data[np.flatnonzero(valid)]\n        self.send_signal(self.widget.Inputs.data, data_1)\n        output_1 = self.get_output(ANNOTATED_DATA_SIGNAL_NAME)\n        scores_1 = output_1[:, self.scorename].metas.flatten()\n        self.assertTrue(np.all(np.isfinite(scores_1)))\n        # the scores must match\n        np.testing.assert_almost_equal(scores_1, scores[valid], decimal=12)\n\n    def test_nan_distances(self):\n        self.widget.distance_idx = 2\n        self.assertEqual(self.widget.Distances[self.widget.distance_idx][0],\n                         'Cosine')\n        data = self.data[[0, 1, 2, 50, 51, 52, 100, 101, 102]]\n        with data.unlocked(data.X):\n            data.X[::3] = 0\n        valid = np.any(data.X != 0, axis=1)\n        self.assertFalse(self.widget.Warning.nan_distances.is_shown())\n        self.send_signal(self.widget.Inputs.data, data)\n        self.assertTrue(np.isnan(self.widget._matrix).any())\n        self.assertTrue(self.widget.Warning.nan_distances.is_shown())\n        output = self.get_output(ANNOTATED_DATA_SIGNAL_NAME)\n        scores = output[:, self.scorename].metas.flatten()\n        self.assertTrue(np.all(np.isnan(scores[::3])))\n        self.assertTrue(np.all(np.isfinite(scores[valid])))\n\n    def test_ignore_categorical(self):\n        data = Table('heart_disease')\n        self.widget.distance_idx = 2\n        self.assertEqual(self.widget.Distances[self.widget.distance_idx][0],\n                         'Cosine')\n        self.assertFalse(self.widget.Warning.ignoring_categorical.is_shown())\n        self.send_signal(self.widget.Inputs.data, data)\n        self.assertTrue(self.widget.Warning.ignoring_categorical.is_shown())\n        output = self.get_output(ANNOTATED_DATA_SIGNAL_NAME)\n        self.assertEqual(len(output.domain.variables), len(data.domain.variables))\n        self.widget.distance_idx = 0\n        self.widget._update()\n        self.assertFalse(self.widget.Warning.ignoring_categorical.is_shown())\n\n    def test_meta_object_dtype(self):\n        # gh-1875: Test on mixed string/discrete metas\n        data = self.data[::5]\n        domain = Domain(data.domain.attributes,\n                        [],\n                        [data.domain[\"iris\"], StringVariable(\"S\")])\n        data = data.from_table(domain, data)\n        self.send_signal(self.widget.Inputs.data, data)\n\n    def test_memory_error(self):\n        \"\"\"\n        Handling memory error.\n        GH-2336\n        Handling value error as well. This value error is in a relation with memory error.\n        GH-2521\n        \"\"\"\n        for i, side_effect in enumerate([MemoryError, ValueError]):\n            data = Table(\"iris\")[::3]\n            self.send_signal(self.widget.Inputs.data, data)\n            self.assertFalse(self.widget.Error.memory_error.is_shown())\n            self.assertFalse(self.widget.Error.value_error.is_shown())\n            with unittest.mock.patch(\"numpy.asarray\", side_effect=side_effect):\n                self.widget._matrix = None\n                self.widget.data = data\n                self.widget._effective_data = data\n                self.widget._update()\n                self.assertTrue(self.widget.Error.memory_error.is_shown() != i)\n                self.assertTrue(self.widget.Error.value_error.is_shown() == i)\n\n    def test_bad_data_range(self):\n        \"\"\"\n        Silhouette Plot now sets axis range properly.\n        GH-2377\n        \"\"\"\n        nan = np.NaN\n        table = Table.from_list(\n            Domain(\n                [ContinuousVariable(\"a\"), ContinuousVariable(\"b\"), ContinuousVariable(\"c\")],\n                [DiscreteVariable(\"d\", values=(\"y\", \"n\"))]),\n            list(zip([4, nan, nan],\n                     [15, nan, nan],\n                     [16, nan, nan],\n                     \"nyy\"))\n        )\n        self.send_signal(self.widget.Inputs.data, table)\n\n    def test_saved_selection(self):\n        self.widget.settingsHandler.pack_data(self.widget)\n        # no assert here, just test is doesn't crash on empty\n\n        iris = Table(\"iris\")\n\n        self.send_signal(self.widget.Inputs.data, iris)\n        random.seed(42)\n        points = random.sample(range(0, len(self.data)), 20)\n        self.widget._silplot.setSelection(points)\n        settings = self.widget.settingsHandler.pack_data(self.widget)\n\n        w = self.create_widget(OWSilhouettePlot, stored_settings=settings)\n        self.send_signal(w.Inputs.data, iris, widget=w)\n        self.assertEqual(len(self.get_output(w.Outputs.selected_data)), 20)\n\n    def test_distance_input(self):\n        widget = self.widget\n        data = Table(\"heart_disease\")[::4]\n        matrix = Orange.distance.Euclidean(data)\n        self.send_signal(widget.Inputs.data, matrix, widget=widget)\n        self.assertIsNotNone(widget.distances)\n        self.assertIsNotNone(widget.data)\n        self.assertFalse(widget._distances_gui_box.isEnabled())\n\n        self.send_signal(widget.Inputs.data, data, widget=widget)\n        self.assertIsNone(widget.distances)\n        self.assertIsNotNone(widget.data)\n        self.assertTrue(widget._distances_gui_box.isEnabled())\n\n    def test_input_distance_no_data(self):\n        widget = self.widget\n        matrix = DistMatrix(\n            np.array([[0, 1, 1], [1, 0, 1], [1, 1, 0]]),\n            row_items=None\n        )\n        self.send_signal(widget.Inputs.data, matrix, widget=widget)\n        self.assertTrue(widget.Error.input_validation_error.is_shown())\n        self.assertIsNone(widget.data)\n        self.assertIsNone(widget.distances)\n        self.send_signal(widget.Inputs.data, None, widget=widget)\n        self.assertFalse(widget.Error.input_validation_error.is_shown())\n\n    def test_no_group_var(self):\n        widget = self.widget\n        data = Table(\"iris\")[::4]\n        data = data[:, data.domain.attributes]\n        matrix = Orange.distance.Euclidean(data)\n        self.send_signal(widget.Inputs.data, matrix, widget=widget)\n\n        self.assertTrue(widget.Error.input_validation_error.is_shown())\n        self.assertIsNone(widget.data)\n        self.assertIsNone(widget.distances)\n\n        self.send_signal(widget.Inputs.data, None, widget=widget)\n        self.assertFalse(widget.Error.input_validation_error.is_shown())\n\n    def test_unique_output_domain(self):\n        widget = self.widget\n        data = possible_duplicate_table('Silhouette (iris)')\n        matrix = Orange.distance.Euclidean(data)\n        self.send_signal(widget.Inputs.data, matrix, widget=widget)\n\n        output = self.get_output(self.widget.Outputs.annotated_data)\n        self.assertEqual(output.domain.metas[0].name, 'Silhouette (iris) (1)')\n\n    def test_report(self):\n        widget = self.widget\n        widget.report_plot = Mock()\n        widget.report_caption = Mock()\n\n        widget.send_report()\n        widget.report_plot.assert_not_called()\n        widget.report_caption.assert_not_called()\n\n        data = Table(\"zoo\")\n        self.send_signal(widget.Inputs.data, data)\n\n        widget.annotation_var = None\n        widget.send_report()\n        widget.report_plot.assert_called()\n        widget.report_caption.assert_called()\n        text = widget.report_caption.call_args[0][0]\n        self.assertIn(data.domain.class_var.name, text)\n        self.assertNotIn(\"nnotated\", text)\n\n        widget.annotation_var = data.domain.metas[0]\n        widget._silplot.rowNamesVisible = lambda: True\n        widget.send_report()\n        text = widget.report_caption.call_args[0][0]\n        self.assertIn(data.domain.class_var.name, text)\n        self.assertIn(\"nnotated\", text)\n        self.assertIn(data.domain.metas[0].name, text)\n\n    def test_migration(self):\n        enc_domain = dict(\n            attributes=(('foo', 1), ('bar', 1), ('baz', 1), ('bax', 1),\n                        ('cfoo', 1), ('mbaz', 1)))\n\n        # No annotation\n        context = Context(\n            values=dict(cluster_var_idx=(0, -2), annotation_var_idx=(0, -2)),\n            **enc_domain\n        )\n        OWSilhouettePlot.migrate_context(context, 1)\n        values = context.values\n        self.assertNotIn(\"cluster_var_idx\", values)\n        self.assertNotIn(\"annotation_var_idx\", values)\n        self.assertEqual(values[\"cluster_var\"], (\"foo\", 101))\n        self.assertEqual(values[\"annotation_var\"], None)\n\n        # We have annotation\n        context = Context(\n            values=dict(cluster_var_idx=(2, -2), annotation_var_idx=(4, -2)),\n            **enc_domain\n        )\n        OWSilhouettePlot.migrate_context(context, 1)\n        self.assertNotIn(\"cluster_var_idx\", values)\n        self.assertNotIn(\"annotation_var_idx\", values)\n        self.assertEqual(context.values[\"cluster_var\"], (\"baz\", 101))\n        self.assertEqual(context.values[\"annotation_var\"], (\"bax\", 101))\n\n        # We thought was had annotation, but the index is wrong due to\n        # incorrect domain\n        context = Context(\n            values=dict(cluster_var_idx=(4, -2), annotation_var_idx=(7, -2)),\n            **enc_domain\n        )\n        OWSilhouettePlot.migrate_context(context, 1)\n        self.assertEqual(context.values[\"cluster_var\"], (\"cfoo\", 101))\n        self.assertNotIn(\"annotation_var_idx\", values)\n\n\nif __name__ == \"__main__\":\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_3617003b27d75572ebbb11a136b3c079829e9bb1/.venv/lib/python3.7/site-packages/numpy/core/include -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_3617003b27d75572ebbb11a136b3c079829e9bb1/.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_3617003b27d75572ebbb11a136b3c079829e9bb1/.venv/lib/python3.7/site-packages/numpy/core/include -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_3617003b27d75572ebbb11a136b3c079829e9bb1/.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_3617003b27d75572ebbb11a136b3c079829e9bb1/.venv/lib/python3.7/site-packages/numpy/core/include -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_3617003b27d75572ebbb11a136b3c079829e9bb1/.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_3617003b27d75572ebbb11a136b3c079829e9bb1/.venv/lib/python3.7/site-packages/numpy/core/include -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_3617003b27d75572ebbb11a136b3c079829e9bb1/.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_3617003b27d75572ebbb11a136b3c079829e9bb1/.venv/lib/python3.7/site-packages/numpy/core/include -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_3617003b27d75572ebbb11a136b3c079829e9bb1/.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_3617003b27d75572ebbb11a136b3c079829e9bb1/.venv/lib/python3.7/site-packages/numpy/core/include -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_3617003b27d75572ebbb11a136b3c079829e9bb1/.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_3617003b27d75572ebbb11a136b3c079829e9bb1/.venv/lib/python3.7/site-packages/numpy/core/include -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_3617003b27d75572ebbb11a136b3c079829e9bb1/.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_3617003b27d75572ebbb11a136b3c079829e9bb1/.venv/lib/python3.7/site-packages/numpy/core/include -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_3617003b27d75572ebbb11a136b3c079829e9bb1/.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_3617003b27d75572ebbb11a136b3c079829e9bb1/.venv/lib/python3.7/site-packages/numpy/core/include -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_3617003b27d75572ebbb11a136b3c079829e9bb1/.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_3617003b27d75572ebbb11a136b3c079829e9bb1/.venv/lib/python3.7/site-packages/numpy/core/include -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_3617003b27d75572ebbb11a136b3c079829e9bb1/.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_3617003b27d75572ebbb11a136b3c079829e9bb1/.venv/lib/python3.7/site-packages/numpy/core/include -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_3617003b27d75572ebbb11a136b3c079829e9bb1/.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_3617003b27d75572ebbb11a136b3c079829e9bb1/.venv/lib/python3.7/site-packages/Orange3.egg-link (link to .)\nAdding Orange3 3.33.0.dev0+3617003 to easy-install.pth file\nInstalling orange-canvas script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_3617003b27d75572ebbb11a136b3c079829e9bb1/.venv/bin\n\nInstalled /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_3617003b27d75572ebbb11a136b3c079829e9bb1\nProcessing dependencies for Orange3==3.33.0.dev0+3617003\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_3617003b27d75572ebbb11a136b3c079829e9bb1/.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_3617003b27d75572ebbb11a136b3c079829e9bb1/.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_3617003b27d75572ebbb11a136b3c079829e9bb1/.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_3617003b27d75572ebbb11a136b3c079829e9bb1/.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_3617003b27d75572ebbb11a136b3c079829e9bb1/.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_3617003b27d75572ebbb11a136b3c079829e9bb1/.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_3617003b27d75572ebbb11a136b3c079829e9bb1/.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_3617003b27d75572ebbb11a136b3c079829e9bb1/.venv/bin\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_3617003b27d75572ebbb11a136b3c079829e9bb1/.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_3617003b27d75572ebbb11a136b3c079829e9bb1/.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_3617003b27d75572ebbb11a136b3c079829e9bb1/.venv/lib/python3.7/site-packages\nSearching for pyqtgraph==0.12.3\nBest match: pyqtgraph 0.12.3\nAdding pyqtgraph 0.12.3 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_3617003b27d75572ebbb11a136b3c079829e9bb1/.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_3617003b27d75572ebbb11a136b3c079829e9bb1/.venv/bin\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_3617003b27d75572ebbb11a136b3c079829e9bb1/.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_3617003b27d75572ebbb11a136b3c079829e9bb1/.venv/bin\nInstalling pip3 script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_3617003b27d75572ebbb11a136b3c079829e9bb1/.venv/bin\nInstalling pip3.10 script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_3617003b27d75572ebbb11a136b3c079829e9bb1/.venv/bin\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_3617003b27d75572ebbb11a136b3c079829e9bb1/.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_3617003b27d75572ebbb11a136b3c079829e9bb1/.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_3617003b27d75572ebbb11a136b3c079829e9bb1/.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_3617003b27d75572ebbb11a136b3c079829e9bb1/.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_3617003b27d75572ebbb11a136b3c079829e9bb1/.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_3617003b27d75572ebbb11a136b3c079829e9bb1/.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_3617003b27d75572ebbb11a136b3c079829e9bb1/.venv/bin\nInstalling f2py3 script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_3617003b27d75572ebbb11a136b3c079829e9bb1/.venv/bin\nInstalling f2py3.7 script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_3617003b27d75572ebbb11a136b3c079829e9bb1/.venv/bin\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_3617003b27d75572ebbb11a136b3c079829e9bb1/.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_3617003b27d75572ebbb11a136b3c079829e9bb1/.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_3617003b27d75572ebbb11a136b3c079829e9bb1/.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_3617003b27d75572ebbb11a136b3c079829e9bb1/.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_3617003b27d75572ebbb11a136b3c079829e9bb1/.venv/bin\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_3617003b27d75572ebbb11a136b3c079829e9bb1/.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_3617003b27d75572ebbb11a136b3c079829e9bb1/.venv/lib/python3.7/site-packages\nSearching for httpx==0.24.1\nBest match: httpx 0.24.1\nAdding httpx 0.24.1 to easy-install.pth file\nInstalling httpx script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_3617003b27d75572ebbb11a136b3c079829e9bb1/.venv/bin\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_3617003b27d75572ebbb11a136b3c079829e9bb1/.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_3617003b27d75572ebbb11a136b3c079829e9bb1/.venv/bin\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_3617003b27d75572ebbb11a136b3c079829e9bb1/.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_3617003b27d75572ebbb11a136b3c079829e9bb1/.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_3617003b27d75572ebbb11a136b3c079829e9bb1/.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_3617003b27d75572ebbb11a136b3c079829e9bb1/.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_3617003b27d75572ebbb11a136b3c079829e9bb1/.venv/bin\nInstalling pyrcc5 script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_3617003b27d75572ebbb11a136b3c079829e9bb1/.venv/bin\nInstalling pyuic5 script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_3617003b27d75572ebbb11a136b3c079829e9bb1/.venv/bin\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_3617003b27d75572ebbb11a136b3c079829e9bb1/.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_3617003b27d75572ebbb11a136b3c079829e9bb1/.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_3617003b27d75572ebbb11a136b3c079829e9bb1/.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_3617003b27d75572ebbb11a136b3c079829e9bb1/.venv/bin\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_3617003b27d75572ebbb11a136b3c079829e9bb1/.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_3617003b27d75572ebbb11a136b3c079829e9bb1/.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_3617003b27d75572ebbb11a136b3c079829e9bb1/.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_3617003b27d75572ebbb11a136b3c079829e9bb1/.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_3617003b27d75572ebbb11a136b3c079829e9bb1/.venv/bin\nInstalling jupyter-migrate script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_3617003b27d75572ebbb11a136b3c079829e9bb1/.venv/bin\nInstalling jupyter-troubleshoot script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_3617003b27d75572ebbb11a136b3c079829e9bb1/.venv/bin\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_3617003b27d75572ebbb11a136b3c079829e9bb1/.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_3617003b27d75572ebbb11a136b3c079829e9bb1/.venv/bin\nInstalling jupyter-kernelspec script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_3617003b27d75572ebbb11a136b3c079829e9bb1/.venv/bin\nInstalling jupyter-run script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_3617003b27d75572ebbb11a136b3c079829e9bb1/.venv/bin\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_3617003b27d75572ebbb11a136b3c079829e9bb1/.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_3617003b27d75572ebbb11a136b3c079829e9bb1/.venv/bin\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_3617003b27d75572ebbb11a136b3c079829e9bb1/.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_3617003b27d75572ebbb11a136b3c079829e9bb1/.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_3617003b27d75572ebbb11a136b3c079829e9bb1/.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_3617003b27d75572ebbb11a136b3c079829e9bb1/.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_3617003b27d75572ebbb11a136b3c079829e9bb1/.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_3617003b27d75572ebbb11a136b3c079829e9bb1/.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_3617003b27d75572ebbb11a136b3c079829e9bb1/.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_3617003b27d75572ebbb11a136b3c079829e9bb1/.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_3617003b27d75572ebbb11a136b3c079829e9bb1/.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_3617003b27d75572ebbb11a136b3c079829e9bb1/.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_3617003b27d75572ebbb11a136b3c079829e9bb1/.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_3617003b27d75572ebbb11a136b3c079829e9bb1/.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_3617003b27d75572ebbb11a136b3c079829e9bb1/.venv/bin\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_3617003b27d75572ebbb11a136b3c079829e9bb1/.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_3617003b27d75572ebbb11a136b3c079829e9bb1/.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_3617003b27d75572ebbb11a136b3c079829e9bb1/.venv/bin\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_3617003b27d75572ebbb11a136b3c079829e9bb1/.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_3617003b27d75572ebbb11a136b3c079829e9bb1/.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_3617003b27d75572ebbb11a136b3c079829e9bb1/.venv/bin\nInstalling pyftmerge script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_3617003b27d75572ebbb11a136b3c079829e9bb1/.venv/bin\nInstalling pyftsubset script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_3617003b27d75572ebbb11a136b3c079829e9bb1/.venv/bin\nInstalling ttx script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_3617003b27d75572ebbb11a136b3c079829e9bb1/.venv/bin\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_3617003b27d75572ebbb11a136b3c079829e9bb1/.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_3617003b27d75572ebbb11a136b3c079829e9bb1/.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_3617003b27d75572ebbb11a136b3c079829e9bb1/.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_3617003b27d75572ebbb11a136b3c079829e9bb1/.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_3617003b27d75572ebbb11a136b3c079829e9bb1/.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_3617003b27d75572ebbb11a136b3c079829e9bb1/.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_3617003b27d75572ebbb11a136b3c079829e9bb1/.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_3617003b27d75572ebbb11a136b3c079829e9bb1/.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_3617003b27d75572ebbb11a136b3c079829e9bb1/.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_3617003b27d75572ebbb11a136b3c079829e9bb1/.venv/lib/python3.7/site-packages\nSearching for httpcore==0.17.3\nBest match: httpcore 0.17.3\nAdding httpcore 0.17.3 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_3617003b27d75572ebbb11a136b3c079829e9bb1/.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_3617003b27d75572ebbb11a136b3c079829e9bb1/.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_3617003b27d75572ebbb11a136b3c079829e9bb1/.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_3617003b27d75572ebbb11a136b3c079829e9bb1/.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_3617003b27d75572ebbb11a136b3c079829e9bb1/.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_3617003b27d75572ebbb11a136b3c079829e9bb1/.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_3617003b27d75572ebbb11a136b3c079829e9bb1/.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_3617003b27d75572ebbb11a136b3c079829e9bb1/.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_3617003b27d75572ebbb11a136b3c079829e9bb1/.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_3617003b27d75572ebbb11a136b3c079829e9bb1/.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_3617003b27d75572ebbb11a136b3c079829e9bb1/.venv/bin\nInstalling iptest3 script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_3617003b27d75572ebbb11a136b3c079829e9bb1/.venv/bin\nInstalling ipython script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_3617003b27d75572ebbb11a136b3c079829e9bb1/.venv/bin\nInstalling ipython3 script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_3617003b27d75572ebbb11a136b3c079829e9bb1/.venv/bin\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_3617003b27d75572ebbb11a136b3c079829e9bb1/.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_3617003b27d75572ebbb11a136b3c079829e9bb1/.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_3617003b27d75572ebbb11a136b3c079829e9bb1/.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_3617003b27d75572ebbb11a136b3c079829e9bb1/.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_3617003b27d75572ebbb11a136b3c079829e9bb1/.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_3617003b27d75572ebbb11a136b3c079829e9bb1/.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_3617003b27d75572ebbb11a136b3c079829e9bb1/.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_3617003b27d75572ebbb11a136b3c079829e9bb1/.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_3617003b27d75572ebbb11a136b3c079829e9bb1/.venv/lib/python3.7/site-packages\nSearching for h11==0.14.0\nBest match: h11 0.14.0\nAdding h11 0.14.0 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_3617003b27d75572ebbb11a136b3c079829e9bb1/.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_3617003b27d75572ebbb11a136b3c079829e9bb1/.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_3617003b27d75572ebbb11a136b3c079829e9bb1/.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_3617003b27d75572ebbb11a136b3c079829e9bb1/.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_3617003b27d75572ebbb11a136b3c079829e9bb1/.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_3617003b27d75572ebbb11a136b3c079829e9bb1/.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_3617003b27d75572ebbb11a136b3c079829e9bb1/.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_3617003b27d75572ebbb11a136b3c079829e9bb1/.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_3617003b27d75572ebbb11a136b3c079829e9bb1/.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_3617003b27d75572ebbb11a136b3c079829e9bb1/.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_3617003b27d75572ebbb11a136b3c079829e9bb1/.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_3617003b27d75572ebbb11a136b3c079829e9bb1/.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_3617003b27d75572ebbb11a136b3c079829e9bb1/.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_3617003b27d75572ebbb11a136b3c079829e9bb1/.venv/lib/python3.7/site-packages\nFinished processing dependencies for Orange3==3.33.0.dev0+3617003\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 227ms\nPrepared 18 packages in 35ms\nInstalled 18 packages in 126ms\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 54 packages in 341ms\nInstalled 47 packages in 533ms\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.14.0\n + httpcore==0.17.3\n + httpx==0.24.1\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 + 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 355ms\nInstalled 38 packages in 391ms\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.3\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 7ms\nInstalled 2 packages in 48ms\n + psycopg2==2.9.9\n + pymssql==2.3.2\nResolved 19 packages in 26ms\nInstalled 5 packages in 894ms\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_3617003b27d75572ebbb11a136b3c079829e9bb1/.venv/lib/python3.7/site-packages/numpy/core/include/numpy/ndarraytypes.h:1830,\n                 from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_3617003b27d75572ebbb11a136b3c079829e9bb1/.venv/lib/python3.7/site-packages/numpy/core/include/numpy/ndarrayobject.h:12,\n                 from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_3617003b27d75572ebbb11a136b3c079829e9bb1/.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_3617003b27d75572ebbb11a136b3c079829e9bb1/.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_3617003b27d75572ebbb11a136b3c079829e9bb1/.venv/lib/python3.7/site-packages/numpy/core/include/numpy/ndarraytypes.h:1830,\n                 from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_3617003b27d75572ebbb11a136b3c079829e9bb1/.venv/lib/python3.7/site-packages/numpy/core/include/numpy/ndarrayobject.h:12,\n                 from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_3617003b27d75572ebbb11a136b3c079829e9bb1/.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_3617003b27d75572ebbb11a136b3c079829e9bb1/.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_3617003b27d75572ebbb11a136b3c079829e9bb1/.venv/lib/python3.7/site-packages/numpy/core/include/numpy/ndarraytypes.h:1830,\n                 from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_3617003b27d75572ebbb11a136b3c079829e9bb1/.venv/lib/python3.7/site-packages/numpy/core/include/numpy/ndarrayobject.h:12,\n                 from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_3617003b27d75572ebbb11a136b3c079829e9bb1/.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_3617003b27d75572ebbb11a136b3c079829e9bb1/.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_3617003b27d75572ebbb11a136b3c079829e9bb1/.venv/lib/python3.7/site-packages/numpy/core/include/numpy/ndarraytypes.h:1830,\n                 from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_3617003b27d75572ebbb11a136b3c079829e9bb1/.venv/lib/python3.7/site-packages/numpy/core/include/numpy/ndarrayobject.h:12,\n                 from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_3617003b27d75572ebbb11a136b3c079829e9bb1/.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_3617003b27d75572ebbb11a136b3c079829e9bb1/.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_3617003b27d75572ebbb11a136b3c079829e9bb1/.venv/lib/python3.7/site-packages/numpy/core/include/numpy/ndarraytypes.h:1830,\n                 from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_3617003b27d75572ebbb11a136b3c079829e9bb1/.venv/lib/python3.7/site-packages/numpy/core/include/numpy/ndarrayobject.h:12,\n                 from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_3617003b27d75572ebbb11a136b3c079829e9bb1/.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_3617003b27d75572ebbb11a136b3c079829e9bb1/.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_3617003b27d75572ebbb11a136b3c079829e9bb1/.venv/lib/python3.7/site-packages/numpy/core/include/numpy/ndarraytypes.h:1830,\n                 from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_3617003b27d75572ebbb11a136b3c079829e9bb1/.venv/lib/python3.7/site-packages/numpy/core/include/numpy/ndarrayobject.h:12,\n                 from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_3617003b27d75572ebbb11a136b3c079829e9bb1/.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_3617003b27d75572ebbb11a136b3c079829e9bb1/.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_3617003b27d75572ebbb11a136b3c079829e9bb1/.venv/lib/python3.7/site-packages/numpy/core/include/numpy/ndarraytypes.h:1830,\n                 from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_3617003b27d75572ebbb11a136b3c079829e9bb1/.venv/lib/python3.7/site-packages/numpy/core/include/numpy/ndarrayobject.h:12,\n                 from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_3617003b27d75572ebbb11a136b3c079829e9bb1/.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_3617003b27d75572ebbb11a136b3c079829e9bb1/.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_3617003b27d75572ebbb11a136b3c079829e9bb1/.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_3617003b27d75572ebbb11a136b3c079829e9bb1/.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_3617003b27d75572ebbb11a136b3c079829e9bb1\nplugins: anyio-3.7.1\ncollected 23 items\n\nr2e_tests/test_1.py sss....................                              [100%]\n\n==================================== PASSES ====================================\n=========================== short test summary info ============================\nPASSED r2e_tests/test_1.py::TestOWSilhouettePlot::test_bad_data_range\nPASSED r2e_tests/test_1.py::TestOWSilhouettePlot::test_distance_input\nPASSED r2e_tests/test_1.py::TestOWSilhouettePlot::test_ignore_categorical\nPASSED r2e_tests/test_1.py::TestOWSilhouettePlot::test_image_export\nPASSED r2e_tests/test_1.py::TestOWSilhouettePlot::test_input_distance_no_data\nPASSED r2e_tests/test_1.py::TestOWSilhouettePlot::test_insufficient_clusters\nPASSED r2e_tests/test_1.py::TestOWSilhouettePlot::test_memory_error\nPASSED r2e_tests/test_1.py::TestOWSilhouettePlot::test_meta_object_dtype\nPASSED r2e_tests/test_1.py::TestOWSilhouettePlot::test_migration\nPASSED r2e_tests/test_1.py::TestOWSilhouettePlot::test_minimum_size\nPASSED r2e_tests/test_1.py::TestOWSilhouettePlot::test_msg_base_class\nPASSED r2e_tests/test_1.py::TestOWSilhouettePlot::test_nan_distances\nPASSED r2e_tests/test_1.py::TestOWSilhouettePlot::test_no_data\nPASSED r2e_tests/test_1.py::TestOWSilhouettePlot::test_no_group_var\nPASSED r2e_tests/test_1.py::TestOWSilhouettePlot::test_outputs\nPASSED r2e_tests/test_1.py::TestOWSilhouettePlot::test_outputs_add_scores\nPASSED r2e_tests/test_1.py::TestOWSilhouettePlot::test_report\nPASSED r2e_tests/test_1.py::TestOWSilhouettePlot::test_saved_selection\nPASSED r2e_tests/test_1.py::TestOWSilhouettePlot::test_unique_output_domain\nPASSED r2e_tests/test_1.py::TestOWSilhouettePlot::test_unknowns_in_labels\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======================== 20 passed, 3 skipped in 5.24s =========================\nWidgets left: 0    Max widgets: 2259 \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_3617003b27d75572ebbb11a136b3c079829e9bb1\nplugins: anyio-3.7.1\ncollected 23 items\n\nr2e_tests/test_1.py sss........F...........                              [100%]\n\n=================================== FAILURES ===================================\n_____________________ TestOWSilhouettePlot.test_migration ______________________\n\nself = <r2e_tests.test_1.TestOWSilhouettePlot testMethod=test_migration>\n\n    def test_migration(self):\n        enc_domain = dict(\n            attributes=(('foo', 1), ('bar', 1), ('baz', 1), ('bax', 1),\n                        ('cfoo', 1), ('mbaz', 1)))\n    \n        # No annotation\n        context = Context(\n            values=dict(cluster_var_idx=(0, -2), annotation_var_idx=(0, -2)),\n            **enc_domain\n        )\n>       OWSilhouettePlot.migrate_context(context, 1)\n\nr2e_tests/test_1.py:270: \n_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ \n\ncls = <class 'Orange.widgets.visualize.owsilhouetteplot.OWSilhouettePlot'>\ncontext = <orangewidget.settings.Context object at 0x72cfa876b3d0>, version = 1\n\n    @classmethod\n    def migrate_context(cls, context, version):\n        values = context.values\n        if version < 2:\n            discrete_vars = (\n                name\n                for name, type_ in itertools.chain(\n>                   context.attributes, context.class_vars, context.metas)\n                if type_ == 1)\nE           AttributeError: 'Context' object has no attribute 'class_vars'\n\nOrange/widgets/visualize/owsilhouetteplot.py:545: AttributeError\n==================================== PASSES ====================================\n=========================== short test summary info ============================\nPASSED r2e_tests/test_1.py::TestOWSilhouettePlot::test_bad_data_range\nPASSED r2e_tests/test_1.py::TestOWSilhouettePlot::test_distance_input\nPASSED r2e_tests/test_1.py::TestOWSilhouettePlot::test_ignore_categorical\nPASSED r2e_tests/test_1.py::TestOWSilhouettePlot::test_image_export\nPASSED r2e_tests/test_1.py::TestOWSilhouettePlot::test_input_distance_no_data\nPASSED r2e_tests/test_1.py::TestOWSilhouettePlot::test_insufficient_clusters\nPASSED r2e_tests/test_1.py::TestOWSilhouettePlot::test_memory_error\nPASSED r2e_tests/test_1.py::TestOWSilhouettePlot::test_meta_object_dtype\nPASSED r2e_tests/test_1.py::TestOWSilhouettePlot::test_minimum_size\nPASSED r2e_tests/test_1.py::TestOWSilhouettePlot::test_msg_base_class\nPASSED r2e_tests/test_1.py::TestOWSilhouettePlot::test_nan_distances\nPASSED r2e_tests/test_1.py::TestOWSilhouettePlot::test_no_data\nPASSED r2e_tests/test_1.py::TestOWSilhouettePlot::test_no_group_var\nPASSED r2e_tests/test_1.py::TestOWSilhouettePlot::test_outputs\nPASSED r2e_tests/test_1.py::TestOWSilhouettePlot::test_outputs_add_scores\nPASSED r2e_tests/test_1.py::TestOWSilhouettePlot::test_report\nPASSED r2e_tests/test_1.py::TestOWSilhouettePlot::test_saved_selection\nPASSED r2e_tests/test_1.py::TestOWSilhouettePlot::test_unique_output_domain\nPASSED r2e_tests/test_1.py::TestOWSilhouettePlot::test_unknowns_in_labels\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::TestOWSilhouettePlot::test_migration - AttributeE...\n=================== 1 failed, 19 passed, 3 skipped in 4.29s ====================\nWidgets left: 0    Max widgets: 2259 \n\n",
    "old_commit_res_stderr": ""
}
Orange/widgets/visualize/owsilhouetteplot.py Orange/widgets/visualize/tests/test_owsilhouetteplot.py
We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
[ISSUE]
**Title:** Context Migration Fails Due to Missing `class_vars` Attribute

**Description:**
When attempting to migrate context for the `OWSilhouettePlot` widget, an error occurs if the `Context` object lacks the `class_vars` attribute. This issue arises during the migration process for contexts with version less than 2, leading to an `AttributeError`.

**Example Buggy Code:**
```python
def test_migration_failure():
    enc_domain = {
        'attributes': (('foo', 1), ('bar', 1), ('baz', 1), ('bax', 1), ('cfoo', 1), ('mbaz', 1))
    }
    context = Context(
        values={'cluster_var_idx': (0, -2), 'annotation_var_idx': (0, -2)},
        **enc_domain
    )
    OWSilhouettePlot.migrate_context(context, 1)
```
In this test, the `Context` object is initialized without the `class_vars` attribute. When `migrate_context` is called with version `1`, it attempts to access `context.class_vars`, which does not exist, resulting in an error.

**Expected Behavior:**
The context migration should handle cases where certain attributes like `class_vars` or `metas` are missing from the `Context` object. Instead of raising an error, it should gracefully migrate the available attributes and maintain the integrity of the context values.

**Actual Behavior:**
An `AttributeError` is raised:
```
AttributeError: 'Context' object has no attribute 'class_vars'
```
This prevents the successful migration of the context, potentially disrupting the workflow for users relying on older context versions.

[/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/owsilhouetteplot.py
import itertools
import enum
from xml.sax.saxutils import escape
from types import SimpleNamespace as namespace

from typing import Optional, Union, Tuple, cast

import numpy as np
import sklearn.metrics

from AnyQt.QtWidgets import (
    QGraphicsWidget, QGraphicsGridLayout,
    QGraphicsRectItem, QStyleOptionGraphicsItem, QSizePolicy, QWidget,
    QVBoxLayout, QGraphicsSimpleTextItem, QWIDGETSIZE_MAX,
    QGraphicsSceneHelpEvent, QToolTip, QApplication,
)
from AnyQt.QtGui import (
    QColor, QPen, QBrush, QPainter, QFontMetrics, QPalette,
)
from AnyQt.QtCore import (
    Qt, QEvent, QRectF, QSizeF, QSize, QPointF, QPoint, QRect
)
from AnyQt.QtCore import pyqtSignal as Signal

import Orange.data
from Orange.data.util import get_unique_names
import Orange.distance
import Orange.misc
from Orange.data import Table, Domain
from Orange.misc import DistMatrix

from Orange.widgets import widget, gui, settings
from Orange.widgets.utils.graphicsscene import GraphicsScene
from Orange.widgets.utils.stickygraphicsview import StickyGraphicsView
from Orange.widgets.utils import itemmodels, apply_all
from Orange.widgets.utils.annotated_data import (create_annotated_table,
                                                 ANNOTATED_DATA_SIGNAL_NAME)
from Orange.widgets.utils.graphicstextlist import TextListWidget
from Orange.widgets.utils.graphicslayoutitem import SimpleLayoutItem
from Orange.widgets.utils.sql import check_sql_input
from Orange.widgets.utils.widgetpreview import WidgetPreview
from Orange.widgets.visualize.utils.plotutils import AxisItem
from Orange.widgets.widget import Msg, Input, Output


class InputValidationError(ValueError):
    message: str


class NoGroupVariable(InputValidationError):
    message = "Input does not have any suitable labels"


class ValidationError(InputValidationError):
    def __init__(self, message: str):
        super().__init__(message)
        self.message = message


class OWSilhouettePlot(widget.OWWidget):
    name = "Silhouette Plot"
    description = "Visually assess cluster quality and " \
                  "the degree of cluster membership."

    icon = "icons/SilhouettePlot.svg"
    priority = 300
    keywords = []

    class Inputs:
        data = Input("Data", (Orange.data.Table, Orange.misc.DistMatrix))

    class Outputs:
        selected_data = Output("Selected Data", Orange.data.Table, default=True)
        annotated_data = Output(ANNOTATED_DATA_SIGNAL_NAME, Orange.data.Table)

    replaces = [
        "orangecontrib.prototypes.widgets.owsilhouetteplot.OWSilhouettePlot",
        "Orange.widgets.unsupervised.owsilhouetteplot.OWSilhouettePlot"
    ]

    settingsHandler = settings.DomainContextHandler()
    settings_version = 2

    #: Distance metric index
    distance_idx = settings.Setting(0)
    #: Group/cluster variable
    cluster_var = settings.ContextSetting(None)
    #: Annotation variable
    annotation_var = settings.ContextSetting(None)
    #: Group the (displayed) silhouettes by cluster
    group_by_cluster = settings.Setting(True)
    #: A fixed size for an instance bar
    bar_size = settings.Setting(3)
    #: Add silhouette scores to output data
    auto_commit = settings.Setting(True)

    pending_selection = settings.Setting(None, schema_only=True)

    Distances = [("Euclidean", Orange.distance.Euclidean),
                 ("Manhattan", Orange.distance.Manhattan),
                 ("Cosine", Orange.distance.Cosine)]

    graph_name = "scene"

    class Error(widget.OWWidget.Error):
        need_two_clusters = Msg("Need at least two non-empty clusters")
        singleton_clusters_all = Msg("All clusters are singletons")
        memory_error = Msg("Not enough memory")
        value_error = Msg("Distances could not be computed: '{}'")
        input_validation_error = Msg("{}")

    class Warning(widget.OWWidget.Warning):
        missing_cluster_assignment = Msg(
            "{} instance{s} omitted (missing cluster assignment)")
        nan_distances = Msg("{} instance{s} omitted (undefined distances)")
        ignoring_categorical = Msg("Ignoring categorical features")

    def __init__(self):
        super().__init__()
        #: The input data
        self.data = None         # type: Optional[Orange.data.Table]
        #: The input distance matrix (if present)
        self.distances = None  # type: Optional[Orange.misc.DistMatrix]
        #: The effective distance matrix (is self.distances or computed from
        #: self.data depending on input)
        self._matrix = None      # type: Optional[Orange.misc.DistMatrix]
        #: An bool mask (size == len(data)) indicating missing group/cluster
        #: assignments
        self._mask = None        # type: Optional[np.ndarray]
        #: An array of cluster/group labels for instances with valid group
        #: assignment
        self._labels = None      # type: Optional[np.ndarray]
        #: An array of silhouette scores for instances with valid group
        #: assignment
        self._silhouette = None  # type: Optional[np.ndarray]
        self._silplot = None     # type: Optional[SilhouettePlot]

        controllayout = self.controlArea.layout()
        assert isinstance(controllayout, QVBoxLayout)
        self._distances_gui_box = distbox = gui.widgetBox(
            None, "Distance"
        )
        self._distances_gui_cb = gui.comboBox(
            distbox, self, "distance_idx",
            items=[name for name, _ in OWSilhouettePlot.Distances],
            orientation=Qt.Horizontal, callback=self._invalidate_distances)
        controllayout.addWidget(distbox)

        box = gui.vBox(self.controlArea, "Grouping")
        self.cluster_var_model = itemmodels.VariableListModel(
            parent=self, placeholder="(None)")
        self.cluster_var_cb = gui.comboBox(
            box, self, "cluster_var", contentsLength=14,
            searchable=True, callback=self._invalidate_scores,
            model=self.cluster_var_model
        )
        gui.checkBox(
            box, self, "group_by_cluster", "Show in groups",
            callback=self._replot)

        box = gui.vBox(self.controlArea, "Bars")
        gui.widgetLabel(box, "Bar width:")
        gui.hSlider(
            box, self, "bar_size", minValue=1, maxValue=10, step=1,
            callback=self._update_bar_size)
        gui.widgetLabel(box, "Annotations:")
        self.annotation_var_model = itemmodels.VariableListModel(parent=self)
        self.annotation_var_model[:] = [None]
        self.annotation_cb = gui.comboBox(
            box, self, "annotation_var", contentsLength=14,
            callback=self._update_annotations,
            model=self.annotation_var_model)
        ibox = gui.indentedBox(box, 5)
        self.ann_hidden_warning = warning = gui.widgetLabel(
            ibox, "(increase the width to show)")
        ibox.setFixedWidth(ibox.sizeHint().width())
        warning.setVisible(False)

        gui.rubber(self.controlArea)

        gui.auto_send(self.buttonsArea, self, "auto_commit")

        self.scene = GraphicsScene(self)
        self.view = StyledGraphicsView(self.scene)
        self.view.setRenderHint(QPainter.Antialiasing, True)
        self.view.setAlignment(Qt.AlignTop | Qt.AlignLeft)
        self.mainArea.layout().addWidget(self.view)

        self.settingsAboutToBePacked.connect(self.pack_settings)

    def sizeHint(self):
        sh = self.controlArea.sizeHint()
        return sh.expandedTo(QSize(600, 720))

    def pack_settings(self):
        if self.data and self._silplot is not None:
            self.pending_selection = list(self._silplot.selection())
        else:
            self.pending_selection = None

    @Inputs.data
    @check_sql_input
    def set_data(self, data: Union[Table, DistMatrix, None]):
        """
        Set the input dataset or distance matrix.
        """
        self.closeContext()
        self.clear()
        try:
            if isinstance(data, Orange.misc.DistMatrix):
                self._set_distances(data)
            elif isinstance(data, Orange.data.Table):
                self._set_table(data)
            else:
                self.distances = None
                self.data = None
        except InputValidationError as err:
            self.Error.input_validation_error(err.message)
            self.distances = None
            self.data = None

    def _set_table(self, data: Table):
        self._setup_control_models(data.domain)
        self.data = data
        self.distances = None

    def _set_distances(self, distances: DistMatrix):
        if isinstance(distances.row_items, Orange.data.Table) and \
                distances.axis == 1:
            data = distances.row_items
        else:
            raise ValidationError("Input matrix does not have associated data")

        if data is not None:
            self._setup_control_models(data.domain)
            self.distances = distances
            self.data = data

    def handleNewSignals(self):
        if not self._is_empty():
            self._update()
            self._replot()
            if self.pending_selection is not None and self._silplot is not None:
                # If selection contains indices that are too large, the data
                # file must had been modified, so we ignore selection
                if max(self.pending_selection, default=-1) < len(self.data):
                    self._silplot.setSelection(np.array(self.pending_selection))
                self.pending_selection = None

        # Disable/enable the Distances GUI controls if applicable
        self._distances_gui_box.setEnabled(self.distances is None)

        self.commit.now()

    def _setup_control_models(self, domain: Domain):
        groupvars = [
            v for v in domain.variables + domain.metas
            if v.is_discrete and len(v.values) >= 2]
        if not groupvars:
            raise NoGroupVariable()
        self.cluster_var_model[:] = groupvars
        if domain.class_var in groupvars:
            self.cluster_var = domain.class_var
        else:
            self.cluster_var = groupvars[0]
        annotvars = [var for var in domain.variables + domain.metas
                     if var.is_string or var.is_discrete]
        self.annotation_var_model[:] = [None] + annotvars
        self.annotation_var = annotvars[0] if annotvars else None
        self.openContext(Orange.data.Domain(groupvars))

    def _is_empty(self) -> bool:
        # Is empty (does not have any input).
        return (self.data is None or len(self.data) == 0) \
               and self.distances is None

    def clear(self):
        """
        Clear the widget state.
        """
        self.data = None
        self.distances = None
        self._matrix = None
        self._mask = None
        self._silhouette = None
        self._labels = None
        self.cluster_var_model[:] = []
        self.annotation_var_model[:] = [None]
        self._clear_scene()
        self.Error.clear()
        self.Warning.clear()

    def _clear_scene(self):
        # Clear the graphics scene and associated objects
        self.scene.clear()
        self.scene.setSceneRect(QRectF())
        self.view.setSceneRect(QRectF())
        self.view.setHeaderSceneRect(QRectF())
        self.view.setFooterSceneRect(QRectF())
        self._silplot = None

    def _invalidate_distances(self):
        # Invalidate the computed distance matrix and recompute the silhouette.
        self._matrix = None
        self._invalidate_scores()

    def _invalidate_scores(self):
        # Invalidate and recompute the current silhouette scores.
        self._labels = self._silhouette = self._mask = None
        self._update()
        self._replot()
        if self.data is not None:
            self.commit.deferred()

    def _ensure_matrix(self):
        # ensure self._matrix is computed if necessary
        if self._is_empty():
            return
        if self._matrix is None:
            if self.distances is not None:
                self._matrix = np.asarray(self.distances)
            elif self.data is not None:
                data = self.data
                _, metric = self.Distances[self.distance_idx]
                if not metric.supports_discrete and any(
                        a.is_discrete for a in data.domain.attributes):
                    self.Warning.ignoring_categorical()
                    data = Orange.distance.remove_discrete_features(data)
                try:
                    self._matrix = np.asarray(metric(data))
                except MemoryError:
                    self.Error.memory_error()
                    return
                except ValueError as err:
                    self.Error.value_error(str(err))
                    return
            else:
                assert False, "invalid state"

    def _update(self):
        # Update/recompute the effective distances and scores as required.
        self._clear_messages()
        if self._is_empty():
            self._reset_all()
            return

        self._ensure_matrix()
        if self._matrix is None:
            return

        labels, _ = self.data.get_column_view(self.cluster_var)
        labels = np.asarray(labels, dtype=float)
        cluster_mask = np.isnan(labels)
        dist_mask = np.isnan(self._matrix).all(axis=0)
        mask = cluster_mask | dist_mask
        labels = labels.astype(int)
        labels = labels[~mask]

        labels_unq = np.unique(labels)

        if len(labels_unq) < 2:
            self.Error.need_two_clusters()
            labels = silhouette = mask = None
        elif len(labels_unq) == len(labels):
            self.Error.singleton_clusters_all()
            labels = silhouette = mask = None
        else:
            silhouette = sklearn.metrics.silhouette_samples(
                self._matrix[~mask, :][:, ~mask], labels, metric="precomputed")
        self._mask = mask
        self._labels = labels
        self._silhouette = silhouette

        if mask is not None:
            count_missing = np.count_nonzero(cluster_mask)
            if count_missing:
                self.Warning.missing_cluster_assignment(
                    count_missing, s="s" if count_missing > 1 else "")
            count_nandist = np.count_nonzero(dist_mask)
            if count_nandist:
                self.Warning.nan_distances(
                    count_nandist, s="s" if count_nandist > 1 else "")

    def _reset_all(self):
        self._mask = None
        self._silhouette = None
        self._labels = None
        self._matrix = None
        self._clear_scene()

    def _clear_messages(self):
        self.Error.clear()
        self.Warning.clear()

    def _set_bar_height(self):
        visible = self.bar_size >= 5
        self._silplot.setBarHeight(self.bar_size)
        self._silplot.setRowNamesVisible(visible)
        self.ann_hidden_warning.setVisible(
            not visible and self.annotation_var is not None)

    def _replot(self):
        # Clear and replot/initialize the scene
        self._clear_scene()
        if self._silhouette is not None and self._labels is not None:
            self._silplot = silplot = SilhouettePlot()
            self._set_bar_height()

            if self.group_by_cluster:
                silplot.setScores(
                    self._silhouette, self._labels,
                    self.cluster_var.values, self.cluster_var.colors)
            else:
                silplot.setScores(
                    self._silhouette,
                    np.zeros(len(self._silhouette), dtype=int),
                    [""], np.array([[63, 207, 207]])
                )

            self.scene.addItem(silplot)
            self._update_annotations()
            silplot.selectionChanged.connect(self.commit.deferred)
            silplot.layout().activate()
            self._update_scene_rect()
            silplot.geometryChanged.connect(self._update_scene_rect)

    def _update_bar_size(self):
        if self._silplot is not None:
            self._set_bar_height()

    def _update_annotations(self):
        annot_var = self.annotation_var
        self.ann_hidden_warning.setVisible(
            self.bar_size < 5 and annot_var is not None)

        if self._silplot is not None:
            if annot_var is not None:
                column, _ = self.data.get_column_view(annot_var)
                if self._mask is not None:
                    assert column.shape == self._mask.shape
                    # pylint: disable=invalid-unary-operand-type
                    column = column[~self._mask]
                self._silplot.setRowNames(
                    [annot_var.str_val(value) for value in column])
            else:
                self._silplot.setRowNames(None)

    def _update_scene_rect(self):
        geom = self._silplot.geometry()
        self.scene.setSceneRect(geom)
        self.view.setSceneRect(geom)

        header = self._silplot.topScaleItem()
        footer = self._silplot.bottomScaleItem()

        def extend_horizontal(rect):
            # type: (QRectF) -> QRectF
            rect = QRectF(rect)
            rect.setLeft(geom.left())
            rect.setRight(geom.right())
            return rect

        margin = 3
        if header is not None:
            self.view.setHeaderSceneRect(
                extend_horizontal(header.geometry().adjusted(0, 0, 0, margin)))
        if footer is not None:
            self.view.setFooterSceneRect(
                extend_horizontal(footer.geometry().adjusted(0, -margin, 0, 0)))

    @gui.deferred
    def commit(self):
        """
        Commit/send the current selection to the output.
        """
        selected = indices = data = None
        if self.data is not None:
            selectedmask = np.full(len(self.data), False, dtype=bool)
            if self._silplot is not None:
                indices = self._silplot.selection()
                assert (np.diff(indices) > 0).all(), "strictly increasing"
                if self._mask is not None:
                    # pylint: disable=invalid-unary-operand-type
                    indices = np.flatnonzero(~self._mask)[indices]
                selectedmask[indices] = True

            if self._mask is not None:
                scores = np.full(shape=selectedmask.shape,
                                 fill_value=np.nan)
                # pylint: disable=invalid-unary-operand-type
                scores[~self._mask] = self._silhouette
            else:
                scores = self._silhouette

            domain = self.data.domain
            proposed = "Silhouette ({})".format(escape(self.cluster_var.name))
            names = [var.name for var in itertools.chain(domain.attributes,
                                                         domain.class_vars,
                                                         domain.metas)]
            unique = get_unique_names(names, proposed)
            silhouette_var = Orange.data.ContinuousVariable(unique)
            domain = Orange.data.Domain(
                domain.attributes,
                domain.class_vars,
                domain.metas + (silhouette_var, ))

            if np.count_nonzero(selectedmask):
                selected = self.data.from_table(
                    domain, self.data, np.flatnonzero(selectedmask))

            if selected is not None:
                with selected.unlocked(selected.metas):
                    selected[:, silhouette_var] = np.c_[scores[selectedmask]]

            data = self.data.transform(domain)
            with data.unlocked(data.metas):
                data[:, silhouette_var] = np.c_[scores]

        self.Outputs.selected_data.send(selected)
        self.Outputs.annotated_data.send(create_annotated_table(data, indices))

    def send_report(self):
        if not len(self.cluster_var_model):
            return

        self.report_plot()
        caption = "Silhouette plot " \
                  f"({self.Distances[self.distance_idx][0]} distance), " \
                  f"clustered by '{self.cluster_var.name}'"
        if self.annotation_var and self._silplot.rowNamesVisible():
            caption += f", annotated with '{self.annotation_var.name}'"
        self.report_caption(caption)

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

    @classmethod
    def migrate_context(cls, context, version):
        values = context.values
        if version < 2:
            discrete_vars = (
                name
                for name, type_ in itertools.chain(
                    context.attributes, context.class_vars, context.metas)
                if type_ == 1)
            index, _ = values.pop("cluster_var_idx")
            name = next(itertools.islice(discrete_vars, index, None)
            )
            values["cluster_var"] = (name, 101)

            index = values.pop("annotation_var_idx")[0] - 1
            if index == -1:
                values["annotation_var"] = None
            else:
                annot_vars = (
                    (name, type_ + 100)
                    for name, type_ in itertools.chain(
                        context.attributes, context.class_vars, context.metas)
                    if type_ in (1, 3))
                values["annotation_var"] = next(
                    itertools.islice(annot_vars, index, None))


class SelectAction(enum.IntEnum):
    NoUpdate, Clear, Select, Deselect, Toogle, Current = 1, 2, 4, 8, 16, 32


def show_tool_tip(pos: QPoint, text: str, widget: Optional[QWidget] = None,
                  rect=QRect(), elide=Qt.ElideRight):
    """
    Show a plain text tool tip with limited length, eliding if necessary.
    """
    if widget is not None:
        screen = widget.screen()
    else:
        screen = QApplication.screenAt(pos)
    font = QApplication.font("QTipLabel")
    fm = QFontMetrics(font)
    geom = screen.availableSize()
    etext = fm.elidedText(text, elide, geom.width())
    if etext != text:
        text = f"<span>{etext}</span>"
    QToolTip.showText(pos, text, widget, rect)


class _SilhouettePlotTextListWidget(TextListWidget):
    # disable default tooltips, SilhouettePlot handles them
    def helpEvent(self, event: QGraphicsSceneHelpEvent):
        return


class StyledGraphicsView(StickyGraphicsView):
    """
    Propagate style and palette changes to the visualized scene.
    """
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.ensurePolished()
        if self.scene() is not None:
            self.scene().setPalette(self.palette())

    def setScene(self, scene):
        super().setScene(scene)
        if self.scene() is not None:
            self.scene().setPalette(self.palette())

    def changeEvent(self, event):
        if event.type() == QEvent.PaletteChange and \
                self.scene() is not None and self.scene().parent() is self:
            self.scene().setPalette(self.palette())
        super().changeEvent(event)


class SilhouettePlot(QGraphicsWidget):
    """
    A silhouette plot widget.
    """
    #: Emitted when the current selection has changed
    selectionChanged = Signal()

    def __init__(self, parent=None, **kwargs):
        super().__init__(parent, **kwargs)
        self.setAcceptHoverEvents(True)
        self.__groups = []
        self.__rowNamesVisible = True
        self.__barHeight = 3
        self.__selectionRect = None
        self.__selection = np.asarray([], dtype=int)
        self.__selstate = None
        self.__pen = QPen(Qt.NoPen)
        self.__layout = QGraphicsGridLayout()
        self.__hoveredItem = None
        self.__topScale = None     # type: Optional[AxisItem]
        self.__bottomScale = None  # type: Optional[AxisItem]
        self.__layout.setColumnSpacing(0, 1.)
        self.setLayout(self.__layout)
        self.setFocusPolicy(Qt.StrongFocus)

    def setScores(self, scores, labels, values, colors, rownames=None):
        """
        Set the silhouette scores/labels to for display.

        Arguments
        ---------
        scores : (N,) ndarray
            The silhouette scores.
        labels : (N,) ndarray
            A ndarray (dtype=int) of label/clusters indices.
        values : list of str
            A list of label/cluster names.
        colors : (N, 3) ndarray
            A ndarray of RGB values.
        rownames : list of str, optional
            A list (len == N) of row names.
        """
        scores = np.asarray(scores, dtype=float)
        labels = np.asarray(labels, dtype=int)
        if rownames is not None:
            rownames = np.asarray(rownames, dtype=object)

        if not scores.ndim == labels.ndim == 1:
            raise ValueError("scores and labels must be 1 dimensional")
        if scores.shape != labels.shape:
            raise ValueError("scores and labels must have the same shape")
        if rownames is not None and rownames.shape != scores.shape:
            raise ValueError("rownames must have the same size as scores")

        Ck = np.unique(labels)
        if not Ck[0] >= 0 and Ck[-1] < len(values):
            raise ValueError(
                "All indices in `labels` must be in `range(len(values))`")
        cluster_indices = [np.flatnonzero(labels == i)
                           for i in range(len(values))]
        cluster_indices = [indices[np.argsort(scores[indices])[::-1]]
                           for indices in cluster_indices]
        groups = [
            namespace(scores=scores[indices], indices=indices, label=label,
                      rownames=(rownames[indices] if rownames is not None
                                else None),
                      color=color)
            for indices, label, color in zip(cluster_indices, values, colors)
        ]
        self.clear()
        self.__groups = groups
        self.__setup()

    def setRowNames(self, names):
        if names is not None:
            names = np.asarray(names, dtype=object)

        layout = self.__layout
        assert layout is self.layout()

        font = self.font()
        font.setPixelSize(self.__barHeight)

        for i, grp in enumerate(self.__groups):
            grp.rownames = names[grp.indices] if names is not None else None
            item = layout.itemAt(i + 1, 3)
            assert isinstance(item, TextListWidget)
            if grp.rownames is not None:
                item.setItems(grp.rownames)
                item.setVisible(self.__rowNamesVisible)
            else:
                item.setItems([])
                item.setVisible(False)
        layout.activate()

    def setRowNamesVisible(self, visible):
        if self.__rowNamesVisible != visible:
            self.__rowNamesVisible = visible
            for item in self.__textItems():
                item.setVisible(visible)
            self.updateGeometry()

    def rowNamesVisible(self):
        return self.__rowNamesVisible

    def setBarHeight(self, height):
        """
        Set silhouette bar height (row height).
        """
        if height != self.__barHeight:
            self.__barHeight = height
            for item in self.__plotItems():
                item.setPreferredBarSize(height)
            font = self.font()
            font.setPixelSize(height)
            for item in self.__textItems():
                item.setFont(font)

    def barHeight(self):
        """
        Return the silhouette bar (row) height.
        """
        return self.__barHeight

    def clear(self):
        """
        Clear the widget state
        """
        scene = self.scene()
        for child in self.childItems():
            child.setParentItem(None)
            scene.removeItem(child)
        self.__groups = []
        self.__topScale = None
        self.__bottomScale = None

    def __setup(self):
        # Setup the subwidgets/groups/layout
        smax = max((np.nanmax(g.scores) for g in self.__groups
                    if g.scores.size),
                   default=1)
        smax = 1 if np.isnan(smax) else smax

        smin = min((np.nanmin(g.scores) for g in self.__groups
                    if g.scores.size),
                   default=-1)
        smin = -1 if np.isnan(smin) else smin
        smin = min(smin, 0)

        font = self.font()
        font.setPixelSize(self.__barHeight)
        foreground = self.palette().brush(QPalette.Foreground)
        ax = AxisItem(parent=self, orientation="top", maxTickLength=7)
        ax.setRange(smin, smax)
        self.__topScale = ax
        layout = self.__layout
        assert layout is self.layout()
        layout.addItem(ax, 0, 2)

        for i, group in enumerate(self.__groups):
            silhouettegroup = BarPlotItem(parent=self)
            silhouettegroup.setBrush(QBrush(QColor(*group.color)))
            silhouettegroup.setPen(self.__pen)
            silhouettegroup.setDataRange(smin, smax)
            silhouettegroup.setPlotData(group.scores)
            silhouettegroup.setPreferredBarSize(self.__barHeight)
            silhouettegroup.setData(0, group.indices)
            layout.addItem(silhouettegroup, i + 1, 2)

            if group.label:
                layout.addItem(Line(orientation=Qt.Vertical), i + 1, 1)
                label = QGraphicsSimpleTextItem(
                    "{} ({})".format(group.label, len(group.scores)), self
                )
                label.setBrush(foreground)
                label.setPen(QPen(Qt.NoPen))
                label.setRotation(-90)
                item = SimpleLayoutItem(
                    label,
                    anchor=(0., 1.0),
                    anchorItem=(0., 0.),
                )
                item.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
                layout.addItem(item, i + 1, 0, Qt.AlignCenter)

            textlist = _SilhouettePlotTextListWidget(
                self, font=font, elideMode=Qt.ElideRight,
                alignment=Qt.AlignLeft | Qt.AlignVCenter
            )
            textlist.setMaximumWidth(750)
            textlist.setFlag(TextListWidget.ItemClipsChildrenToShape, False)
            sp = textlist.sizePolicy()
            sp.setVerticalPolicy(QSizePolicy.Ignored)
            textlist.setSizePolicy(sp)
            if group.rownames is not None:
                textlist.setItems(group.items)
                textlist.setVisible(self.__rowNamesVisible)
            else:
                textlist.setVisible(False)

            layout.addItem(textlist, i + 1, 3)

        ax = AxisItem(parent=self, orientation="bottom", maxTickLength=7)
        ax.setRange(smin, smax)
        self.__bottomScale = ax
        layout.addItem(ax, len(self.__groups) + 1, 2)

    def topScaleItem(self):
        # type: () -> Optional[QGraphicsWidget]
        return self.__topScale

    def bottomScaleItem(self):
        # type: () -> Optional[QGraphicsWidget]
        return self.__bottomScale

    def __updateSizeConstraints(self):
        # set/update fixed height constraint on the text annotation items so
        # it matches the silhouette's height
        for silitem, textitem in zip(self.__plotItems(), self.__textItems()):
            height = silitem.effectiveSizeHint(Qt.PreferredSize).height()
            textitem.setMaximumHeight(height)
            textitem.setMinimumHeight(height)
        mwidth = max((silitem.effectiveSizeHint(Qt.PreferredSize).width()
                     for silitem in self.__plotItems()), default=300)
        # match the AxisItem's width to the bars
        for axis in self.__axisItems():
            axis.setMaximumWidth(mwidth)
            axis.setMinimumWidth(mwidth)

    def changeEvent(self, event: QEvent) -> None:
        if event.type() == QEvent.PaletteChange:
            brush = self.palette().brush(QPalette.Text)
            labels = [it for it in self.childItems()
                      if isinstance(it, QGraphicsSimpleTextItem)]
            apply_all(labels, lambda it: it.setBrush(brush))
        super().changeEvent(event)

    def event(self, event: QEvent) -> bool:
        # Reimplemented
        if event.type() == QEvent.LayoutRequest and \
                self.parentLayoutItem() is None:
            self.__updateSizeConstraints()
            self.resize(self.effectiveSizeHint(Qt.PreferredSize))
        elif event.type() == QEvent.GraphicsSceneHelp:
            self.helpEvent(cast(QGraphicsSceneHelpEvent, event))
            if event.isAccepted():
                return True
        return super().event(event)

    def helpEvent(self, event: QGraphicsSceneHelpEvent):
        pos = self.mapFromScene(event.scenePos())
        item = self.__itemDataAtPos(pos)
        if item is None:
            return
        data, index, rect = item
        if data.rownames is None:
            return
        ttip = data.rownames[index]
        if ttip:
            view = event.widget().parentWidget()
            rect = view.mapFromScene(self.mapToScene(rect)).boundingRect()
            show_tool_tip(event.screenPos(), ttip, event.widget(), rect)
            event.setAccepted(True)

    def __setHoveredItem(self, item):
        # Set the current hovered `item` (:class:`QGraphicsRectItem`)
        if self.__hoveredItem is not item:
            if self.__hoveredItem is not None:
                self.__hoveredItem.setPen(QPen(Qt.NoPen))
            self.__hoveredItem = item
            if item is not None:
                item.setPen(QPen(Qt.lightGray))

    def hoverEnterEvent(self, event):
        # Reimplemented
        event.accept()

    def hoverMoveEvent(self, event):
        # Reimplemented
        event.accept()
        item = self.itemAtPos(event.pos())
        self.__setHoveredItem(item)

    def hoverLeaveEvent(self, event):
        # Reimplemented
        self.__setHoveredItem(None)
        event.accept()

    def mousePressEvent(self, event):
        # Reimplemented
        if event.button() == Qt.LeftButton:
            if event.modifiers() & Qt.ControlModifier:
                saction = SelectAction.Toogle
            elif event.modifiers() & Qt.AltModifier:
                saction = SelectAction.Deselect
            elif event.modifiers() & Qt.ShiftModifier:
                saction = SelectAction.Select
            else:
                saction = SelectAction.Clear | SelectAction.Select
            self.__selstate = namespace(
                modifiers=event.modifiers(),
                selection=self.__selection,
                action=saction,
                rect=None,
            )
            if saction & SelectAction.Clear:
                self.__selstate.selection = np.array([], dtype=int)
                self.setSelection(self.__selstate.selection)
            event.accept()

    def keyPressEvent(self, event):
        if event.key() in (Qt.Key_Up, Qt.Key_Down):
            if event.key() == Qt.Key_Up:
                self.__move_selection(self.selection(), -1)
            elif event.key() == Qt.Key_Down:
                self.__move_selection(self.selection(), 1)
            event.accept()
            return
        super().keyPressEvent(event)

    def mouseMoveEvent(self, event):
        # Reimplemented
        if event.buttons() & Qt.LeftButton:
            assert self.__selstate is not None
            if self.__selectionRect is None:
                self.__selectionRect = QGraphicsRectItem(self)

            rect = (QRectF(event.buttonDownPos(Qt.LeftButton),
                           event.pos()).normalized())

            if not rect.width():
                rect = rect.adjusted(-1e-7, -1e-7, 1e-7, 1e-7)

            rect = rect.intersected(self.contentsRect())
            self.__selectionRect.setRect(rect)
            self.__selstate.rect = rect
            self.__selstate.action |= SelectAction.Current

            self.__setSelectionRect(rect, self.__selstate.action)
            event.accept()

    def mouseReleaseEvent(self, event):
        # Reimplemented
        if event.button() == Qt.LeftButton:
            if self.__selectionRect is not None:
                self.__selectionRect.setParentItem(None)
                if self.scene() is not None:
                    self.scene().removeItem(self.__selectionRect)
                self.__selectionRect = None
            event.accept()

            rect = (QRectF(event.buttonDownPos(Qt.LeftButton), event.pos())
                    .normalized())

            if not rect.isValid():
                rect = rect.adjusted(-1e-7, -1e-7, 1e-7, 1e-7)

            rect = rect.intersected(self.contentsRect())
            action = self.__selstate.action & ~SelectAction.Current
            self.__setSelectionRect(rect, action)
            self.__selstate = None

    def __move_selection(self, selection, offset):
        ids = np.asarray([pi.data(0) for pi in self.__plotItems()]).ravel()
        indices = [np.where(ids == i)[0] for i in selection]
        indices = np.asarray(indices) + offset
        if min(indices) >= 0 and max(indices) < len(ids):
            self.setSelection(ids[indices])

    def __setSelectionRect(self, rect, action):
        # Set the current mouse drag selection rectangle
        if not rect.isValid():
            rect = rect.adjusted(-0.01, -0.01, 0.01, 0.01)

        rect = rect.intersected(self.contentsRect())

        indices = self.__selectionIndices(rect)

        if action & SelectAction.Clear:
            selection = []
        elif self.__selstate is not None:
            # Mouse drag selection is in progress. Update only the current
            # selection
            selection = self.__selstate.selection
        else:
            selection = self.__selection

        if action & SelectAction.Toogle:
            selection = np.setxor1d(selection, indices)
        elif action & SelectAction.Deselect:
            selection = np.setdiff1d(selection, indices)
        elif action & SelectAction.Select:
            selection = np.union1d(selection, indices)

        self.setSelection(selection)

    def __selectionIndices(self, rect):
        items = [item for item in self.__plotItems()
                 if item.geometry().intersects(rect)]
        selection = [np.array([], dtype=int)]
        for item in items:
            indices = item.data(0)
            itemrect = item.geometry().intersected(rect)
            crect = item.contentsRect()
            itemrect = (item.mapFromParent(itemrect).boundingRect()
                        .intersected(crect))
            assert itemrect.top() >= 0
            rowh = crect.height() / item.count()
            indextop = np.floor(itemrect.top() / rowh)
            indexbottom = np.ceil(itemrect.bottom() / rowh)
            selection.append(indices[int(indextop): int(indexbottom)])
        return np.hstack(selection)

    def itemAtPos(self, pos):
        items = [item for item in self.__plotItems()
                 if item.geometry().contains(pos)]
        if not items:
            return None
        else:
            item = items[0]
        crect = item.contentsRect()
        pos = item.mapFromParent(pos)
        if not crect.contains(pos):
            return None

        assert pos.x() >= 0
        rowh = crect.height() / item.count()
        index = int(np.floor(pos.y() / rowh))
        index = min(index, item.count() - 1)
        if index >= 0:
            return item.items()[index]
        else:
            return None

    def __itemDataAtPos(self, pos) -> Optional[Tuple[namespace, int, QRectF]]:
        items = [(sitem, tlist, grp) for sitem, tlist, grp
                 in zip(self.__plotItems(), self.__textItems(), self.__groups)
                 if sitem.geometry().contains(pos) or tlist.isVisible()
                 and tlist.geometry().contains(pos)]
        if not items:
            return None
        else:
            sitem, _, grp = items[0]
        indices = grp.indices
        assert (isinstance(indices, np.ndarray) and
                indices.shape == (sitem.count(),))
        crect = sitem.contentsRect()
        pos = sitem.mapFromParent(pos)
        if not crect.top() <= pos.y() <= crect.bottom():
            return None
        rowh = crect.height() / sitem.count()
        index = int(np.floor(pos.y() / rowh))
        index = min(index, indices.size - 1)
        baritem = sitem.items()[index]
        rect = self.mapRectFromItem(baritem, baritem.rect())
        crect = self.contentsRect()
        rect.setLeft(crect.left())
        rect.setRight(crect.right())
        return grp, index, rect

    def __selectionChanged(self, selected, deselected):
        for item, grp in zip(self.__plotItems(), self.__groups):
            select = np.flatnonzero(
                np.in1d(grp.indices, selected, assume_unique=True))
            items = item.items()
            if select.size:
                for i in select:
                    color = np.hstack((grp.color, np.array([130])))
                    items[i].setBrush(QBrush(QColor(*color)))

            deselect = np.flatnonzero(
                np.in1d(grp.indices, deselected, assume_unique=True))
            if deselect.size:
                for i in deselect:
                    items[i].setBrush(QBrush(QColor(*grp.color)))

    def __plotItems(self):
        for i in range(len(self.__groups)):
            item = self.layout().itemAt(i + 1, 2)
            if item is not None:
                assert isinstance(item, BarPlotItem)
                yield item

    def __textItems(self):
        for i in range(len(self.__groups)):
            item = self.layout().itemAt(i + 1, 3)
            if item is not None:
                assert isinstance(item, TextListWidget)
                yield item

    def __axisItems(self):
        return self.__topScale, self.__bottomScale

    def setSelection(self, indices):
        indices = np.unique(np.asarray(indices, dtype=int))
        select = np.setdiff1d(indices, self.__selection)
        deselect = np.setdiff1d(self.__selection, indices)

        self.__selectionChanged(select, deselect)

        self.__selection = indices

        if deselect.size or select.size:
            self.selectionChanged.emit()

    def selection(self):
        return np.asarray(self.__selection, dtype=int)


class Line(QGraphicsWidget):
    """
    A line separator graphics widget
    """
    def __init__(self, parent=None, orientation=Qt.Horizontal, **kwargs):
        sizePolicy = kwargs.pop("sizePolicy", None)
        super().__init__(None, **kwargs)
        self.__orientation = Qt.Horizontal
        if sizePolicy is None:
            sizePolicy = QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
            sizePolicy.setControlType(QSizePolicy.Frame)
            self.setSizePolicy(sizePolicy)
        else:
            self.setSizePolicy(sizePolicy)

        self.setOrientation(orientation)

        if parent is not None:
            self.setParentItem(parent)

    def setOrientation(self, orientation):
        if self.__orientation != orientation:
            self.__orientation = orientation
            sp = self.sizePolicy()
            if orientation == Qt.Vertical:
                sp.setVerticalPolicy(QSizePolicy.Expanding)
                sp.setHorizontalPolicy(QSizePolicy.Fixed)
            else:
                sp.setVerticalPolicy(QSizePolicy.Fixed)
                sp.setHorizontalPolicy(QSizePolicy.Expanding)
            self.setSizePolicy(sp)
            self.updateGeometry()

    def sizeHint(self, which, constraint=QRectF()):
        # type: (Qt.SizeHint, QSizeF) -> QSizeF
        pw = 1.
        sh = QSizeF()
        if which == Qt.MinimumSize:
            sh = QSizeF(pw, pw)
        elif which == Qt.PreferredSize:
            sh = QSizeF(pw, 30.)
        elif which == Qt.MaximumSize:
            sh = QSizeF(pw, QWIDGETSIZE_MAX)

        if self.__orientation == Qt.Horizontal:
            sh.transpose()  # Qt4 compatible
        return sh

    def paint(self, painter, option, widget=None):
        # type: (QPainter, QStyleOptionGraphicsItem, Optional[QWidget]) -> None
        palette = self.palette()  # type: QPalette
        color = palette.color(QPalette.Foreground)
        painter.setPen(QPen(color, 1))
        rect = self.contentsRect()
        center = rect.center()
        if self.__orientation == Qt.Vertical:
            p1 = QPointF(center.x(), rect.top())
            p2 = QPointF(center.x(), rect.bottom())
        elif self.__orientation == Qt.Horizontal:
            p1 = QPointF(rect.left(), center.y())
            p2 = QPointF(rect.right(), center.y())
        else:
            assert False
        painter.drawLine(p1, p2)


class BarPlotItem(QGraphicsWidget):
    def __init__(self, parent=None, **kwargs):
        super().__init__(parent, **kwargs)
        self.__barsize = 5
        self.__spacing = 1
        self.__pen = QPen(Qt.NoPen)
        self.__brush = QBrush(QColor("#3FCFCF"))
        self.__range = (0., 1.)
        self.__data = np.array([], dtype=float)
        self.__items = []

    def count(self):
        return self.__data.size

    def items(self):
        return list(self.__items)

    def setGeometry(self, geom):
        super().setGeometry(geom)
        self.__layout()

    def event(self, event):
        if event.type() == QEvent.GraphicsSceneResize:
            self.__layout()
        return super().event(event)

    def sizeHint(self, which, constraint=QSizeF()):
        spacing = max(self.__spacing * (self.count() - 1), 0)
        return QSizeF(300, self.__barsize * self.count() + spacing)

    def setPreferredBarSize(self, size):
        if self.__barsize != size:
            self.__barsize = size
            self.updateGeometry()

    def spacing(self):
        return self.__spacing

    def setSpacing(self, spacing):
        if self.__spacing != spacing:
            self.__spacing = spacing
            self.updateGeometry()

    def setPen(self, pen):
        pen = QPen(pen)
        if self.__pen != pen:
            self.__pen = pen
            for item in self.__items:
                item.setPen(pen)

    def pen(self):
        return QPen(self.__pen)

    def setBrush(self, brush):
        brush = QBrush(brush)
        if self.__brush != brush:
            self.__brush = brush
            for item in self.__items:
                item.setBrush(brush)

    def brush(self):
        return QBrush(self.__brush)

    def setPlotData(self, values):
        self.__data = np.array(values, copy=True)
        self.__update()
        self.updateGeometry()

    def setDataRange(self, rangemin, rangemax):
        if self.__range != (rangemin, rangemax):
            self.__range = (rangemin, rangemax)
            self.__layout()

    def __clear(self):
        for item in self.__items:
            item.setParentItem(None)
        scene = self.scene()
        if scene is not None:
            for item in self.__items:
                scene.removeItem(item)
        self.__items = []

    def __update(self):
        self.__clear()

        pen = self.pen()
        brush = self.brush()
        for _ in range(self.count()):
            item = QGraphicsRectItem(self)
            item.setPen(pen)
            item.setBrush(brush)
            self.__items.append(item)

        self.__layout()

    def __layout(self):
        (N, ) = self.__data.shape
        if not N:
            return

        spacing = self.__spacing
        rect = self.contentsRect()
        w = rect.width()
        if rect.height() - (spacing * (N - 1)) <= 0:
            spacing = 0

        h = (rect.height() - (spacing * (N - 1))) / N
        xmin, xmax = self.__range
        span = xmax - xmin
        if span < 1e-9:
            span = 1
        scalef = w * 1 / span

        base = 0
        base = (base - xmin) * scalef
        datascaled = (self.__data - xmin) * scalef

        for i, (v, item) in enumerate(zip(datascaled, self.__items)):
            item.setRect(QRectF(base, rect.top() + i * (h + spacing),
                                v - base, h).normalized())


if __name__ == "__main__":  # pragma: no cover
    WidgetPreview(OWSilhouettePlot).run(Orange.data.Table("brown-selected"))

```
--- 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:** Context Migration Fails Due to Missing `class_vars` Attribute

**Description:**
When attempting to migrate context for the `OWSilhouettePlot` widget, an error occurs if the `Context` object lacks the `class_vars` attribute. This issue arises during the migration process for contexts with version less than 2, leading to an `AttributeError`.

**Example Buggy Code:**
```python
def test_migration_failure():
    enc_domain = {
        'attributes': (('foo', 1), ('bar', 1), ('baz', 1), ('bax', 1), ('cfoo', 1), ('mbaz', 1))
    }
    context = Context(
        values={'cluster_var_idx': (0, -2), 'annotation_var_idx': (0, -2)},
        **enc_domain
    )
    OWSilhouettePlot.migrate_context(context, 1)
```
In this test, the `Context` object is initialized without the `class_vars` attribute. When `migrate_context` is called with version `1`, it attempts to access `context.class_vars`, which does not exist, resulting in an error.

**Expected Behavior:**
The context migration should handle cases where certain attributes like `class_vars` or `metas` are missing from the `Context` object. Instead of raising an error, it should gracefully migrate the available attributes and maintain the integrity of the context values.

**Actual Behavior:**
An `AttributeError` is raised:
```
AttributeError: 'Context' object has no attribute 'class_vars'
```
This prevents the successful migration of the context, potentially disrupting the workflow for users relying on older context versions.

[/ISSUE]
{
    "TestOWSilhouettePlot.test_bad_data_range": "PASSED",
    "TestOWSilhouettePlot.test_distance_input": "PASSED",
    "TestOWSilhouettePlot.test_ignore_categorical": "PASSED",
    "TestOWSilhouettePlot.test_image_export": "PASSED",
    "TestOWSilhouettePlot.test_input_distance_no_data": "PASSED",
    "TestOWSilhouettePlot.test_insufficient_clusters": "PASSED",
    "TestOWSilhouettePlot.test_memory_error": "PASSED",
    "TestOWSilhouettePlot.test_meta_object_dtype": "PASSED",
    "TestOWSilhouettePlot.test_migration": "PASSED",
    "TestOWSilhouettePlot.test_minimum_size": "PASSED",
    "TestOWSilhouettePlot.test_msg_base_class": "PASSED",
    "TestOWSilhouettePlot.test_nan_distances": "PASSED",
    "TestOWSilhouettePlot.test_no_data": "PASSED",
    "TestOWSilhouettePlot.test_no_group_var": "PASSED",
    "TestOWSilhouettePlot.test_outputs": "PASSED",
    "TestOWSilhouettePlot.test_outputs_add_scores": "PASSED",
    "TestOWSilhouettePlot.test_report": "PASSED",
    "TestOWSilhouettePlot.test_saved_selection": "PASSED",
    "TestOWSilhouettePlot.test_unique_output_domain": "PASSED",
    "TestOWSilhouettePlot.test_unknowns_in_labels": "PASSED"
}
diff --git a/Orange/widgets/visualize/owsilhouetteplot.py b/Orange/widgets/visualize/owsilhouetteplot.py
index 82b0b33d8..3a4534164 100644
--- a/Orange/widgets/visualize/owsilhouetteplot.py
+++ b/Orange/widgets/visualize/owsilhouetteplot.py
@@ -267,7 +267,7 @@ class OWSilhouettePlot(widget.OWWidget):
                      if var.is_string or var.is_discrete]
         self.annotation_var_model[:] = [None] + annotvars
         self.annotation_var = annotvars[0] if annotvars else None
-        self.openContext(Orange.data.Domain(groupvars))
+        self.openContext(domain)
 
     def _is_empty(self) -> bool:
         # Is empty (does not have any input).
@@ -539,27 +539,22 @@ class OWSilhouettePlot(widget.OWWidget):
     def migrate_context(cls, context, version):
         values = context.values
         if version < 2:
-            discrete_vars = (
-                name
-                for name, type_ in itertools.chain(
-                    context.attributes, context.class_vars, context.metas)
-                if type_ == 1)
+            # contexts were constructed from Domain containing vars shown in
+            # the list view, context.class_vars and context.metas were always
+            # empty, and context.attributes contained discrete attributes
             index, _ = values.pop("cluster_var_idx")
-            name = next(itertools.islice(discrete_vars, index, None)
-            )
-            values["cluster_var"] = (name, 101)
+            values["cluster_var"] = (context.attributes[index][0], 101)
 
             index = values.pop("annotation_var_idx")[0] - 1
             if index == -1:
                 values["annotation_var"] = None
-            else:
-                annot_vars = (
-                    (name, type_ + 100)
-                    for name, type_ in itertools.chain(
-                        context.attributes, context.class_vars, context.metas)
-                    if type_ in (1, 3))
-                values["annotation_var"] = next(
-                    itertools.islice(annot_vars, index, None))
+            elif index < len(context.attributes):
+                name, _ = context.attributes[index]
+                values["annotation_var"] = (name, 101)
+            # else we cannot migrate
+            # Even this migration can be erroneous if metas contained a mixture
+            # of discrete and string attributes; the latter were not stored in
+            # context, so indices in context could have been wrong
 
 
 class SelectAction(enum.IntEnum):

{"Orange/widgets/visualize/owsilhouetteplot.py": "import itertools\nimport enum\nfrom xml.sax.saxutils import escape\nfrom types import SimpleNamespace as namespace\n\nfrom typing import Optional, Union, Tuple, cast\n\nimport numpy as np\nimport sklearn.metrics\n\nfrom AnyQt.QtWidgets import (\n    QGraphicsWidget, QGraphicsGridLayout,\n    QGraphicsRectItem, QStyleOptionGraphicsItem, QSizePolicy, QWidget,\n    QVBoxLayout, QGraphicsSimpleTextItem, QWIDGETSIZE_MAX,\n    QGraphicsSceneHelpEvent, QToolTip, QApplication,\n)\nfrom AnyQt.QtGui import (\n    QColor, QPen, QBrush, QPainter, QFontMetrics, QPalette,\n)\nfrom AnyQt.QtCore import (\n    Qt, QEvent, QRectF, QSizeF, QSize, QPointF, QPoint, QRect\n)\nfrom AnyQt.QtCore import pyqtSignal as Signal\n\nimport Orange.data\nfrom Orange.data.util import get_unique_names\nimport Orange.distance\nimport Orange.misc\nfrom Orange.data import Table, Domain\nfrom Orange.misc import DistMatrix\n\nfrom Orange.widgets import widget, gui, settings\nfrom Orange.widgets.utils.graphicsscene import GraphicsScene\nfrom Orange.widgets.utils.stickygraphicsview import StickyGraphicsView\nfrom Orange.widgets.utils import itemmodels, apply_all\nfrom Orange.widgets.utils.annotated_data import (create_annotated_table,\n                                                 ANNOTATED_DATA_SIGNAL_NAME)\nfrom Orange.widgets.utils.graphicstextlist import TextListWidget\nfrom Orange.widgets.utils.graphicslayoutitem import SimpleLayoutItem\nfrom Orange.widgets.utils.sql import check_sql_input\nfrom Orange.widgets.utils.widgetpreview import WidgetPreview\nfrom Orange.widgets.visualize.utils.plotutils import AxisItem\nfrom Orange.widgets.widget import Msg, Input, Output\n\n\nclass InputValidationError(ValueError):\n    message: str\n\n\nclass NoGroupVariable(InputValidationError):\n    message = \"Input does not have any suitable labels\"\n\n\nclass ValidationError(InputValidationError):\n    def __init__(self, message: str):\n        super().__init__(message)\n        self.message = message\n\n\nclass OWSilhouettePlot(widget.OWWidget):\n    name = \"Silhouette Plot\"\n    description = \"Visually assess cluster quality and \" \\\n                  \"the degree of cluster membership.\"\n\n    icon = \"icons/SilhouettePlot.svg\"\n    priority = 300\n    keywords = []\n\n    class Inputs:\n        data = Input(\"Data\", (Orange.data.Table, Orange.misc.DistMatrix))\n\n    class Outputs:\n        selected_data = Output(\"Selected Data\", Orange.data.Table, default=True)\n        annotated_data = Output(ANNOTATED_DATA_SIGNAL_NAME, Orange.data.Table)\n\n    replaces = [\n        \"orangecontrib.prototypes.widgets.owsilhouetteplot.OWSilhouettePlot\",\n        \"Orange.widgets.unsupervised.owsilhouetteplot.OWSilhouettePlot\"\n    ]\n\n    settingsHandler = settings.DomainContextHandler()\n    settings_version = 2\n\n    #: Distance metric index\n    distance_idx = settings.Setting(0)\n    #: Group/cluster variable\n    cluster_var = settings.ContextSetting(None)\n    #: Annotation variable\n    annotation_var = settings.ContextSetting(None)\n    #: Group the (displayed) silhouettes by cluster\n    group_by_cluster = settings.Setting(True)\n    #: A fixed size for an instance bar\n    bar_size = settings.Setting(3)\n    #: Add silhouette scores to output data\n    auto_commit = settings.Setting(True)\n\n    pending_selection = settings.Setting(None, schema_only=True)\n\n    Distances = [(\"Euclidean\", Orange.distance.Euclidean),\n                 (\"Manhattan\", Orange.distance.Manhattan),\n                 (\"Cosine\", Orange.distance.Cosine)]\n\n    graph_name = \"scene\"\n\n    class Error(widget.OWWidget.Error):\n        need_two_clusters = Msg(\"Need at least two non-empty clusters\")\n        singleton_clusters_all = Msg(\"All clusters are singletons\")\n        memory_error = Msg(\"Not enough memory\")\n        value_error = Msg(\"Distances could not be computed: '{}'\")\n        input_validation_error = Msg(\"{}\")\n\n    class Warning(widget.OWWidget.Warning):\n        missing_cluster_assignment = Msg(\n            \"{} instance{s} omitted (missing cluster assignment)\")\n        nan_distances = Msg(\"{} instance{s} omitted (undefined distances)\")\n        ignoring_categorical = Msg(\"Ignoring categorical features\")\n\n    def __init__(self):\n        super().__init__()\n        #: The input data\n        self.data = None         # type: Optional[Orange.data.Table]\n        #: The input distance matrix (if present)\n        self.distances = None  # type: Optional[Orange.misc.DistMatrix]\n        #: The effective distance matrix (is self.distances or computed from\n        #: self.data depending on input)\n        self._matrix = None      # type: Optional[Orange.misc.DistMatrix]\n        #: An bool mask (size == len(data)) indicating missing group/cluster\n        #: assignments\n        self._mask = None        # type: Optional[np.ndarray]\n        #: An array of cluster/group labels for instances with valid group\n        #: assignment\n        self._labels = None      # type: Optional[np.ndarray]\n        #: An array of silhouette scores for instances with valid group\n        #: assignment\n        self._silhouette = None  # type: Optional[np.ndarray]\n        self._silplot = None     # type: Optional[SilhouettePlot]\n\n        controllayout = self.controlArea.layout()\n        assert isinstance(controllayout, QVBoxLayout)\n        self._distances_gui_box = distbox = gui.widgetBox(\n            None, \"Distance\"\n        )\n        self._distances_gui_cb = gui.comboBox(\n            distbox, self, \"distance_idx\",\n            items=[name for name, _ in OWSilhouettePlot.Distances],\n            orientation=Qt.Horizontal, callback=self._invalidate_distances)\n        controllayout.addWidget(distbox)\n\n        box = gui.vBox(self.controlArea, \"Grouping\")\n        self.cluster_var_model = itemmodels.VariableListModel(\n            parent=self, placeholder=\"(None)\")\n        self.cluster_var_cb = gui.comboBox(\n            box, self, \"cluster_var\", contentsLength=14,\n            searchable=True, callback=self._invalidate_scores,\n            model=self.cluster_var_model\n        )\n        gui.checkBox(\n            box, self, \"group_by_cluster\", \"Show in groups\",\n            callback=self._replot)\n\n        box = gui.vBox(self.controlArea, \"Bars\")\n        gui.widgetLabel(box, \"Bar width:\")\n        gui.hSlider(\n            box, self, \"bar_size\", minValue=1, maxValue=10, step=1,\n            callback=self._update_bar_size)\n        gui.widgetLabel(box, \"Annotations:\")\n        self.annotation_var_model = itemmodels.VariableListModel(parent=self)\n        self.annotation_var_model[:] = [None]\n        self.annotation_cb = gui.comboBox(\n            box, self, \"annotation_var\", contentsLength=14,\n            callback=self._update_annotations,\n            model=self.annotation_var_model)\n        ibox = gui.indentedBox(box, 5)\n        self.ann_hidden_warning = warning = gui.widgetLabel(\n            ibox, \"(increase the width to show)\")\n        ibox.setFixedWidth(ibox.sizeHint().width())\n        warning.setVisible(False)\n\n        gui.rubber(self.controlArea)\n\n        gui.auto_send(self.buttonsArea, self, \"auto_commit\")\n\n        self.scene = GraphicsScene(self)\n        self.view = StyledGraphicsView(self.scene)\n        self.view.setRenderHint(QPainter.Antialiasing, True)\n        self.view.setAlignment(Qt.AlignTop | Qt.AlignLeft)\n        self.mainArea.layout().addWidget(self.view)\n\n        self.settingsAboutToBePacked.connect(self.pack_settings)\n\n    def sizeHint(self):\n        sh = self.controlArea.sizeHint()\n        return sh.expandedTo(QSize(600, 720))\n\n    def pack_settings(self):\n        if self.data and self._silplot is not None:\n            self.pending_selection = list(self._silplot.selection())\n        else:\n            self.pending_selection = None\n\n    @Inputs.data\n    @check_sql_input\n    def set_data(self, data: Union[Table, DistMatrix, None]):\n        \"\"\"\n        Set the input dataset or distance matrix.\n        \"\"\"\n        self.closeContext()\n        self.clear()\n        try:\n            if isinstance(data, Orange.misc.DistMatrix):\n                self._set_distances(data)\n            elif isinstance(data, Orange.data.Table):\n                self._set_table(data)\n            else:\n                self.distances = None\n                self.data = None\n        except InputValidationError as err:\n            self.Error.input_validation_error(err.message)\n            self.distances = None\n            self.data = None\n\n    def _set_table(self, data: Table):\n        self._setup_control_models(data.domain)\n        self.data = data\n        self.distances = None\n\n    def _set_distances(self, distances: DistMatrix):\n        if isinstance(distances.row_items, Orange.data.Table) and \\\n                distances.axis == 1:\n            data = distances.row_items\n        else:\n            raise ValidationError(\"Input matrix does not have associated data\")\n\n        if data is not None:\n            self._setup_control_models(data.domain)\n            self.distances = distances\n            self.data = data\n\n    def handleNewSignals(self):\n        if not self._is_empty():\n            self._update()\n            self._replot()\n            if self.pending_selection is not None and self._silplot is not None:\n                # If selection contains indices that are too large, the data\n                # file must had been modified, so we ignore selection\n                if max(self.pending_selection, default=-1) < len(self.data):\n                    self._silplot.setSelection(np.array(self.pending_selection))\n                self.pending_selection = None\n\n        # Disable/enable the Distances GUI controls if applicable\n        self._distances_gui_box.setEnabled(self.distances is None)\n\n        self.commit.now()\n\n    def _setup_control_models(self, domain: Domain):\n        groupvars = [\n            v for v in domain.variables + domain.metas\n            if v.is_discrete and len(v.values) >= 2]\n        if not groupvars:\n            raise NoGroupVariable()\n        self.cluster_var_model[:] = groupvars\n        if domain.class_var in groupvars:\n            self.cluster_var = domain.class_var\n        else:\n            self.cluster_var = groupvars[0]\n        annotvars = [var for var in domain.variables + domain.metas\n                     if var.is_string or var.is_discrete]\n        self.annotation_var_model[:] = [None] + annotvars\n        self.annotation_var = annotvars[0] if annotvars else None\n        self.openContext(Orange.data.Domain(groupvars))\n\n    def _is_empty(self) -> bool:\n        # Is empty (does not have any input).\n        return (self.data is None or len(self.data) == 0) \\\n               and self.distances is None\n\n    def clear(self):\n        \"\"\"\n        Clear the widget state.\n        \"\"\"\n        self.data = None\n        self.distances = None\n        self._matrix = None\n        self._mask = None\n        self._silhouette = None\n        self._labels = None\n        self.cluster_var_model[:] = []\n        self.annotation_var_model[:] = [None]\n        self._clear_scene()\n        self.Error.clear()\n        self.Warning.clear()\n\n    def _clear_scene(self):\n        # Clear the graphics scene and associated objects\n        self.scene.clear()\n        self.scene.setSceneRect(QRectF())\n        self.view.setSceneRect(QRectF())\n        self.view.setHeaderSceneRect(QRectF())\n        self.view.setFooterSceneRect(QRectF())\n        self._silplot = None\n\n    def _invalidate_distances(self):\n        # Invalidate the computed distance matrix and recompute the silhouette.\n        self._matrix = None\n        self._invalidate_scores()\n\n    def _invalidate_scores(self):\n        # Invalidate and recompute the current silhouette scores.\n        self._labels = self._silhouette = self._mask = None\n        self._update()\n        self._replot()\n        if self.data is not None:\n            self.commit.deferred()\n\n    def _ensure_matrix(self):\n        # ensure self._matrix is computed if necessary\n        if self._is_empty():\n            return\n        if self._matrix is None:\n            if self.distances is not None:\n                self._matrix = np.asarray(self.distances)\n            elif self.data is not None:\n                data = self.data\n                _, metric = self.Distances[self.distance_idx]\n                if not metric.supports_discrete and any(\n                        a.is_discrete for a in data.domain.attributes):\n                    self.Warning.ignoring_categorical()\n                    data = Orange.distance.remove_discrete_features(data)\n                try:\n                    self._matrix = np.asarray(metric(data))\n                except MemoryError:\n                    self.Error.memory_error()\n                    return\n                except ValueError as err:\n                    self.Error.value_error(str(err))\n                    return\n            else:\n                assert False, \"invalid state\"\n\n    def _update(self):\n        # Update/recompute the effective distances and scores as required.\n        self._clear_messages()\n        if self._is_empty():\n            self._reset_all()\n            return\n\n        self._ensure_matrix()\n        if self._matrix is None:\n            return\n\n        labels, _ = self.data.get_column_view(self.cluster_var)\n        labels = np.asarray(labels, dtype=float)\n        cluster_mask = np.isnan(labels)\n        dist_mask = np.isnan(self._matrix).all(axis=0)\n        mask = cluster_mask | dist_mask\n        labels = labels.astype(int)\n        labels = labels[~mask]\n\n        labels_unq = np.unique(labels)\n\n        if len(labels_unq) < 2:\n            self.Error.need_two_clusters()\n            labels = silhouette = mask = None\n        elif len(labels_unq) == len(labels):\n            self.Error.singleton_clusters_all()\n            labels = silhouette = mask = None\n        else:\n            silhouette = sklearn.metrics.silhouette_samples(\n                self._matrix[~mask, :][:, ~mask], labels, metric=\"precomputed\")\n        self._mask = mask\n        self._labels = labels\n        self._silhouette = silhouette\n\n        if mask is not None:\n            count_missing = np.count_nonzero(cluster_mask)\n            if count_missing:\n                self.Warning.missing_cluster_assignment(\n                    count_missing, s=\"s\" if count_missing > 1 else \"\")\n            count_nandist = np.count_nonzero(dist_mask)\n            if count_nandist:\n                self.Warning.nan_distances(\n                    count_nandist, s=\"s\" if count_nandist > 1 else \"\")\n\n    def _reset_all(self):\n        self._mask = None\n        self._silhouette = None\n        self._labels = None\n        self._matrix = None\n        self._clear_scene()\n\n    def _clear_messages(self):\n        self.Error.clear()\n        self.Warning.clear()\n\n    def _set_bar_height(self):\n        visible = self.bar_size >= 5\n        self._silplot.setBarHeight(self.bar_size)\n        self._silplot.setRowNamesVisible(visible)\n        self.ann_hidden_warning.setVisible(\n            not visible and self.annotation_var is not None)\n\n    def _replot(self):\n        # Clear and replot/initialize the scene\n        self._clear_scene()\n        if self._silhouette is not None and self._labels is not None:\n            self._silplot = silplot = SilhouettePlot()\n            self._set_bar_height()\n\n            if self.group_by_cluster:\n                silplot.setScores(\n                    self._silhouette, self._labels,\n                    self.cluster_var.values, self.cluster_var.colors)\n            else:\n                silplot.setScores(\n                    self._silhouette,\n                    np.zeros(len(self._silhouette), dtype=int),\n                    [\"\"], np.array([[63, 207, 207]])\n                )\n\n            self.scene.addItem(silplot)\n            self._update_annotations()\n            silplot.selectionChanged.connect(self.commit.deferred)\n            silplot.layout().activate()\n            self._update_scene_rect()\n            silplot.geometryChanged.connect(self._update_scene_rect)\n\n    def _update_bar_size(self):\n        if self._silplot is not None:\n            self._set_bar_height()\n\n    def _update_annotations(self):\n        annot_var = self.annotation_var\n        self.ann_hidden_warning.setVisible(\n            self.bar_size < 5 and annot_var is not None)\n\n        if self._silplot is not None:\n            if annot_var is not None:\n                column, _ = self.data.get_column_view(annot_var)\n                if self._mask is not None:\n                    assert column.shape == self._mask.shape\n                    # pylint: disable=invalid-unary-operand-type\n                    column = column[~self._mask]\n                self._silplot.setRowNames(\n                    [annot_var.str_val(value) for value in column])\n            else:\n                self._silplot.setRowNames(None)\n\n    def _update_scene_rect(self):\n        geom = self._silplot.geometry()\n        self.scene.setSceneRect(geom)\n        self.view.setSceneRect(geom)\n\n        header = self._silplot.topScaleItem()\n        footer = self._silplot.bottomScaleItem()\n\n        def extend_horizontal(rect):\n            # type: (QRectF) -> QRectF\n            rect = QRectF(rect)\n            rect.setLeft(geom.left())\n            rect.setRight(geom.right())\n            return rect\n\n        margin = 3\n        if header is not None:\n            self.view.setHeaderSceneRect(\n                extend_horizontal(header.geometry().adjusted(0, 0, 0, margin)))\n        if footer is not None:\n            self.view.setFooterSceneRect(\n                extend_horizontal(footer.geometry().adjusted(0, -margin, 0, 0)))\n\n    @gui.deferred\n    def commit(self):\n        \"\"\"\n        Commit/send the current selection to the output.\n        \"\"\"\n        selected = indices = data = None\n        if self.data is not None:\n            selectedmask = np.full(len(self.data), False, dtype=bool)\n            if self._silplot is not None:\n                indices = self._silplot.selection()\n                assert (np.diff(indices) > 0).all(), \"strictly increasing\"\n                if self._mask is not None:\n                    # pylint: disable=invalid-unary-operand-type\n                    indices = np.flatnonzero(~self._mask)[indices]\n                selectedmask[indices] = True\n\n            if self._mask is not None:\n                scores = np.full(shape=selectedmask.shape,\n                                 fill_value=np.nan)\n                # pylint: disable=invalid-unary-operand-type\n                scores[~self._mask] = self._silhouette\n            else:\n                scores = self._silhouette\n\n            domain = self.data.domain\n            proposed = \"Silhouette ({})\".format(escape(self.cluster_var.name))\n            names = [var.name for var in itertools.chain(domain.attributes,\n                                                         domain.class_vars,\n                                                         domain.metas)]\n            unique = get_unique_names(names, proposed)\n            silhouette_var = Orange.data.ContinuousVariable(unique)\n            domain = Orange.data.Domain(\n                domain.attributes,\n                domain.class_vars,\n                domain.metas + (silhouette_var, ))\n\n            if np.count_nonzero(selectedmask):\n                selected = self.data.from_table(\n                    domain, self.data, np.flatnonzero(selectedmask))\n\n            if selected is not None:\n                with selected.unlocked(selected.metas):\n                    selected[:, silhouette_var] = np.c_[scores[selectedmask]]\n\n            data = self.data.transform(domain)\n            with data.unlocked(data.metas):\n                data[:, silhouette_var] = np.c_[scores]\n\n        self.Outputs.selected_data.send(selected)\n        self.Outputs.annotated_data.send(create_annotated_table(data, indices))\n\n    def send_report(self):\n        if not len(self.cluster_var_model):\n            return\n\n        self.report_plot()\n        caption = \"Silhouette plot \" \\\n                  f\"({self.Distances[self.distance_idx][0]} distance), \" \\\n                  f\"clustered by '{self.cluster_var.name}'\"\n        if self.annotation_var and self._silplot.rowNamesVisible():\n            caption += f\", annotated with '{self.annotation_var.name}'\"\n        self.report_caption(caption)\n\n    def onDeleteWidget(self):\n        self.clear()\n        super().onDeleteWidget()\n\n    @classmethod\n    def migrate_context(cls, context, version):\n        values = context.values\n        if version < 2:\n            discrete_vars = (\n                name\n                for name, type_ in itertools.chain(\n                    context.attributes, context.class_vars, context.metas)\n                if type_ == 1)\n            index, _ = values.pop(\"cluster_var_idx\")\n            name = next(itertools.islice(discrete_vars, index, None)\n            )\n            values[\"cluster_var\"] = (name, 101)\n\n            index = values.pop(\"annotation_var_idx\")[0] - 1\n            if index == -1:\n                values[\"annotation_var\"] = None\n            else:\n                annot_vars = (\n                    (name, type_ + 100)\n                    for name, type_ in itertools.chain(\n                        context.attributes, context.class_vars, context.metas)\n                    if type_ in (1, 3))\n                values[\"annotation_var\"] = next(\n                    itertools.islice(annot_vars, index, None))\n\n\nclass SelectAction(enum.IntEnum):\n    NoUpdate, Clear, Select, Deselect, Toogle, Current = 1, 2, 4, 8, 16, 32\n\n\ndef show_tool_tip(pos: QPoint, text: str, widget: Optional[QWidget] = None,\n                  rect=QRect(), elide=Qt.ElideRight):\n    \"\"\"\n    Show a plain text tool tip with limited length, eliding if necessary.\n    \"\"\"\n    if widget is not None:\n        screen = widget.screen()\n    else:\n        screen = QApplication.screenAt(pos)\n    font = QApplication.font(\"QTipLabel\")\n    fm = QFontMetrics(font)\n    geom = screen.availableSize()\n    etext = fm.elidedText(text, elide, geom.width())\n    if etext != text:\n        text = f\"<span>{etext}</span>\"\n    QToolTip.showText(pos, text, widget, rect)\n\n\nclass _SilhouettePlotTextListWidget(TextListWidget):\n    # disable default tooltips, SilhouettePlot handles them\n    def helpEvent(self, event: QGraphicsSceneHelpEvent):\n        return\n\n\nclass StyledGraphicsView(StickyGraphicsView):\n    \"\"\"\n    Propagate style and palette changes to the visualized scene.\n    \"\"\"\n    def __init__(self, *args, **kwargs):\n        super().__init__(*args, **kwargs)\n        self.ensurePolished()\n        if self.scene() is not None:\n            self.scene().setPalette(self.palette())\n\n    def setScene(self, scene):\n        super().setScene(scene)\n        if self.scene() is not None:\n            self.scene().setPalette(self.palette())\n\n    def changeEvent(self, event):\n        if event.type() == QEvent.PaletteChange and \\\n                self.scene() is not None and self.scene().parent() is self:\n            self.scene().setPalette(self.palette())\n        super().changeEvent(event)\n\n\nclass SilhouettePlot(QGraphicsWidget):\n    \"\"\"\n    A silhouette plot widget.\n    \"\"\"\n    #: Emitted when the current selection has changed\n    selectionChanged = Signal()\n\n    def __init__(self, parent=None, **kwargs):\n        super().__init__(parent, **kwargs)\n        self.setAcceptHoverEvents(True)\n        self.__groups = []\n        self.__rowNamesVisible = True\n        self.__barHeight = 3\n        self.__selectionRect = None\n        self.__selection = np.asarray([], dtype=int)\n        self.__selstate = None\n        self.__pen = QPen(Qt.NoPen)\n        self.__layout = QGraphicsGridLayout()\n        self.__hoveredItem = None\n        self.__topScale = None     # type: Optional[AxisItem]\n        self.__bottomScale = None  # type: Optional[AxisItem]\n        self.__layout.setColumnSpacing(0, 1.)\n        self.setLayout(self.__layout)\n        self.setFocusPolicy(Qt.StrongFocus)\n\n    def setScores(self, scores, labels, values, colors, rownames=None):\n        \"\"\"\n        Set the silhouette scores/labels to for display.\n\n        Arguments\n        ---------\n        scores : (N,) ndarray\n            The silhouette scores.\n        labels : (N,) ndarray\n            A ndarray (dtype=int) of label/clusters indices.\n        values : list of str\n            A list of label/cluster names.\n        colors : (N, 3) ndarray\n            A ndarray of RGB values.\n        rownames : list of str, optional\n            A list (len == N) of row names.\n        \"\"\"\n        scores = np.asarray(scores, dtype=float)\n        labels = np.asarray(labels, dtype=int)\n        if rownames is not None:\n            rownames = np.asarray(rownames, dtype=object)\n\n        if not scores.ndim == labels.ndim == 1:\n            raise ValueError(\"scores and labels must be 1 dimensional\")\n        if scores.shape != labels.shape:\n            raise ValueError(\"scores and labels must have the same shape\")\n        if rownames is not None and rownames.shape != scores.shape:\n            raise ValueError(\"rownames must have the same size as scores\")\n\n        Ck = np.unique(labels)\n        if not Ck[0] >= 0 and Ck[-1] < len(values):\n            raise ValueError(\n                \"All indices in `labels` must be in `range(len(values))`\")\n        cluster_indices = [np.flatnonzero(labels == i)\n                           for i in range(len(values))]\n        cluster_indices = [indices[np.argsort(scores[indices])[::-1]]\n                           for indices in cluster_indices]\n        groups = [\n            namespace(scores=scores[indices], indices=indices, label=label,\n                      rownames=(rownames[indices] if rownames is not None\n                                else None),\n                      color=color)\n            for indices, label, color in zip(cluster_indices, values, colors)\n        ]\n        self.clear()\n        self.__groups = groups\n        self.__setup()\n\n    def setRowNames(self, names):\n        if names is not None:\n            names = np.asarray(names, dtype=object)\n\n        layout = self.__layout\n        assert layout is self.layout()\n\n        font = self.font()\n        font.setPixelSize(self.__barHeight)\n\n        for i, grp in enumerate(self.__groups):\n            grp.rownames = names[grp.indices] if names is not None else None\n            item = layout.itemAt(i + 1, 3)\n            assert isinstance(item, TextListWidget)\n            if grp.rownames is not None:\n                item.setItems(grp.rownames)\n                item.setVisible(self.__rowNamesVisible)\n            else:\n                item.setItems([])\n                item.setVisible(False)\n        layout.activate()\n\n    def setRowNamesVisible(self, visible):\n        if self.__rowNamesVisible != visible:\n            self.__rowNamesVisible = visible\n            for item in self.__textItems():\n                item.setVisible(visible)\n            self.updateGeometry()\n\n    def rowNamesVisible(self):\n        return self.__rowNamesVisible\n\n    def setBarHeight(self, height):\n        \"\"\"\n        Set silhouette bar height (row height).\n        \"\"\"\n        if height != self.__barHeight:\n            self.__barHeight = height\n            for item in self.__plotItems():\n                item.setPreferredBarSize(height)\n            font = self.font()\n            font.setPixelSize(height)\n            for item in self.__textItems():\n                item.setFont(font)\n\n    def barHeight(self):\n        \"\"\"\n        Return the silhouette bar (row) height.\n        \"\"\"\n        return self.__barHeight\n\n    def clear(self):\n        \"\"\"\n        Clear the widget state\n        \"\"\"\n        scene = self.scene()\n        for child in self.childItems():\n            child.setParentItem(None)\n            scene.removeItem(child)\n        self.__groups = []\n        self.__topScale = None\n        self.__bottomScale = None\n\n    def __setup(self):\n        # Setup the subwidgets/groups/layout\n        smax = max((np.nanmax(g.scores) for g in self.__groups\n                    if g.scores.size),\n                   default=1)\n        smax = 1 if np.isnan(smax) else smax\n\n        smin = min((np.nanmin(g.scores) for g in self.__groups\n                    if g.scores.size),\n                   default=-1)\n        smin = -1 if np.isnan(smin) else smin\n        smin = min(smin, 0)\n\n        font = self.font()\n        font.setPixelSize(self.__barHeight)\n        foreground = self.palette().brush(QPalette.Foreground)\n        ax = AxisItem(parent=self, orientation=\"top\", maxTickLength=7)\n        ax.setRange(smin, smax)\n        self.__topScale = ax\n        layout = self.__layout\n        assert layout is self.layout()\n        layout.addItem(ax, 0, 2)\n\n        for i, group in enumerate(self.__groups):\n            silhouettegroup = BarPlotItem(parent=self)\n            silhouettegroup.setBrush(QBrush(QColor(*group.color)))\n            silhouettegroup.setPen(self.__pen)\n            silhouettegroup.setDataRange(smin, smax)\n            silhouettegroup.setPlotData(group.scores)\n            silhouettegroup.setPreferredBarSize(self.__barHeight)\n            silhouettegroup.setData(0, group.indices)\n            layout.addItem(silhouettegroup, i + 1, 2)\n\n            if group.label:\n                layout.addItem(Line(orientation=Qt.Vertical), i + 1, 1)\n                label = QGraphicsSimpleTextItem(\n                    \"{} ({})\".format(group.label, len(group.scores)), self\n                )\n                label.setBrush(foreground)\n                label.setPen(QPen(Qt.NoPen))\n                label.setRotation(-90)\n                item = SimpleLayoutItem(\n                    label,\n                    anchor=(0., 1.0),\n                    anchorItem=(0., 0.),\n                )\n                item.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)\n                layout.addItem(item, i + 1, 0, Qt.AlignCenter)\n\n            textlist = _SilhouettePlotTextListWidget(\n                self, font=font, elideMode=Qt.ElideRight,\n                alignment=Qt.AlignLeft | Qt.AlignVCenter\n            )\n            textlist.setMaximumWidth(750)\n            textlist.setFlag(TextListWidget.ItemClipsChildrenToShape, False)\n            sp = textlist.sizePolicy()\n            sp.setVerticalPolicy(QSizePolicy.Ignored)\n            textlist.setSizePolicy(sp)\n            if group.rownames is not None:\n                textlist.setItems(group.items)\n                textlist.setVisible(self.__rowNamesVisible)\n            else:\n                textlist.setVisible(False)\n\n            layout.addItem(textlist, i + 1, 3)\n\n        ax = AxisItem(parent=self, orientation=\"bottom\", maxTickLength=7)\n        ax.setRange(smin, smax)\n        self.__bottomScale = ax\n        layout.addItem(ax, len(self.__groups) + 1, 2)\n\n    def topScaleItem(self):\n        # type: () -> Optional[QGraphicsWidget]\n        return self.__topScale\n\n    def bottomScaleItem(self):\n        # type: () -> Optional[QGraphicsWidget]\n        return self.__bottomScale\n\n    def __updateSizeConstraints(self):\n        # set/update fixed height constraint on the text annotation items so\n        # it matches the silhouette's height\n        for silitem, textitem in zip(self.__plotItems(), self.__textItems()):\n            height = silitem.effectiveSizeHint(Qt.PreferredSize).height()\n            textitem.setMaximumHeight(height)\n            textitem.setMinimumHeight(height)\n        mwidth = max((silitem.effectiveSizeHint(Qt.PreferredSize).width()\n                     for silitem in self.__plotItems()), default=300)\n        # match the AxisItem's width to the bars\n        for axis in self.__axisItems():\n            axis.setMaximumWidth(mwidth)\n            axis.setMinimumWidth(mwidth)\n\n    def changeEvent(self, event: QEvent) -> None:\n        if event.type() == QEvent.PaletteChange:\n            brush = self.palette().brush(QPalette.Text)\n            labels = [it for it in self.childItems()\n                      if isinstance(it, QGraphicsSimpleTextItem)]\n            apply_all(labels, lambda it: it.setBrush(brush))\n        super().changeEvent(event)\n\n    def event(self, event: QEvent) -> bool:\n        # Reimplemented\n        if event.type() == QEvent.LayoutRequest and \\\n                self.parentLayoutItem() is None:\n            self.__updateSizeConstraints()\n            self.resize(self.effectiveSizeHint(Qt.PreferredSize))\n        elif event.type() == QEvent.GraphicsSceneHelp:\n            self.helpEvent(cast(QGraphicsSceneHelpEvent, event))\n            if event.isAccepted():\n                return True\n        return super().event(event)\n\n    def helpEvent(self, event: QGraphicsSceneHelpEvent):\n        pos = self.mapFromScene(event.scenePos())\n        item = self.__itemDataAtPos(pos)\n        if item is None:\n            return\n        data, index, rect = item\n        if data.rownames is None:\n            return\n        ttip = data.rownames[index]\n        if ttip:\n            view = event.widget().parentWidget()\n            rect = view.mapFromScene(self.mapToScene(rect)).boundingRect()\n            show_tool_tip(event.screenPos(), ttip, event.widget(), rect)\n            event.setAccepted(True)\n\n    def __setHoveredItem(self, item):\n        # Set the current hovered `item` (:class:`QGraphicsRectItem`)\n        if self.__hoveredItem is not item:\n            if self.__hoveredItem is not None:\n                self.__hoveredItem.setPen(QPen(Qt.NoPen))\n            self.__hoveredItem = item\n            if item is not None:\n                item.setPen(QPen(Qt.lightGray))\n\n    def hoverEnterEvent(self, event):\n        # Reimplemented\n        event.accept()\n\n    def hoverMoveEvent(self, event):\n        # Reimplemented\n        event.accept()\n        item = self.itemAtPos(event.pos())\n        self.__setHoveredItem(item)\n\n    def hoverLeaveEvent(self, event):\n        # Reimplemented\n        self.__setHoveredItem(None)\n        event.accept()\n\n    def mousePressEvent(self, event):\n        # Reimplemented\n        if event.button() == Qt.LeftButton:\n            if event.modifiers() & Qt.ControlModifier:\n                saction = SelectAction.Toogle\n            elif event.modifiers() & Qt.AltModifier:\n                saction = SelectAction.Deselect\n            elif event.modifiers() & Qt.ShiftModifier:\n                saction = SelectAction.Select\n            else:\n                saction = SelectAction.Clear | SelectAction.Select\n            self.__selstate = namespace(\n                modifiers=event.modifiers(),\n                selection=self.__selection,\n                action=saction,\n                rect=None,\n            )\n            if saction & SelectAction.Clear:\n                self.__selstate.selection = np.array([], dtype=int)\n                self.setSelection(self.__selstate.selection)\n            event.accept()\n\n    def keyPressEvent(self, event):\n        if event.key() in (Qt.Key_Up, Qt.Key_Down):\n            if event.key() == Qt.Key_Up:\n                self.__move_selection(self.selection(), -1)\n            elif event.key() == Qt.Key_Down:\n                self.__move_selection(self.selection(), 1)\n            event.accept()\n            return\n        super().keyPressEvent(event)\n\n    def mouseMoveEvent(self, event):\n        # Reimplemented\n        if event.buttons() & Qt.LeftButton:\n            assert self.__selstate is not None\n            if self.__selectionRect is None:\n                self.__selectionRect = QGraphicsRectItem(self)\n\n            rect = (QRectF(event.buttonDownPos(Qt.LeftButton),\n                           event.pos()).normalized())\n\n            if not rect.width():\n                rect = rect.adjusted(-1e-7, -1e-7, 1e-7, 1e-7)\n\n            rect = rect.intersected(self.contentsRect())\n            self.__selectionRect.setRect(rect)\n            self.__selstate.rect = rect\n            self.__selstate.action |= SelectAction.Current\n\n            self.__setSelectionRect(rect, self.__selstate.action)\n            event.accept()\n\n    def mouseReleaseEvent(self, event):\n        # Reimplemented\n        if event.button() == Qt.LeftButton:\n            if self.__selectionRect is not None:\n                self.__selectionRect.setParentItem(None)\n                if self.scene() is not None:\n                    self.scene().removeItem(self.__selectionRect)\n                self.__selectionRect = None\n            event.accept()\n\n            rect = (QRectF(event.buttonDownPos(Qt.LeftButton), event.pos())\n                    .normalized())\n\n            if not rect.isValid():\n                rect = rect.adjusted(-1e-7, -1e-7, 1e-7, 1e-7)\n\n            rect = rect.intersected(self.contentsRect())\n            action = self.__selstate.action & ~SelectAction.Current\n            self.__setSelectionRect(rect, action)\n            self.__selstate = None\n\n    def __move_selection(self, selection, offset):\n        ids = np.asarray([pi.data(0) for pi in self.__plotItems()]).ravel()\n        indices = [np.where(ids == i)[0] for i in selection]\n        indices = np.asarray(indices) + offset\n        if min(indices) >= 0 and max(indices) < len(ids):\n            self.setSelection(ids[indices])\n\n    def __setSelectionRect(self, rect, action):\n        # Set the current mouse drag selection rectangle\n        if not rect.isValid():\n            rect = rect.adjusted(-0.01, -0.01, 0.01, 0.01)\n\n        rect = rect.intersected(self.contentsRect())\n\n        indices = self.__selectionIndices(rect)\n\n        if action & SelectAction.Clear:\n            selection = []\n        elif self.__selstate is not None:\n            # Mouse drag selection is in progress. Update only the current\n            # selection\n            selection = self.__selstate.selection\n        else:\n            selection = self.__selection\n\n        if action & SelectAction.Toogle:\n            selection = np.setxor1d(selection, indices)\n        elif action & SelectAction.Deselect:\n            selection = np.setdiff1d(selection, indices)\n        elif action & SelectAction.Select:\n            selection = np.union1d(selection, indices)\n\n        self.setSelection(selection)\n\n    def __selectionIndices(self, rect):\n        items = [item for item in self.__plotItems()\n                 if item.geometry().intersects(rect)]\n        selection = [np.array([], dtype=int)]\n        for item in items:\n            indices = item.data(0)\n            itemrect = item.geometry().intersected(rect)\n            crect = item.contentsRect()\n            itemrect = (item.mapFromParent(itemrect).boundingRect()\n                        .intersected(crect))\n            assert itemrect.top() >= 0\n            rowh = crect.height() / item.count()\n            indextop = np.floor(itemrect.top() / rowh)\n            indexbottom = np.ceil(itemrect.bottom() / rowh)\n            selection.append(indices[int(indextop): int(indexbottom)])\n        return np.hstack(selection)\n\n    def itemAtPos(self, pos):\n        items = [item for item in self.__plotItems()\n                 if item.geometry().contains(pos)]\n        if not items:\n            return None\n        else:\n            item = items[0]\n        crect = item.contentsRect()\n        pos = item.mapFromParent(pos)\n        if not crect.contains(pos):\n            return None\n\n        assert pos.x() >= 0\n        rowh = crect.height() / item.count()\n        index = int(np.floor(pos.y() / rowh))\n        index = min(index, item.count() - 1)\n        if index >= 0:\n            return item.items()[index]\n        else:\n            return None\n\n    def __itemDataAtPos(self, pos) -> Optional[Tuple[namespace, int, QRectF]]:\n        items = [(sitem, tlist, grp) for sitem, tlist, grp\n                 in zip(self.__plotItems(), self.__textItems(), self.__groups)\n                 if sitem.geometry().contains(pos) or tlist.isVisible()\n                 and tlist.geometry().contains(pos)]\n        if not items:\n            return None\n        else:\n            sitem, _, grp = items[0]\n        indices = grp.indices\n        assert (isinstance(indices, np.ndarray) and\n                indices.shape == (sitem.count(),))\n        crect = sitem.contentsRect()\n        pos = sitem.mapFromParent(pos)\n        if not crect.top() <= pos.y() <= crect.bottom():\n            return None\n        rowh = crect.height() / sitem.count()\n        index = int(np.floor(pos.y() / rowh))\n        index = min(index, indices.size - 1)\n        baritem = sitem.items()[index]\n        rect = self.mapRectFromItem(baritem, baritem.rect())\n        crect = self.contentsRect()\n        rect.setLeft(crect.left())\n        rect.setRight(crect.right())\n        return grp, index, rect\n\n    def __selectionChanged(self, selected, deselected):\n        for item, grp in zip(self.__plotItems(), self.__groups):\n            select = np.flatnonzero(\n                np.in1d(grp.indices, selected, assume_unique=True))\n            items = item.items()\n            if select.size:\n                for i in select:\n                    color = np.hstack((grp.color, np.array([130])))\n                    items[i].setBrush(QBrush(QColor(*color)))\n\n            deselect = np.flatnonzero(\n                np.in1d(grp.indices, deselected, assume_unique=True))\n            if deselect.size:\n                for i in deselect:\n                    items[i].setBrush(QBrush(QColor(*grp.color)))\n\n    def __plotItems(self):\n        for i in range(len(self.__groups)):\n            item = self.layout().itemAt(i + 1, 2)\n            if item is not None:\n                assert isinstance(item, BarPlotItem)\n                yield item\n\n    def __textItems(self):\n        for i in range(len(self.__groups)):\n            item = self.layout().itemAt(i + 1, 3)\n            if item is not None:\n                assert isinstance(item, TextListWidget)\n                yield item\n\n    def __axisItems(self):\n        return self.__topScale, self.__bottomScale\n\n    def setSelection(self, indices):\n        indices = np.unique(np.asarray(indices, dtype=int))\n        select = np.setdiff1d(indices, self.__selection)\n        deselect = np.setdiff1d(self.__selection, indices)\n\n        self.__selectionChanged(select, deselect)\n\n        self.__selection = indices\n\n        if deselect.size or select.size:\n            self.selectionChanged.emit()\n\n    def selection(self):\n        return np.asarray(self.__selection, dtype=int)\n\n\nclass Line(QGraphicsWidget):\n    \"\"\"\n    A line separator graphics widget\n    \"\"\"\n    def __init__(self, parent=None, orientation=Qt.Horizontal, **kwargs):\n        sizePolicy = kwargs.pop(\"sizePolicy\", None)\n        super().__init__(None, **kwargs)\n        self.__orientation = Qt.Horizontal\n        if sizePolicy is None:\n            sizePolicy = QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)\n            sizePolicy.setControlType(QSizePolicy.Frame)\n            self.setSizePolicy(sizePolicy)\n        else:\n            self.setSizePolicy(sizePolicy)\n\n        self.setOrientation(orientation)\n\n        if parent is not None:\n            self.setParentItem(parent)\n\n    def setOrientation(self, orientation):\n        if self.__orientation != orientation:\n            self.__orientation = orientation\n            sp = self.sizePolicy()\n            if orientation == Qt.Vertical:\n                sp.setVerticalPolicy(QSizePolicy.Expanding)\n                sp.setHorizontalPolicy(QSizePolicy.Fixed)\n            else:\n                sp.setVerticalPolicy(QSizePolicy.Fixed)\n                sp.setHorizontalPolicy(QSizePolicy.Expanding)\n            self.setSizePolicy(sp)\n            self.updateGeometry()\n\n    def sizeHint(self, which, constraint=QRectF()):\n        # type: (Qt.SizeHint, QSizeF) -> QSizeF\n        pw = 1.\n        sh = QSizeF()\n        if which == Qt.MinimumSize:\n            sh = QSizeF(pw, pw)\n        elif which == Qt.PreferredSize:\n            sh = QSizeF(pw, 30.)\n        elif which == Qt.MaximumSize:\n            sh = QSizeF(pw, QWIDGETSIZE_MAX)\n\n        if self.__orientation == Qt.Horizontal:\n            sh.transpose()  # Qt4 compatible\n        return sh\n\n    def paint(self, painter, option, widget=None):\n        # type: (QPainter, QStyleOptionGraphicsItem, Optional[QWidget]) -> None\n        palette = self.palette()  # type: QPalette\n        color = palette.color(QPalette.Foreground)\n        painter.setPen(QPen(color, 1))\n        rect = self.contentsRect()\n        center = rect.center()\n        if self.__orientation == Qt.Vertical:\n            p1 = QPointF(center.x(), rect.top())\n            p2 = QPointF(center.x(), rect.bottom())\n        elif self.__orientation == Qt.Horizontal:\n            p1 = QPointF(rect.left(), center.y())\n            p2 = QPointF(rect.right(), center.y())\n        else:\n            assert False\n        painter.drawLine(p1, p2)\n\n\nclass BarPlotItem(QGraphicsWidget):\n    def __init__(self, parent=None, **kwargs):\n        super().__init__(parent, **kwargs)\n        self.__barsize = 5\n        self.__spacing = 1\n        self.__pen = QPen(Qt.NoPen)\n        self.__brush = QBrush(QColor(\"#3FCFCF\"))\n        self.__range = (0., 1.)\n        self.__data = np.array([], dtype=float)\n        self.__items = []\n\n    def count(self):\n        return self.__data.size\n\n    def items(self):\n        return list(self.__items)\n\n    def setGeometry(self, geom):\n        super().setGeometry(geom)\n        self.__layout()\n\n    def event(self, event):\n        if event.type() == QEvent.GraphicsSceneResize:\n            self.__layout()\n        return super().event(event)\n\n    def sizeHint(self, which, constraint=QSizeF()):\n        spacing = max(self.__spacing * (self.count() - 1), 0)\n        return QSizeF(300, self.__barsize * self.count() + spacing)\n\n    def setPreferredBarSize(self, size):\n        if self.__barsize != size:\n            self.__barsize = size\n            self.updateGeometry()\n\n    def spacing(self):\n        return self.__spacing\n\n    def setSpacing(self, spacing):\n        if self.__spacing != spacing:\n            self.__spacing = spacing\n            self.updateGeometry()\n\n    def setPen(self, pen):\n        pen = QPen(pen)\n        if self.__pen != pen:\n            self.__pen = pen\n            for item in self.__items:\n                item.setPen(pen)\n\n    def pen(self):\n        return QPen(self.__pen)\n\n    def setBrush(self, brush):\n        brush = QBrush(brush)\n        if self.__brush != brush:\n            self.__brush = brush\n            for item in self.__items:\n                item.setBrush(brush)\n\n    def brush(self):\n        return QBrush(self.__brush)\n\n    def setPlotData(self, values):\n        self.__data = np.array(values, copy=True)\n        self.__update()\n        self.updateGeometry()\n\n    def setDataRange(self, rangemin, rangemax):\n        if self.__range != (rangemin, rangemax):\n            self.__range = (rangemin, rangemax)\n            self.__layout()\n\n    def __clear(self):\n        for item in self.__items:\n            item.setParentItem(None)\n        scene = self.scene()\n        if scene is not None:\n            for item in self.__items:\n                scene.removeItem(item)\n        self.__items = []\n\n    def __update(self):\n        self.__clear()\n\n        pen = self.pen()\n        brush = self.brush()\n        for _ in range(self.count()):\n            item = QGraphicsRectItem(self)\n            item.setPen(pen)\n            item.setBrush(brush)\n            self.__items.append(item)\n\n        self.__layout()\n\n    def __layout(self):\n        (N, ) = self.__data.shape\n        if not N:\n            return\n\n        spacing = self.__spacing\n        rect = self.contentsRect()\n        w = rect.width()\n        if rect.height() - (spacing * (N - 1)) <= 0:\n            spacing = 0\n\n        h = (rect.height() - (spacing * (N - 1))) / N\n        xmin, xmax = self.__range\n        span = xmax - xmin\n        if span < 1e-9:\n            span = 1\n        scalef = w * 1 / span\n\n        base = 0\n        base = (base - xmin) * scalef\n        datascaled = (self.__data - xmin) * scalef\n\n        for i, (v, item) in enumerate(zip(datascaled, self.__items)):\n            item.setRect(QRectF(base, rect.top() + i * (h + spacing),\n                                v - base, h).normalized())\n\n\nif __name__ == \"__main__\":  # pragma: no cover\n    WidgetPreview(OWSilhouettePlot).run(Orange.data.Table(\"brown-selected\"))\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:** Context Migration Fails Due to Missing `class_vars` Attribute

**Description:**
When attempting to migrate context for the `OWSilhouettePlot` widget, an error occurs if the `Context` object lacks the `class_vars` attribute. This issue arises during the migration process for contexts with version less than 2, leading to an `AttributeError`.

**Example Buggy Code:**
```python
def test_migration_failure():
    enc_domain = {
        'attributes': (('foo', 1), ('bar', 1), ('baz', 1), ('bax', 1), ('cfoo', 1), ('mbaz', 1))
    }
    context = Context(
        values={'cluster_var_idx': (0, -2), 'annotation_var_idx': (0, -2)},
        **enc_domain
    )
    OWSilhouettePlot.migrate_context(context, 1)
```
In this test, the `Context` object is initialized without the `class_vars` attribute. When `migrate_context` is called with version `1`, it attempts to access `context.class_vars`, which does not exist, resulting in an error.

**Expected Behavior:**
The context migration should handle cases where certain attributes like `class_vars` or `metas` are missing from the `Context` object. Instead of raising an error, it should gracefully migrate the available attributes and maintain the integrity of the context values.

**Actual Behavior:**
An `AttributeError` is raised:
```
AttributeError: 'Context' object has no attribute 'class_vars'
```
This prevents the successful migration of the context, potentially disrupting the workflow for users relying on older context versions.

[/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/owsilhouetteplot.py
import itertools
import enum
from xml.sax.saxutils import escape
from types import SimpleNamespace as namespace

from typing import Optional, Union, Tuple, cast

import numpy as np
import sklearn.metrics

from AnyQt.QtWidgets import (
    QGraphicsWidget, QGraphicsGridLayout,
    QGraphicsRectItem, QStyleOptionGraphicsItem, QSizePolicy, QWidget,
    QVBoxLayout, QGraphicsSimpleTextItem, QWIDGETSIZE_MAX,
    QGraphicsSceneHelpEvent, QToolTip, QApplication,
)
from AnyQt.QtGui import (
    QColor, QPen, QBrush, QPainter, QFontMetrics, QPalette,
)
from AnyQt.QtCore import (
    Qt, QEvent, QRectF, QSizeF, QSize, QPointF, QPoint, QRect
)
from AnyQt.QtCore import pyqtSignal as Signal

import Orange.data
from Orange.data.util import get_unique_names
import Orange.distance
import Orange.misc
from Orange.data import Table, Domain
from Orange.misc import DistMatrix

from Orange.widgets import widget, gui, settings
from Orange.widgets.utils.graphicsscene import GraphicsScene
from Orange.widgets.utils.stickygraphicsview import StickyGraphicsView
from Orange.widgets.utils import itemmodels, apply_all
from Orange.widgets.utils.annotated_data import (create_annotated_table,
                                                 ANNOTATED_DATA_SIGNAL_NAME)
from Orange.widgets.utils.graphicstextlist import TextListWidget
from Orange.widgets.utils.graphicslayoutitem import SimpleLayoutItem
from Orange.widgets.utils.sql import check_sql_input
from Orange.widgets.utils.widgetpreview import WidgetPreview
from Orange.widgets.visualize.utils.plotutils import AxisItem
from Orange.widgets.widget import Msg, Input, Output


class InputValidationError(ValueError):
    message: str


class NoGroupVariable(InputValidationError):
    message = "Input does not have any suitable labels"


class ValidationError(InputValidationError):
    def __init__(self, message: str):
        super().__init__(message)
        self.message = message


class OWSilhouettePlot(widget.OWWidget):
    name = "Silhouette Plot"
    description = "Visually assess cluster quality and " \
                  "the degree of cluster membership."

    icon = "icons/SilhouettePlot.svg"
    priority = 300
    keywords = []

    class Inputs:
        data = Input("Data", (Orange.data.Table, Orange.misc.DistMatrix))

    class Outputs:
        selected_data = Output("Selected Data", Orange.data.Table, default=True)
        annotated_data = Output(ANNOTATED_DATA_SIGNAL_NAME, Orange.data.Table)

    replaces = [
        "orangecontrib.prototypes.widgets.owsilhouetteplot.OWSilhouettePlot",
        "Orange.widgets.unsupervised.owsilhouetteplot.OWSilhouettePlot"
    ]

    settingsHandler = settings.DomainContextHandler()
    settings_version = 2

    #: Distance metric index
    distance_idx = settings.Setting(0)
    #: Group/cluster variable
    cluster_var = settings.ContextSetting(None)
    #: Annotation variable
    annotation_var = settings.ContextSetting(None)
    #: Group the (displayed) silhouettes by cluster
    group_by_cluster = settings.Setting(True)
    #: A fixed size for an instance bar
    bar_size = settings.Setting(3)
    #: Add silhouette scores to output data
    auto_commit = settings.Setting(True)

    pending_selection = settings.Setting(None, schema_only=True)

    Distances = [("Euclidean", Orange.distance.Euclidean),
                 ("Manhattan", Orange.distance.Manhattan),
                 ("Cosine", Orange.distance.Cosine)]

    graph_name = "scene"

    class Error(widget.OWWidget.Error):
        need_two_clusters = Msg("Need at least two non-empty clusters")
        singleton_clusters_all = Msg("All clusters are singletons")
        memory_error = Msg("Not enough memory")
        value_error = Msg("Distances could not be computed: '{}'")
        input_validation_error = Msg("{}")

    class Warning(widget.OWWidget.Warning):
        missing_cluster_assignment = Msg(
            "{} instance{s} omitted (missing cluster assignment)")
        nan_distances = Msg("{} instance{s} omitted (undefined distances)")
        ignoring_categorical = Msg("Ignoring categorical features")

    def __init__(self):
        super().__init__()
        #: The input data
        self.data = None         # type: Optional[Orange.data.Table]
        #: The input distance matrix (if present)
        self.distances = None  # type: Optional[Orange.misc.DistMatrix]
        #: The effective distance matrix (is self.distances or computed from
        #: self.data depending on input)
        self._matrix = None      # type: Optional[Orange.misc.DistMatrix]
        #: An bool mask (size == len(data)) indicating missing group/cluster
        #: assignments
        self._mask = None        # type: Optional[np.ndarray]
        #: An array of cluster/group labels for instances with valid group
        #: assignment
        self._labels = None      # type: Optional[np.ndarray]
        #: An array of silhouette scores for instances with valid group
        #: assignment
        self._silhouette = None  # type: Optional[np.ndarray]
        self._silplot = None     # type: Optional[SilhouettePlot]

        controllayout = self.controlArea.layout()
        assert isinstance(controllayout, QVBoxLayout)
        self._distances_gui_box = distbox = gui.widgetBox(
            None, "Distance"
        )
        self._distances_gui_cb = gui.comboBox(
            distbox, self, "distance_idx",
            items=[name for name, _ in OWSilhouettePlot.Distances],
            orientation=Qt.Horizontal, callback=self._invalidate_distances)
        controllayout.addWidget(distbox)

        box = gui.vBox(self.controlArea, "Grouping")
        self.cluster_var_model = itemmodels.VariableListModel(
            parent=self, placeholder="(None)")
        self.cluster_var_cb = gui.comboBox(
            box, self, "cluster_var", contentsLength=14,
            searchable=True, callback=self._invalidate_scores,
            model=self.cluster_var_model
        )
        gui.checkBox(
            box, self, "group_by_cluster", "Show in groups",
            callback=self._replot)

        box = gui.vBox(self.controlArea, "Bars")
        gui.widgetLabel(box, "Bar width:")
        gui.hSlider(
            box, self, "bar_size", minValue=1, maxValue=10, step=1,
            callback=self._update_bar_size)
        gui.widgetLabel(box, "Annotations:")
        self.annotation_var_model = itemmodels.VariableListModel(parent=self)
        self.annotation_var_model[:] = [None]
        self.annotation_cb = gui.comboBox(
            box, self, "annotation_var", contentsLength=14,
            callback=self._update_annotations,
            model=self.annotation_var_model)
        ibox = gui.indentedBox(box, 5)
        self.ann_hidden_warning = warning = gui.widgetLabel(
            ibox, "(increase the width to show)")
        ibox.setFixedWidth(ibox.sizeHint().width())
        warning.setVisible(False)

        gui.rubber(self.controlArea)

        gui.auto_send(self.buttonsArea, self, "auto_commit")

        self.scene = GraphicsScene(self)
        self.view = StyledGraphicsView(self.scene)
        self.view.setRenderHint(QPainter.Antialiasing, True)
        self.view.setAlignment(Qt.AlignTop | Qt.AlignLeft)
        self.mainArea.layout().addWidget(self.view)

        self.settingsAboutToBePacked.connect(self.pack_settings)

    def sizeHint(self):
        sh = self.controlArea.sizeHint()
        return sh.expandedTo(QSize(600, 720))

    def pack_settings(self):
        if self.data and self._silplot is not None:
            self.pending_selection = list(self._silplot.selection())
        else:
            self.pending_selection = None

    @Inputs.data
    @check_sql_input
    def set_data(self, data: Union[Table, DistMatrix, None]):
        """
        Set the input dataset or distance matrix.
        """
        self.closeContext()
        self.clear()
        try:
            if isinstance(data, Orange.misc.DistMatrix):
                self._set_distances(data)
            elif isinstance(data, Orange.data.Table):
                self._set_table(data)
            else:
                self.distances = None
                self.data = None
        except InputValidationError as err:
            self.Error.input_validation_error(err.message)
            self.distances = None
            self.data = None

    def _set_table(self, data: Table):
        self._setup_control_models(data.domain)
        self.data = data
        self.distances = None

    def _set_distances(self, distances: DistMatrix):
        if isinstance(distances.row_items, Orange.data.Table) and \
                distances.axis == 1:
            data = distances.row_items
        else:
            raise ValidationError("Input matrix does not have associated data")

        if data is not None:
            self._setup_control_models(data.domain)
            self.distances = distances
            self.data = data

    def handleNewSignals(self):
        if not self._is_empty():
            self._update()
            self._replot()
            if self.pending_selection is not None and self._silplot is not None:
                # If selection contains indices that are too large, the data
                # file must had been modified, so we ignore selection
                if max(self.pending_selection, default=-1) < len(self.data):
                    self._silplot.setSelection(np.array(self.pending_selection))
                self.pending_selection = None

        # Disable/enable the Distances GUI controls if applicable
        self._distances_gui_box.setEnabled(self.distances is None)

        self.commit.now()

    def _setup_control_models(self, domain: Domain):
        groupvars = [
            v for v in domain.variables + domain.metas
            if v.is_discrete and len(v.values) >= 2]
        if not groupvars:
            raise NoGroupVariable()
        self.cluster_var_model[:] = groupvars
        if domain.class_var in groupvars:
            self.cluster_var = domain.class_var
        else:
            self.cluster_var = groupvars[0]
        annotvars = [var for var in domain.variables + domain.metas
                     if var.is_string or var.is_discrete]
        self.annotation_var_model[:] = [None] + annotvars
        self.annotation_var = annotvars[0] if annotvars else None
        self.openContext(Orange.data.Domain(groupvars))

    def _is_empty(self) -> bool:
        # Is empty (does not have any input).
        return (self.data is None or len(self.data) == 0) \
               and self.distances is None

    def clear(self):
        """
        Clear the widget state.
        """
        self.data = None
        self.distances = None
        self._matrix = None
        self._mask = None
        self._silhouette = None
        self._labels = None
        self.cluster_var_model[:] = []
        self.annotation_var_model[:] = [None]
        self._clear_scene()
        self.Error.clear()
        self.Warning.clear()

    def _clear_scene(self):
        # Clear the graphics scene and associated objects
        self.scene.clear()
        self.scene.setSceneRect(QRectF())
        self.view.setSceneRect(QRectF())
        self.view.setHeaderSceneRect(QRectF())
        self.view.setFooterSceneRect(QRectF())
        self._silplot = None

    def _invalidate_distances(self):
        # Invalidate the computed distance matrix and recompute the silhouette.
        self._matrix = None
        self._invalidate_scores()

    def _invalidate_scores(self):
        # Invalidate and recompute the current silhouette scores.
        self._labels = self._silhouette = self._mask = None
        self._update()
        self._replot()
        if self.data is not None:
            self.commit.deferred()

    def _ensure_matrix(self):
        # ensure self._matrix is computed if necessary
        if self._is_empty():
            return
        if self._matrix is None:
            if self.distances is not None:
                self._matrix = np.asarray(self.distances)
            elif self.data is not None:
                data = self.data
                _, metric = self.Distances[self.distance_idx]
                if not metric.supports_discrete and any(
                        a.is_discrete for a in data.domain.attributes):
                    self.Warning.ignoring_categorical()
                    data = Orange.distance.remove_discrete_features(data)
                try:
                    self._matrix = np.asarray(metric(data))
                except MemoryError:
                    self.Error.memory_error()
                    return
                except ValueError as err:
                    self.Error.value_error(str(err))
                    return
            else:
                assert False, "invalid state"

    def _update(self):
        # Update/recompute the effective distances and scores as required.
        self._clear_messages()
        if self._is_empty():
            self._reset_all()
            return

        self._ensure_matrix()
        if self._matrix is None:
            return

        labels, _ = self.data.get_column_view(self.cluster_var)
        labels = np.asarray(labels, dtype=float)
        cluster_mask = np.isnan(labels)
        dist_mask = np.isnan(self._matrix).all(axis=0)
        mask = cluster_mask | dist_mask
        labels = labels.astype(int)
        labels = labels[~mask]

        labels_unq = np.unique(labels)

        if len(labels_unq) < 2:
            self.Error.need_two_clusters()
            labels = silhouette = mask = None
        elif len(labels_unq) == len(labels):
            self.Error.singleton_clusters_all()
            labels = silhouette = mask = None
        else:
            silhouette = sklearn.metrics.silhouette_samples(
                self._matrix[~mask, :][:, ~mask], labels, metric="precomputed")
        self._mask = mask
        self._labels = labels
        self._silhouette = silhouette

        if mask is not None:
            count_missing = np.count_nonzero(cluster_mask)
            if count_missing:
                self.Warning.missing_cluster_assignment(
                    count_missing, s="s" if count_missing > 1 else "")
            count_nandist = np.count_nonzero(dist_mask)
            if count_nandist:
                self.Warning.nan_distances(
                    count_nandist, s="s" if count_nandist > 1 else "")

    def _reset_all(self):
        self._mask = None
        self._silhouette = None
        self._labels = None
        self._matrix = None
        self._clear_scene()

    def _clear_messages(self):
        self.Error.clear()
        self.Warning.clear()

    def _set_bar_height(self):
        visible = self.bar_size >= 5
        self._silplot.setBarHeight(self.bar_size)
        self._silplot.setRowNamesVisible(visible)
        self.ann_hidden_warning.setVisible(
            not visible and self.annotation_var is not None)

    def _replot(self):
        # Clear and replot/initialize the scene
        self._clear_scene()
        if self._silhouette is not None and self._labels is not None:
            self._silplot = silplot = SilhouettePlot()
            self._set_bar_height()

            if self.group_by_cluster:
                silplot.setScores(
                    self._silhouette, self._labels,
                    self.cluster_var.values, self.cluster_var.colors)
            else:
                silplot.setScores(
                    self._silhouette,
                    np.zeros(len(self._silhouette), dtype=int),
                    [""], np.array([[63, 207, 207]])
                )

            self.scene.addItem(silplot)
            self._update_annotations()
            silplot.selectionChanged.connect(self.commit.deferred)
            silplot.layout().activate()
            self._update_scene_rect()
            silplot.geometryChanged.connect(self._update_scene_rect)

    def _update_bar_size(self):
        if self._silplot is not None:
            self._set_bar_height()

    def _update_annotations(self):
        annot_var = self.annotation_var
        self.ann_hidden_warning.setVisible(
            self.bar_size < 5 and annot_var is not None)

        if self._silplot is not None:
            if annot_var is not None:
                column, _ = self.data.get_column_view(annot_var)
                if self._mask is not None:
                    assert column.shape == self._mask.shape
                    # pylint: disable=invalid-unary-operand-type
                    column = column[~self._mask]
                self._silplot.setRowNames(
                    [annot_var.str_val(value) for value in column])
            else:
                self._silplot.setRowNames(None)

    def _update_scene_rect(self):
        geom = self._silplot.geometry()
        self.scene.setSceneRect(geom)
        self.view.setSceneRect(geom)

        header = self._silplot.topScaleItem()
        footer = self._silplot.bottomScaleItem()

        def extend_horizontal(rect):
            # type: (QRectF) -> QRectF
            rect = QRectF(rect)
            rect.setLeft(geom.left())
            rect.setRight(geom.right())
            return rect

        margin = 3
        if header is not None:
            self.view.setHeaderSceneRect(
                extend_horizontal(header.geometry().adjusted(0, 0, 0, margin)))
        if footer is not None:
            self.view.setFooterSceneRect(
                extend_horizontal(footer.geometry().adjusted(0, -margin, 0, 0)))

    @gui.deferred
    def commit(self):
        """
        Commit/send the current selection to the output.
        """
        selected = indices = data = None
        if self.data is not None:
            selectedmask = np.full(len(self.data), False, dtype=bool)
            if self._silplot is not None:
                indices = self._silplot.selection()
                assert (np.diff(indices) > 0).all(), "strictly increasing"
                if self._mask is not None:
                    # pylint: disable=invalid-unary-operand-type
                    indices = np.flatnonzero(~self._mask)[indices]
                selectedmask[indices] = True

            if self._mask is not None:
                scores = np.full(shape=selectedmask.shape,
                                 fill_value=np.nan)
                # pylint: disable=invalid-unary-operand-type
                scores[~self._mask] = self._silhouette
            else:
                scores = self._silhouette

            domain = self.data.domain
            proposed = "Silhouette ({})".format(escape(self.cluster_var.name))
            names = [var.name for var in itertools.chain(domain.attributes,
                                                         domain.class_vars,
                                                         domain.metas)]
            unique = get_unique_names(names, proposed)
            silhouette_var = Orange.data.ContinuousVariable(unique)
            domain = Orange.data.Domain(
                domain.attributes,
                domain.class_vars,
                domain.metas + (silhouette_var, ))

            if np.count_nonzero(selectedmask):
                selected = self.data.from_table(
                    domain, self.data, np.flatnonzero(selectedmask))

            if selected is not None:
                with selected.unlocked(selected.metas):
                    selected[:, silhouette_var] = np.c_[scores[selectedmask]]

            data = self.data.transform(domain)
            with data.unlocked(data.metas):
                data[:, silhouette_var] = np.c_[scores]

        self.Outputs.selected_data.send(selected)
        self.Outputs.annotated_data.send(create_annotated_table(data, indices))

    def send_report(self):
        if not len(self.cluster_var_model):
            return

        self.report_plot()
        caption = "Silhouette plot " \
                  f"({self.Distances[self.distance_idx][0]} distance), " \
                  f"clustered by '{self.cluster_var.name}'"
        if self.annotation_var and self._silplot.rowNamesVisible():
            caption += f", annotated with '{self.annotation_var.name}'"
        self.report_caption(caption)

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

    @classmethod
    def migrate_context(cls, context, version):
        values = context.values
        if version < 2:
            discrete_vars = (
                name
                for name, type_ in itertools.chain(
                    context.attributes, context.class_vars, context.metas)
                if type_ == 1)
            index, _ = values.pop("cluster_var_idx")
            name = next(itertools.islice(discrete_vars, index, None)
            )
            values["cluster_var"] = (name, 101)

            index = values.pop("annotation_var_idx")[0] - 1
            if index == -1:
                values["annotation_var"] = None
            else:
                annot_vars = (
                    (name, type_ + 100)
                    for name, type_ in itertools.chain(
                        context.attributes, context.class_vars, context.metas)
                    if type_ in (1, 3))
                values["annotation_var"] = next(
                    itertools.islice(annot_vars, index, None))


class SelectAction(enum.IntEnum):
    NoUpdate, Clear, Select, Deselect, Toogle, Current = 1, 2, 4, 8, 16, 32


def show_tool_tip(pos: QPoint, text: str, widget: Optional[QWidget] = None,
                  rect=QRect(), elide=Qt.ElideRight):
    """
    Show a plain text tool tip with limited length, eliding if necessary.
    """
    if widget is not None:
        screen = widget.screen()
    else:
        screen = QApplication.screenAt(pos)
    font = QApplication.font("QTipLabel")
    fm = QFontMetrics(font)
    geom = screen.availableSize()
    etext = fm.elidedText(text, elide, geom.width())
    if etext != text:
        text = f"<span>{etext}</span>"
    QToolTip.showText(pos, text, widget, rect)


class _SilhouettePlotTextListWidget(TextListWidget):
    # disable default tooltips, SilhouettePlot handles them
    def helpEvent(self, event: QGraphicsSceneHelpEvent):
        return


class StyledGraphicsView(StickyGraphicsView):
    """
    Propagate style and palette changes to the visualized scene.
    """
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.ensurePolished()
        if self.scene() is not None:
            self.scene().setPalette(self.palette())

    def setScene(self, scene):
        super().setScene(scene)
        if self.scene() is not None:
            self.scene().setPalette(self.palette())

    def changeEvent(self, event):
        if event.type() == QEvent.PaletteChange and \
                self.scene() is not None and self.scene().parent() is self:
            self.scene().setPalette(self.palette())
        super().changeEvent(event)


class SilhouettePlot(QGraphicsWidget):
    """
    A silhouette plot widget.
    """
    #: Emitted when the current selection has changed
    selectionChanged = Signal()

    def __init__(self, parent=None, **kwargs):
        super().__init__(parent, **kwargs)
        self.setAcceptHoverEvents(True)
        self.__groups = []
        self.__rowNamesVisible = True
        self.__barHeight = 3
        self.__selectionRect = None
        self.__selection = np.asarray([], dtype=int)
        self.__selstate = None
        self.__pen = QPen(Qt.NoPen)
        self.__layout = QGraphicsGridLayout()
        self.__hoveredItem = None
        self.__topScale = None     # type: Optional[AxisItem]
        self.__bottomScale = None  # type: Optional[AxisItem]
        self.__layout.setColumnSpacing(0, 1.)
        self.setLayout(self.__layout)
        self.setFocusPolicy(Qt.StrongFocus)

    def setScores(self, scores, labels, values, colors, rownames=None):
        """
        Set the silhouette scores/labels to for display.

        Arguments
        ---------
        scores : (N,) ndarray
            The silhouette scores.
        labels : (N,) ndarray
            A ndarray (dtype=int) of label/clusters indices.
        values : list of str
            A list of label/cluster names.
        colors : (N, 3) ndarray
            A ndarray of RGB values.
        rownames : list of str, optional
            A list (len == N) of row names.
        """
        scores = np.asarray(scores, dtype=float)
        labels = np.asarray(labels, dtype=int)
        if rownames is not None:
            rownames = np.asarray(rownames, dtype=object)

        if not scores.ndim == labels.ndim == 1:
            raise ValueError("scores and labels must be 1 dimensional")
        if scores.shape != labels.shape:
            raise ValueError("scores and labels must have the same shape")
        if rownames is not None and rownames.shape != scores.shape:
            raise ValueError("rownames must have the same size as scores")

        Ck = np.unique(labels)
        if not Ck[0] >= 0 and Ck[-1] < len(values):
            raise ValueError(
                "All indices in `labels` must be in `range(len(values))`")
        cluster_indices = [np.flatnonzero(labels == i)
                           for i in range(len(values))]
        cluster_indices = [indices[np.argsort(scores[indices])[::-1]]
                           for indices in cluster_indices]
        groups = [
            namespace(scores=scores[indices], indices=indices, label=label,
                      rownames=(rownames[indices] if rownames is not None
                                else None),
                      color=color)
            for indices, label, color in zip(cluster_indices, values, colors)
        ]
        self.clear()
        self.__groups = groups
        self.__setup()

    def setRowNames(self, names):
        if names is not None:
            names = np.asarray(names, dtype=object)

        layout = self.__layout
        assert layout is self.layout()

        font = self.font()
        font.setPixelSize(self.__barHeight)

        for i, grp in enumerate(self.__groups):
            grp.rownames = names[grp.indices] if names is not None else None
            item = layout.itemAt(i + 1, 3)
            assert isinstance(item, TextListWidget)
            if grp.rownames is not None:
                item.setItems(grp.rownames)
                item.setVisible(self.__rowNamesVisible)
            else:
                item.setItems([])
                item.setVisible(False)
        layout.activate()

    def setRowNamesVisible(self, visible):
        if self.__rowNamesVisible != visible:
            self.__rowNamesVisible = visible
            for item in self.__textItems():
                item.setVisible(visible)
            self.updateGeometry()

    def rowNamesVisible(self):
        return self.__rowNamesVisible

    def setBarHeight(self, height):
        """
        Set silhouette bar height (row height).
        """
        if height != self.__barHeight:
            self.__barHeight = height
            for item in self.__plotItems():
                item.setPreferredBarSize(height)
            font = self.font()
            font.setPixelSize(height)
            for item in self.__textItems():
                item.setFont(font)

    def barHeight(self):
        """
        Return the silhouette bar (row) height.
        """
        return self.__barHeight

    def clear(self):
        """
        Clear the widget state
        """
        scene = self.scene()
        for child in self.childItems():
            child.setParentItem(None)
            scene.removeItem(child)
        self.__groups = []
        self.__topScale = None
        self.__bottomScale = None

    def __setup(self):
        # Setup the subwidgets/groups/layout
        smax = max((np.nanmax(g.scores) for g in self.__groups
                    if g.scores.size),
                   default=1)
        smax = 1 if np.isnan(smax) else smax

        smin = min((np.nanmin(g.scores) for g in self.__groups
                    if g.scores.size),
                   default=-1)
        smin = -1 if np.isnan(smin) else smin
        smin = min(smin, 0)

        font = self.font()
        font.setPixelSize(self.__barHeight)
        foreground = self.palette().brush(QPalette.Foreground)
        ax = AxisItem(parent=self, orientation="top", maxTickLength=7)
        ax.setRange(smin, smax)
        self.__topScale = ax
        layout = self.__layout
        assert layout is self.layout()
        layout.addItem(ax, 0, 2)

        for i, group in enumerate(self.__groups):
            silhouettegroup = BarPlotItem(parent=self)
            silhouettegroup.setBrush(QBrush(QColor(*group.color)))
            silhouettegroup.setPen(self.__pen)
            silhouettegroup.setDataRange(smin, smax)
            silhouettegroup.setPlotData(group.scores)
            silhouettegroup.setPreferredBarSize(self.__barHeight)
            silhouettegroup.setData(0, group.indices)
            layout.addItem(silhouettegroup, i + 1, 2)

            if group.label:
                layout.addItem(Line(orientation=Qt.Vertical), i + 1, 1)
                label = QGraphicsSimpleTextItem(
                    "{} ({})".format(group.label, len(group.scores)), self
                )
                label.setBrush(foreground)
                label.setPen(QPen(Qt.NoPen))
                label.setRotation(-90)
                item = SimpleLayoutItem(
                    label,
                    anchor=(0., 1.0),
                    anchorItem=(0., 0.),
                )
                item.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
                layout.addItem(item, i + 1, 0, Qt.AlignCenter)

            textlist = _SilhouettePlotTextListWidget(
                self, font=font, elideMode=Qt.ElideRight,
                alignment=Qt.AlignLeft | Qt.AlignVCenter
            )
            textlist.setMaximumWidth(750)
            textlist.setFlag(TextListWidget.ItemClipsChildrenToShape, False)
            sp = textlist.sizePolicy()
            sp.setVerticalPolicy(QSizePolicy.Ignored)
            textlist.setSizePolicy(sp)
            if group.rownames is not None:
                textlist.setItems(group.items)
                textlist.setVisible(self.__rowNamesVisible)
            else:
                textlist.setVisible(False)

            layout.addItem(textlist, i + 1, 3)

        ax = AxisItem(parent=self, orientation="bottom", maxTickLength=7)
        ax.setRange(smin, smax)
        self.__bottomScale = ax
        layout.addItem(ax, len(self.__groups) + 1, 2)

    def topScaleItem(self):
        # type: () -> Optional[QGraphicsWidget]
        return self.__topScale

    def bottomScaleItem(self):
        # type: () -> Optional[QGraphicsWidget]
        return self.__bottomScale

    def __updateSizeConstraints(self):
        # set/update fixed height constraint on the text annotation items so
        # it matches the silhouette's height
        for silitem, textitem in zip(self.__plotItems(), self.__textItems()):
            height = silitem.effectiveSizeHint(Qt.PreferredSize).height()
            textitem.setMaximumHeight(height)
            textitem.setMinimumHeight(height)
        mwidth = max((silitem.effectiveSizeHint(Qt.PreferredSize).width()
                     for silitem in self.__plotItems()), default=300)
        # match the AxisItem's width to the bars
        for axis in self.__axisItems():
            axis.setMaximumWidth(mwidth)
            axis.setMinimumWidth(mwidth)

    def changeEvent(self, event: QEvent) -> None:
        if event.type() == QEvent.PaletteChange:
            brush = self.palette().brush(QPalette.Text)
            labels = [it for it in self.childItems()
                      if isinstance(it, QGraphicsSimpleTextItem)]
            apply_all(labels, lambda it: it.setBrush(brush))
        super().changeEvent(event)

    def event(self, event: QEvent) -> bool:
        # Reimplemented
        if event.type() == QEvent.LayoutRequest and \
                self.parentLayoutItem() is None:
            self.__updateSizeConstraints()
            self.resize(self.effectiveSizeHint(Qt.PreferredSize))
        elif event.type() == QEvent.GraphicsSceneHelp:
            self.helpEvent(cast(QGraphicsSceneHelpEvent, event))
            if event.isAccepted():
                return True
        return super().event(event)

    def helpEvent(self, event: QGraphicsSceneHelpEvent):
        pos = self.mapFromScene(event.scenePos())
        item = self.__itemDataAtPos(pos)
        if item is None:
            return
        data, index, rect = item
        if data.rownames is None:
            return
        ttip = data.rownames[index]
        if ttip:
            view = event.widget().parentWidget()
            rect = view.mapFromScene(self.mapToScene(rect)).boundingRect()
            show_tool_tip(event.screenPos(), ttip, event.widget(), rect)
            event.setAccepted(True)

    def __setHoveredItem(self, item):
        # Set the current hovered `item` (:class:`QGraphicsRectItem`)
        if self.__hoveredItem is not item:
            if self.__hoveredItem is not None:
                self.__hoveredItem.setPen(QPen(Qt.NoPen))
            self.__hoveredItem = item
            if item is not None:
                item.setPen(QPen(Qt.lightGray))

    def hoverEnterEvent(self, event):
        # Reimplemented
        event.accept()

    def hoverMoveEvent(self, event):
        # Reimplemented
        event.accept()
        item = self.itemAtPos(event.pos())
        self.__setHoveredItem(item)

    def hoverLeaveEvent(self, event):
        # Reimplemented
        self.__setHoveredItem(None)
        event.accept()

    def mousePressEvent(self, event):
        # Reimplemented
        if event.button() == Qt.LeftButton:
            if event.modifiers() & Qt.ControlModifier:
                saction = SelectAction.Toogle
            elif event.modifiers() & Qt.AltModifier:
                saction = SelectAction.Deselect
            elif event.modifiers() & Qt.ShiftModifier:
                saction = SelectAction.Select
            else:
                saction = SelectAction.Clear | SelectAction.Select
            self.__selstate = namespace(
                modifiers=event.modifiers(),
                selection=self.__selection,
                action=saction,
                rect=None,
            )
            if saction & SelectAction.Clear:
                self.__selstate.selection = np.array([], dtype=int)
                self.setSelection(self.__selstate.selection)
            event.accept()

    def keyPressEvent(self, event):
        if event.key() in (Qt.Key_Up, Qt.Key_Down):
            if event.key() == Qt.Key_Up:
                self.__move_selection(self.selection(), -1)
            elif event.key() == Qt.Key_Down:
                self.__move_selection(self.selection(), 1)
            event.accept()
            return
        super().keyPressEvent(event)

    def mouseMoveEvent(self, event):
        # Reimplemented
        if event.buttons() & Qt.LeftButton:
            assert self.__selstate is not None
            if self.__selectionRect is None:
                self.__selectionRect = QGraphicsRectItem(self)

            rect = (QRectF(event.buttonDownPos(Qt.LeftButton),
                           event.pos()).normalized())

            if not rect.width():
                rect = rect.adjusted(-1e-7, -1e-7, 1e-7, 1e-7)

            rect = rect.intersected(self.contentsRect())
            self.__selectionRect.setRect(rect)
            self.__selstate.rect = rect
            self.__selstate.action |= SelectAction.Current

            self.__setSelectionRect(rect, self.__selstate.action)
            event.accept()

    def mouseReleaseEvent(self, event):
        # Reimplemented
        if event.button() == Qt.LeftButton:
            if self.__selectionRect is not None:
                self.__selectionRect.setParentItem(None)
                if self.scene() is not None:
                    self.scene().removeItem(self.__selectionRect)
                self.__selectionRect = None
            event.accept()

            rect = (QRectF(event.buttonDownPos(Qt.LeftButton), event.pos())
                    .normalized())

            if not rect.isValid():
                rect = rect.adjusted(-1e-7, -1e-7, 1e-7, 1e-7)

            rect = rect.intersected(self.contentsRect())
            action = self.__selstate.action & ~SelectAction.Current
            self.__setSelectionRect(rect, action)
            self.__selstate = None

    def __move_selection(self, selection, offset):
        ids = np.asarray([pi.data(0) for pi in self.__plotItems()]).ravel()
        indices = [np.where(ids == i)[0] for i in selection]
        indices = np.asarray(indices) + offset
        if min(indices) >= 0 and max(indices) < len(ids):
            self.setSelection(ids[indices])

    def __setSelectionRect(self, rect, action):
        # Set the current mouse drag selection rectangle
        if not rect.isValid():
            rect = rect.adjusted(-0.01, -0.01, 0.01, 0.01)

        rect = rect.intersected(self.contentsRect())

        indices = self.__selectionIndices(rect)

        if action & SelectAction.Clear:
            selection = []
        elif self.__selstate is not None:
            # Mouse drag selection is in progress. Update only the current
            # selection
            selection = self.__selstate.selection
        else:
            selection = self.__selection

        if action & SelectAction.Toogle:
            selection = np.setxor1d(selection, indices)
        elif action & SelectAction.Deselect:
            selection = np.setdiff1d(selection, indices)
        elif action & SelectAction.Select:
            selection = np.union1d(selection, indices)

        self.setSelection(selection)

    def __selectionIndices(self, rect):
        items = [item for item in self.__plotItems()
                 if item.geometry().intersects(rect)]
        selection = [np.array([], dtype=int)]
        for item in items:
            indices = item.data(0)
            itemrect = item.geometry().intersected(rect)
            crect = item.contentsRect()
            itemrect = (item.mapFromParent(itemrect).boundingRect()
                        .intersected(crect))
            assert itemrect.top() >= 0
            rowh = crect.height() / item.count()
            indextop = np.floor(itemrect.top() / rowh)
            indexbottom = np.ceil(itemrect.bottom() / rowh)
            selection.append(indices[int(indextop): int(indexbottom)])
        return np.hstack(selection)

    def itemAtPos(self, pos):
        items = [item for item in self.__plotItems()
                 if item.geometry().contains(pos)]
        if not items:
            return None
        else:
            item = items[0]
        crect = item.contentsRect()
        pos = item.mapFromParent(pos)
        if not crect.contains(pos):
            return None

        assert pos.x() >= 0
        rowh = crect.height() / item.count()
        index = int(np.floor(pos.y() / rowh))
        index = min(index, item.count() - 1)
        if index >= 0:
            return item.items()[index]
        else:
            return None

    def __itemDataAtPos(self, pos) -> Optional[Tuple[namespace, int, QRectF]]:
        items = [(sitem, tlist, grp) for sitem, tlist, grp
                 in zip(self.__plotItems(), self.__textItems(), self.__groups)
                 if sitem.geometry().contains(pos) or tlist.isVisible()
                 and tlist.geometry().contains(pos)]
        if not items:
            return None
        else:
            sitem, _, grp = items[0]
        indices = grp.indices
        assert (isinstance(indices, np.ndarray) and
                indices.shape == (sitem.count(),))
        crect = sitem.contentsRect()
        pos = sitem.mapFromParent(pos)
        if not crect.top() <= pos.y() <= crect.bottom():
            return None
        rowh = crect.height() / sitem.count()
        index = int(np.floor(pos.y() / rowh))
        index = min(index, indices.size - 1)
        baritem = sitem.items()[index]
        rect = self.mapRectFromItem(baritem, baritem.rect())
        crect = self.contentsRect()
        rect.setLeft(crect.left())
        rect.setRight(crect.right())
        return grp, index, rect

    def __selectionChanged(self, selected, deselected):
        for item, grp in zip(self.__plotItems(), self.__groups):
            select = np.flatnonzero(
                np.in1d(grp.indices, selected, assume_unique=True))
            items = item.items()
            if select.size:
                for i in select:
                    color = np.hstack((grp.color, np.array([130])))
                    items[i].setBrush(QBrush(QColor(*color)))

            deselect = np.flatnonzero(
                np.in1d(grp.indices, deselected, assume_unique=True))
            if deselect.size:
                for i in deselect:
                    items[i].setBrush(QBrush(QColor(*grp.color)))

    def __plotItems(self):
        for i in range(len(self.__groups)):
            item = self.layout().itemAt(i + 1, 2)
            if item is not None:
                assert isinstance(item, BarPlotItem)
                yield item

    def __textItems(self):
        for i in range(len(self.__groups)):
            item = self.layout().itemAt(i + 1, 3)
            if item is not None:
                assert isinstance(item, TextListWidget)
                yield item

    def __axisItems(self):
        return self.__topScale, self.__bottomScale

    def setSelection(self, indices):
        indices = np.unique(np.asarray(indices, dtype=int))
        select = np.setdiff1d(indices, self.__selection)
        deselect = np.setdiff1d(self.__selection, indices)

        self.__selectionChanged(select, deselect)

        self.__selection = indices

        if deselect.size or select.size:
            self.selectionChanged.emit()

    def selection(self):
        return np.asarray(self.__selection, dtype=int)


class Line(QGraphicsWidget):
    """
    A line separator graphics widget
    """
    def __init__(self, parent=None, orientation=Qt.Horizontal, **kwargs):
        sizePolicy = kwargs.pop("sizePolicy", None)
        super().__init__(None, **kwargs)
        self.__orientation = Qt.Horizontal
        if sizePolicy is None:
            sizePolicy = QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
            sizePolicy.setControlType(QSizePolicy.Frame)
            self.setSizePolicy(sizePolicy)
        else:
            self.setSizePolicy(sizePolicy)

        self.setOrientation(orientation)

        if parent is not None:
            self.setParentItem(parent)

    def setOrientation(self, orientation):
        if self.__orientation != orientation:
            self.__orientation = orientation
            sp = self.sizePolicy()
            if orientation == Qt.Vertical:
                sp.setVerticalPolicy(QSizePolicy.Expanding)
                sp.setHorizontalPolicy(QSizePolicy.Fixed)
            else:
                sp.setVerticalPolicy(QSizePolicy.Fixed)
                sp.setHorizontalPolicy(QSizePolicy.Expanding)
            self.setSizePolicy(sp)
            self.updateGeometry()

    def sizeHint(self, which, constraint=QRectF()):
        # type: (Qt.SizeHint, QSizeF) -> QSizeF
        pw = 1.
        sh = QSizeF()
        if which == Qt.MinimumSize:
            sh = QSizeF(pw, pw)
        elif which == Qt.PreferredSize:
            sh = QSizeF(pw, 30.)
        elif which == Qt.MaximumSize:
            sh = QSizeF(pw, QWIDGETSIZE_MAX)

        if self.__orientation == Qt.Horizontal:
            sh.transpose()  # Qt4 compatible
        return sh

    def paint(self, painter, option, widget=None):
        # type: (QPainter, QStyleOptionGraphicsItem, Optional[QWidget]) -> None
        palette = self.palette()  # type: QPalette
        color = palette.color(QPalette.Foreground)
        painter.setPen(QPen(color, 1))
        rect = self.contentsRect()
        center = rect.center()
        if self.__orientation == Qt.Vertical:
            p1 = QPointF(center.x(), rect.top())
            p2 = QPointF(center.x(), rect.bottom())
        elif self.__orientation == Qt.Horizontal:
            p1 = QPointF(rect.left(), center.y())
            p2 = QPointF(rect.right(), center.y())
        else:
            assert False
        painter.drawLine(p1, p2)


class BarPlotItem(QGraphicsWidget):
    def __init__(self, parent=None, **kwargs):
        super().__init__(parent, **kwargs)
        self.__barsize = 5
        self.__spacing = 1
        self.__pen = QPen(Qt.NoPen)
        self.__brush = QBrush(QColor("#3FCFCF"))
        self.__range = (0., 1.)
        self.__data = np.array([], dtype=float)
        self.__items = []

    def count(self):
        return self.__data.size

    def items(self):
        return list(self.__items)

    def setGeometry(self, geom):
        super().setGeometry(geom)
        self.__layout()

    def event(self, event):
        if event.type() == QEvent.GraphicsSceneResize:
            self.__layout()
        return super().event(event)

    def sizeHint(self, which, constraint=QSizeF()):
        spacing = max(self.__spacing * (self.count() - 1), 0)
        return QSizeF(300, self.__barsize * self.count() + spacing)

    def setPreferredBarSize(self, size):
        if self.__barsize != size:
            self.__barsize = size
            self.updateGeometry()

    def spacing(self):
        return self.__spacing

    def setSpacing(self, spacing):
        if self.__spacing != spacing:
            self.__spacing = spacing
            self.updateGeometry()

    def setPen(self, pen):
        pen = QPen(pen)
        if self.__pen != pen:
            self.__pen = pen
            for item in self.__items:
                item.setPen(pen)

    def pen(self):
        return QPen(self.__pen)

    def setBrush(self, brush):
        brush = QBrush(brush)
        if self.__brush != brush:
            self.__brush = brush
            for item in self.__items:
                item.setBrush(brush)

    def brush(self):
        return QBrush(self.__brush)

    def setPlotData(self, values):
        self.__data = np.array(values, copy=True)
        self.__update()
        self.updateGeometry()

    def setDataRange(self, rangemin, rangemax):
        if self.__range != (rangemin, rangemax):
            self.__range = (rangemin, rangemax)
            self.__layout()

    def __clear(self):
        for item in self.__items:
            item.setParentItem(None)
        scene = self.scene()
        if scene is not None:
            for item in self.__items:
                scene.removeItem(item)
        self.__items = []

    def __update(self):
        self.__clear()

        pen = self.pen()
        brush = self.brush()
        for _ in range(self.count()):
            item = QGraphicsRectItem(self)
            item.setPen(pen)
            item.setBrush(brush)
            self.__items.append(item)

        self.__layout()

    def __layout(self):
        (N, ) = self.__data.shape
        if not N:
            return

        spacing = self.__spacing
        rect = self.contentsRect()
        w = rect.width()
        if rect.height() - (spacing * (N - 1)) <= 0:
            spacing = 0

        h = (rect.height() - (spacing * (N - 1))) / N
        xmin, xmax = self.__range
        span = xmax - xmin
        if span < 1e-9:
            span = 1
        scalef = w * 1 / span

        base = 0
        base = (base - xmin) * scalef
        datascaled = (self.__data - xmin) * scalef

        for i, (v, item) in enumerate(zip(datascaled, self.__items)):
            item.setRect(QRectF(base, rect.top() + i * (h + spacing),
                                v - base, h).normalized())


if __name__ == "__main__":  # pragma: no cover
    WidgetPreview(OWSilhouettePlot).run(Orange.data.Table("brown-selected"))

```
--- 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