{
    "file_diffs": [
        {
            "old_file_content": "from itertools import chain\nfrom contextlib import contextmanager\n\nimport typing\nfrom typing import Any, List, Tuple, Dict, Optional, Set, Union\n\nimport numpy as np\n\nfrom AnyQt.QtWidgets import (\n    QGraphicsWidget, QGraphicsScene, QGridLayout, QSizePolicy,\n    QAction, QComboBox, QGraphicsGridLayout, QGraphicsSceneMouseEvent, QLabel\n)\nfrom AnyQt.QtGui import (QPen, QFont, QKeySequence, QPainterPath, QColor,\n    QFontMetrics)\nfrom AnyQt.QtCore import (\n    Qt, QObject, QSize, QPointF, QRectF, QLineF, QEvent, QModelIndex\n)\nfrom AnyQt.QtCore import pyqtSignal as Signal, pyqtSlot as Slot\n\nfrom Orange.widgets.utils.localization import pl\nfrom orangewidget.utils.itemmodels import PyListModel\nfrom orangewidget.utils.signals import LazyValue\n\nimport Orange.data\nfrom Orange.data.domain import filter_visible\nfrom Orange.data import Domain, DiscreteVariable, ContinuousVariable, \\\n    StringVariable, Table\nimport Orange.misc\nfrom Orange.clustering.hierarchical import \\\n    postorder, preorder, Tree, tree_from_linkage, dist_matrix_linkage, \\\n    leaves, prune, top_clusters\nfrom Orange.data.util import get_unique_names\n\nfrom Orange.widgets import widget, gui, settings\nfrom Orange.widgets.utils import itemmodels, combobox\nfrom Orange.widgets.utils.annotated_data import (lazy_annotated_table,\n                                                 ANNOTATED_DATA_SIGNAL_NAME,\n                                                 domain_with_annotation_column,\n                                                 add_columns,\n                                                 create_annotated_table)\nfrom Orange.widgets.utils.widgetpreview import WidgetPreview\nfrom Orange.widgets.visualize.utils.plotutils import AxisItem\nfrom Orange.widgets.widget import Input, Output, Msg\n\nfrom Orange.widgets.utils.stickygraphicsview import StickyGraphicsView\nfrom Orange.widgets.utils.graphicsview import GraphicsWidgetView\nfrom Orange.widgets.utils.graphicstextlist import TextListView\nfrom Orange.widgets.utils.dendrogram import DendrogramWidget\n\n__all__ = [\"OWHierarchicalClustering\"]\n\n\nLINKAGE = [\"Single\", \"Average\", \"Weighted\", \"Complete\", \"Ward\"]\nLINKAGE_ARGS = [\"single\", \"average\", \"weighted\", \"complete\", \"ward\"]\nDEFAULT_LINKAGE = \"Ward\"\n\n\ndef make_pen(brush=Qt.black, width=1, style=Qt.SolidLine,\n             cap_style=Qt.SquareCap, join_style=Qt.BevelJoin,\n             cosmetic=False):\n    pen = QPen(brush)\n    pen.setWidth(width)\n    pen.setStyle(style)\n    pen.setCapStyle(cap_style)\n    pen.setJoinStyle(join_style)\n    pen.setCosmetic(cosmetic)\n    return pen\n\n\n@contextmanager\ndef blocked(obj):\n    old = obj.signalsBlocked()\n    obj.blockSignals(True)\n    try:\n        yield obj\n    finally:\n        obj.blockSignals(old)\n\n\nclass SaveStateSettingsHandler(settings.SettingsHandler):\n    \"\"\"\n    A settings handler that delegates session data store/restore to the\n    OWWidget instance.\n\n    The OWWidget subclass must implement `save_state() -> Dict[str, Any]` and\n    `set_restore_state(state: Dict[str, Any])` methods.\n    \"\"\"\n    def initialize(self, instance, data=None):\n        super().initialize(instance, data)\n        if data is not None and \"__session_state_data\" in data:\n            session_data = data[\"__session_state_data\"]\n            instance.set_restore_state(session_data)\n\n    def pack_data(self, widget):\n        # type: (widget.OWWidget) -> dict\n        res = super().pack_data(widget)\n        state = widget.save_state()\n        if state:\n            assert \"__session_state_data\" not in res\n            res[\"__session_state_data\"] = state\n        return res\n\n\nclass _DomainContextHandler(settings.DomainContextHandler,\n                            SaveStateSettingsHandler):\n    pass\n\n\nif typing.TYPE_CHECKING:\n    #: Encoded selection state for persistent storage.\n    #: This is a list of tuples of leaf indices in the selection and\n    #: a (N, 3) linkage matrix for validation (the 4-th column from scipy\n    #: is omitted).\n    SelectionState = Tuple[List[Tuple[int]], List[Tuple[int, int, float]]]\n\n\nclass SelectedLabelsModel(PyListModel):\n    def __init__(self):\n        super().__init__([])\n        self.subset = set()\n        self.__font = QFont()\n        self.__colors = None\n\n    def rowCount(self, parent=QModelIndex()):\n        count = super().rowCount()\n        if self.__colors is not None:\n            count = max(count, len(self.__colors))\n        return count\n\n    def _emit_data_changed(self):\n        self.dataChanged.emit(self.index(0, 0), self.index(len(self) - 1, 0))\n\n    def set_subset(self, subset):\n        self.subset = set(subset)\n        self._emit_data_changed()\n\n    def set_colors(self, colors):\n        self.__colors = colors\n        self._emit_data_changed()\n\n    def setFont(self, font):\n        self.__font = font\n        self._emit_data_changed()\n\n    def data(self, index, role=Qt.DisplayRole):\n        if role == Qt.FontRole:\n            font = QFont(self.__font)\n            font.setBold(index.row() in self.subset)\n            return font\n        if role == Qt.BackgroundRole:\n            if self.__colors is not None:\n                return self.__colors[index.row()]\n            elif not any(self) and self.subset:  # no labels, no color, but subset\n                return QColor(0, 0, 0)\n        if role == Qt.UserRole and self.subset:\n            return index.row() in self.subset\n\n        return super().data(index, role)\n\n\nclass GraphicsView(GraphicsWidgetView, StickyGraphicsView):\n    def minimumSizeHint(self) -> QSize:\n        msh = super().minimumSizeHint()\n        w = self.centralWidget()\n        if w is not None:\n            width = w.minimumWidth() + 4 + self.verticalScrollBar().width()\n            msh.setWidth(max(int(width), msh.width()))\n        return msh\n\n    def eventFilter(self, recv: QObject, event: QEvent) -> bool:\n        ret = super().eventFilter(recv, event)\n        if event.type() == QEvent.LayoutRequest and recv is self.centralWidget():\n            self.updateGeometry()\n        return ret\n\n\nclass OWHierarchicalClustering(widget.OWWidget):\n    name = \"Hierarchical Clustering\"\n    description = \"Display a dendrogram of a hierarchical clustering \" \\\n                  \"constructed from the input distance matrix.\"\n    icon = \"icons/HierarchicalClustering.svg\"\n    priority = 2100\n    keywords = \"hierarchical clustering\"\n\n    class Inputs:\n        distances = Input(\"Distances\", Orange.misc.DistMatrix)\n        subset = Input(\"Data Subset\", Orange.data.Table, explicit=True)\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    settings_version = 2\n    settingsHandler = _DomainContextHandler()\n\n    #: Selected linkage\n    linkage = settings.Setting(LINKAGE.index(DEFAULT_LINKAGE))\n    #: Index of the selected annotation item (variable, ...)\n    annotation = settings.ContextSetting(\"Enumeration\")\n    #: Out-of-context setting for the case when the \"Name\" option is available\n    annotation_if_names = settings.Setting(\"Name\")\n    #: Out-of-context setting for the case with just \"Enumeration\" and \"None\"\n    annotation_if_enumerate = settings.Setting(\"Enumeration\")\n    #: Selected tree pruning (none/max depth)\n    pruning = settings.Setting(0)\n    #: Maximum depth when max depth pruning is selected\n    max_depth = settings.Setting(10)\n\n    #: Selected cluster selection method (none, cut distance, top n)\n    selection_method = settings.Setting(0)\n    #: Cut height ratio wrt root height\n    cut_ratio = settings.Setting(75.0)\n    #: Number of top clusters to select\n    top_n = settings.Setting(3)\n    #: Dendrogram zoom factor\n    zoom_factor = settings.Setting(0)\n    #: Show labels only for subset (if present)\n    label_only_subset = settings.Setting(False)\n    #: Color for label decoration\n    color_by: Union[DiscreteVariable, ContinuousVariable, None] = \\\n        settings.ContextSetting(None)\n\n    autocommit = settings.Setting(True)\n\n    graph_name = \"scene\"  # QGraphicsScene\n\n    basic_annotations = [None, \"Enumeration\"]\n\n    class Error(widget.OWWidget.Error):\n        empty_matrix = Msg(\"Distance matrix is empty.\")\n        not_finite_distances = Msg(\"Some distances are infinite\")\n        not_symmetric = widget.Msg(\"Distance matrix is not symmetric.\")\n\n    class Warning(widget.OWWidget.Warning):\n        subset_on_no_table = \\\n            Msg(\"Unused data subset: distances do not refer to data instances\")\n        subset_not_subset = \\\n            Msg(\"Some data from the subset does not appear in distance matrix\")\n        subset_wrong = \\\n            Msg(\"Subset data refers to a different table\")\n        pruning_disables_colors = \\\n            Msg(\"Pruned cluster doesn't show colors and indicate subset\")\n\n    #: Stored (manual) selection state (from a saved workflow) to restore.\n    __pending_selection_restore = None  # type: Optional[SelectionState]\n\n    def __init__(self):\n        super().__init__()\n\n        self.matrix = None\n        self.items = None\n        self.subset = None\n        self.subset_rows = set()\n        self.linkmatrix = None\n        self.root = None\n        self._displayed_root = None\n        self.cutoff_height = 0.0\n\n        spin_width = QFontMetrics(self.font()).horizontalAdvance(\"M\" * 7)\n        gui.comboBox(\n            self.controlArea, self, \"linkage\", items=LINKAGE, box=\"Linkage\",\n            callback=self._invalidate_clustering)\n\n        model = itemmodels.VariableListModel(placeholder=\"None\")\n        model[:] = self.basic_annotations\n\n        grid = QGridLayout()\n        gui.widgetBox(self.controlArea, \"Annotations\", orientation=grid)\n        self.label_cb = cb = combobox.ComboBoxSearch(\n            minimumContentsLength=14,\n            sizeAdjustPolicy=QComboBox.AdjustToMinimumContentsLengthWithIcon\n        )\n        cb.setModel(model)\n        cb.setCurrentIndex(cb.findData(self.annotation, Qt.EditRole))\n\n        def on_annotation_activated():\n            self.annotation = self.label_cb.currentData(Qt.EditRole)\n            self._update_labels()\n        cb.activated.connect(on_annotation_activated)\n\n        def on_annotation_changed(value):\n            self.label_cb.setCurrentIndex(\n                self.label_cb.findData(value, Qt.EditRole))\n        self.connect_control(\"annotation\", on_annotation_changed)\n\n        grid.addWidget(self.label_cb, 0, 0, 1, 2)\n\n        cb = gui.checkBox(\n            None, self, \"label_only_subset\", \"Show labels only for subset\",\n            disabled=True,\n            callback=self._update_labels, stateWhenDisabled=False)\n        grid.addWidget(cb, 1, 0, 1, 2)\n\n        model = itemmodels.DomainModel(\n            valid_types=(DiscreteVariable, ContinuousVariable),\n            placeholder=\"None\")\n        cb = gui.comboBox(\n            None, self, \"color_by\", orientation=Qt.Horizontal,\n            model=model, callback=self._update_labels,\n            sizePolicy=QSizePolicy(QSizePolicy.MinimumExpanding,\n                                   QSizePolicy.Fixed))\n        self.color_by_label = QLabel(\"Color by:\")\n        grid.addWidget(self.color_by_label, 2, 0)\n        grid.addWidget(cb, 2, 1)\n\n        box = gui.radioButtons(\n            self.controlArea, self, \"pruning\", box=\"Pruning\",\n            callback=self._invalidate_pruning)\n        grid = QGridLayout()\n        box.layout().addLayout(grid)\n        grid.addWidget(\n            gui.appendRadioButton(box, \"None\", addToLayout=False),\n            0, 0\n        )\n        self.max_depth_spin = gui.spin(\n            box, self, \"max_depth\", minv=1, maxv=100,\n            controlWidth=spin_width, alignment=Qt.AlignRight,\n            callback=self._max_depth_changed,\n            keyboardTracking=False, addToLayout=False\n        )\n        self.max_depth_spin.lineEdit().returnPressed.connect(\n            self._max_depth_return)\n\n        grid.addWidget(\n            gui.appendRadioButton(box, \"Max depth:\", addToLayout=False),\n            1, 0)\n        grid.addWidget(self.max_depth_spin, 1, 1)\n\n        self.selection_box = gui.radioButtons(\n            self.controlArea, self, \"selection_method\",\n            box=\"Selection\",\n            callback=self._selection_method_changed)\n\n        grid = QGridLayout()\n        self.selection_box.layout().addLayout(grid)\n        grid.addWidget(\n            gui.appendRadioButton(\n                self.selection_box, \"Manual\", addToLayout=False),\n            0, 0\n        )\n        grid.addWidget(\n            gui.appendRadioButton(\n                self.selection_box, \"Height ratio:\", addToLayout=False),\n            1, 0\n        )\n        self.cut_ratio_spin = gui.spin(\n            self.selection_box, self, \"cut_ratio\", 0, 100, step=1e-1,\n            controlWidth=spin_width, alignment = Qt.AlignRight,\n            spinType=float, callback=self._cut_ratio_changed,\n            addToLayout=False\n        )\n        self.cut_ratio_spin.setSuffix(\" %\")\n        self.cut_ratio_spin.lineEdit().returnPressed.connect(\n            self._cut_ratio_return)\n\n        grid.addWidget(self.cut_ratio_spin, 1, 1)\n\n        grid.addWidget(\n            gui.appendRadioButton(\n                self.selection_box, \"Top N:\", addToLayout=False),\n            2, 0\n        )\n        self.top_n_spin = gui.spin(\n            self.selection_box, self, \"top_n\", 1, 20,\n            controlWidth=spin_width, alignment=Qt.AlignRight,\n            callback=self._top_n_changed, addToLayout=False)\n        self.top_n_spin.lineEdit().returnPressed.connect(self._top_n_return)\n        grid.addWidget(self.top_n_spin, 2, 1)\n\n        self.zoom_slider = gui.hSlider(\n            self.controlArea, self, \"zoom_factor\", box=\"Zoom\",\n            minValue=-6, maxValue=3, step=1, ticks=True, createLabel=False,\n            callback=self.__update_font_scale)\n\n        zoom_in = QAction(\n            \"Zoom in\", self, shortcut=QKeySequence.ZoomIn,\n            triggered=self.__zoom_in\n        )\n        zoom_out = QAction(\n            \"Zoom out\", self, shortcut=QKeySequence.ZoomOut,\n            triggered=self.__zoom_out\n        )\n        zoom_reset = QAction(\n            \"Reset zoom\", self,\n            shortcut=QKeySequence(Qt.ControlModifier | Qt.Key_0),\n            triggered=self.__zoom_reset\n        )\n        self.addActions([zoom_in, zoom_out, zoom_reset])\n\n        self.controlArea.layout().addStretch()\n\n        gui.auto_send(self.buttonsArea, self, \"autocommit\")\n\n        self.scene = QGraphicsScene(self)\n        self.view = GraphicsView(\n            self.scene,\n            horizontalScrollBarPolicy=Qt.ScrollBarAlwaysOff,\n            verticalScrollBarPolicy=Qt.ScrollBarAlwaysOn,\n            alignment=Qt.AlignLeft | Qt.AlignVCenter,\n            widgetResizable=True,\n        )\n        # Disable conflicting action shortcuts. We define our own.\n        for a in self.view.viewActions():\n            a.setEnabled(False)\n        self.mainArea.layout().setSpacing(1)\n        self.mainArea.layout().addWidget(self.view)\n\n        def axis_view(orientation):\n            ax = AxisItem(orientation=orientation, maxTickLength=7)\n            ax.mousePressed.connect(self._activate_cut_line)\n            ax.mouseMoved.connect(self._activate_cut_line)\n            ax.mouseReleased.connect(self._activate_cut_line)\n            ax.setRange(1.0, 0.0)\n            return ax\n\n        self.top_axis = axis_view(\"top\")\n        self.bottom_axis = axis_view(\"bottom\")\n\n        self._main_graphics = QGraphicsWidget(\n            sizePolicy=QSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.Preferred)\n        )\n        scenelayout = QGraphicsGridLayout()\n        scenelayout.setHorizontalSpacing(10)\n        scenelayout.setVerticalSpacing(10)\n\n        self._main_graphics.setLayout(scenelayout)\n        self.scene.addItem(self._main_graphics)\n        self.view.setCentralWidget(self._main_graphics)\n        self.scene.addItem(self._main_graphics)\n\n        self.dendrogram = DendrogramWidget(pen_width=2)\n        self.dendrogram.setSizePolicy(QSizePolicy.MinimumExpanding,\n                                      QSizePolicy.MinimumExpanding)\n        self.dendrogram.selectionChanged.connect(self._invalidate_output)\n        self.dendrogram.selectionEdited.connect(self._selection_edited)\n\n        self.labels = TextListView()\n        self.label_model = SelectedLabelsModel()\n        self.labels.setModel(self.label_model)\n        self.labels.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Preferred)\n        self.labels.setAlignment(Qt.AlignLeft)\n        self.labels.setMaximumWidth(200)\n\n        scenelayout.addItem(self.top_axis, 0, 0,\n                            alignment=Qt.AlignLeft | Qt.AlignVCenter)\n        scenelayout.addItem(self.dendrogram, 1, 0,\n                            alignment=Qt.AlignLeft | Qt.AlignVCenter)\n        scenelayout.addItem(self.labels, 1, 1,\n                            alignment=Qt.AlignLeft | Qt.AlignVCenter)\n        scenelayout.addItem(self.bottom_axis, 2, 0,\n                            alignment=Qt.AlignLeft | Qt.AlignVCenter)\n        self.top_axis.setZValue(self.dendrogram.zValue() + 10)\n        self.bottom_axis.setZValue(self.dendrogram.zValue() + 10)\n        self.cut_line = SliderLine(self.top_axis,\n                                   orientation=Qt.Horizontal)\n        self.cut_line.valueChanged.connect(self._dendrogram_slider_changed)\n        self.dendrogram.geometryChanged.connect(self._dendrogram_geom_changed)\n        self._set_cut_line_visible(self.selection_method == 1)\n        self.__update_font_scale()\n\n    @Inputs.distances\n    def set_distances(self, matrix):\n        self.error()\n        self.Error.clear()\n\n        self.matrix = None\n        self.Error.clear()\n        if matrix is not None:\n            if len(matrix) < 2:\n                self.Error.empty_matrix()\n            elif not matrix.is_symmetric():\n                self.Error.not_symmetric()\n            elif not np.all(np.isfinite(matrix)):\n                self.Error.not_finite_distances()\n            else:\n                self.matrix = matrix\n\n    @Inputs.subset\n    def set_subset(self, subset):\n        self.subset = subset\n        self.controls.label_only_subset.setDisabled(subset is None)\n\n    def handleNewSignals(self):\n        if self.__pending_selection_restore is not None:\n            selection_state = self.__pending_selection_restore\n        else:\n            # save the current selection to (possibly) restore later\n            selection_state = self._save_selection()\n\n        matrix = self.matrix\n        if matrix is not None:\n            self._set_items(matrix.row_items, matrix.axis)\n        else:\n            self._set_items(None)\n        self._update()\n\n        # Can now attempt to restore session state from a saved workflow.\n        if self.root and selection_state is not None:\n            self._restore_selection(selection_state)\n            self.__pending_selection_restore = None\n\n        self.Warning.clear()\n        rows = set()\n        if self.subset:\n            subsetids = set(self.subset.ids)\n            if not isinstance(self.items, Orange.data.Table) \\\n                    or not self.matrix.axis:\n                self.Warning.subset_on_no_table()\n            elif (dataids := set(self.items.ids)) and not subsetids & dataids:\n                self.Warning.subset_wrong()\n            elif not subsetids <= dataids:\n                self.Warning.subset_not_subset()\n            else:\n                indices = [leaf.value.index for leaf in leaves(self.root)]\n                rows = {\n                    row for row, rowid in enumerate(self.items.ids[indices])\n                    if rowid in subsetids\n                }\n\n        self.subset_rows = rows\n        self._update_labels()\n        self.commit.now()\n\n    def _set_items(self, items, axis=1):\n        self.closeContext()\n        self.items = items\n        model = self.label_cb.model()\n        color_model = self.controls.color_by.model()\n        color_model.set_domain(None)\n        self.color_by = None\n        if len(model) == 2 and model[0] is None:\n            if model[1] == \"Name\":\n                self.annotation_if_names = self.annotation\n            if model[1] == self.basic_annotations[1]:\n                self.annotation_if_enumerate = self.annotation\n        if isinstance(items, Orange.data.Table) and axis:\n            metas_class = tuple(\n                filter_visible(chain(items.domain.metas,\n                                     items.domain.class_vars)))\n            visible_attrs = tuple(filter_visible(items.domain.attributes))\n            if not (metas_class or visible_attrs):\n                model[:] = self.basic_annotations\n            else:\n                model[:] = (\n                    (None, )\n                    + metas_class\n                    + (model.Separator, ) * bool(metas_class and visible_attrs)\n                    + visible_attrs)\n            for meta in items.domain.metas:\n                if isinstance(meta, StringVariable):\n                    self.annotation = meta\n                    break\n            else:\n                if items.domain.class_vars:\n                    # No string metas: show class\n                    self.annotation = items.domain.class_vars[0]\n                else:\n                    # No string metas and no class: show the first option\n                    # which is not None (in the worst case, Enumeration)\n                    self.annotation = model[1]\n\n            color_model.set_domain(items.domain)\n            if items.domain.class_vars:\n                self.color_by = items.domain.class_vars[0]\n            self.openContext(items.domain)\n        elif isinstance(items, Orange.data.Table) and not axis \\\n                or (isinstance(items, list) and \\\n                    all(isinstance(var, Orange.data.Variable)\n                        for var in items)):\n            model[:] = (None, \"Name\")\n            self.annotation = self.annotation_if_names\n        else:\n            model[:] = self.basic_annotations\n            self.annotation = self.annotation_if_enumerate\n\n        no_colors = len(color_model) == 1\n        self.controls.color_by.setDisabled(no_colors)\n        self.color_by_label.setDisabled(no_colors)\n\n    def _clear_plot(self):\n        self.dendrogram.set_root(None)\n        self.label_model.clear()\n\n    def _set_displayed_root(self, root):\n        self._clear_plot()\n        self._displayed_root = root\n        self.dendrogram.set_root(root)\n        self._update_labels()\n\n    def _update(self):\n        self._clear_plot()\n\n        distances = self.matrix\n\n        if distances is not None:\n            method = LINKAGE_ARGS[self.linkage]\n            Z = dist_matrix_linkage(distances, linkage=method)\n\n            tree = tree_from_linkage(Z)\n            self.linkmatrix = Z\n            self.root = tree\n\n            self.top_axis.setRange(tree.value.height, 0.0)\n            self.bottom_axis.setRange(tree.value.height, 0.0)\n\n            if self.pruning:\n                self._set_displayed_root(prune(tree, level=self.max_depth))\n            else:\n                self._set_displayed_root(tree)\n        else:\n            self.linkmatrix = None\n            self.root = None\n            self._set_displayed_root(None)\n\n        self._apply_selection()\n\n    def _update_labels(self):\n        if not hasattr(self, \"label_model\"):\n            # This method can be called during widget initialization when\n            # creating check box for label_only_subset, if it's value is\n            # initially True.\n            # See https://github.com/biolab/orange-widget-base/pull/213;\n            # if it's merged, this check can be removed.\n            return\n\n        self.Warning.pruning_disables_colors(\n            shown=self.pruning\n                  and (self.subset_rows or self.color_by is not None))\n        labels = []\n        if self.root and self._displayed_root:\n            indices = [leaf.value.index for leaf in leaves(self.root)]\n\n            if self.annotation is None:\n                if not self.pruning \\\n                        and self.subset_rows and self.color_by is None:\n                    # Model fails if number of labels and of colors mismatch\n                    labels = [\"\"] * len(indices)\n                else:\n                    labels = []\n            elif self.annotation == \"Enumeration\":\n                labels = [str(i+1) for i in indices]\n            elif self.annotation == \"Name\":\n                attr = self.matrix.row_items.domain.attributes\n                labels = [str(attr[i]) for i in indices]\n            elif isinstance(self.annotation, Orange.data.Variable):\n                col_data = self.items.get_column(self.annotation)\n                labels = [self.annotation.str_val(val) for val in col_data]\n                labels = [labels[idx] for idx in indices]\n            else:\n                labels = []\n            if not self.pruning and \\\n                    labels and self.label_only_subset and self.subset_rows:\n                labels = [label if row in self.subset_rows else \"\"\n                          for row, label in enumerate(labels)]\n\n            if labels and self._displayed_root is not self.root:\n                joined = leaves(self._displayed_root)\n                labels = [\", \".join(labels[leaf.value.first: leaf.value.last])\n                          for leaf in joined]\n\n        self.label_model[:] = labels\n        self.label_model.set_subset(set() if self.pruning else self.subset_rows)\n        self.labels.setMinimumWidth(1 if labels else -1)\n\n        if not self.pruning and self.color_by is not None:\n            col = self.items.get_column(self.color_by)\n            self.label_model.set_colors(\n                self.color_by.palette.values_to_qcolors(col[indices]))\n        else:\n            self.label_model.set_colors(None)\n\n    def _restore_selection(self, state):\n        # type: (SelectionState) -> bool\n        \"\"\"\n        Restore the (manual) node selection state.\n\n        Return True if successful; False otherwise.\n        \"\"\"\n        linkmatrix = self.linkmatrix\n        if self.selection_method == 0 and self.root:\n            selected, linksaved = state\n            linkstruct = np.array(linksaved, dtype=float)\n            selected = set(selected)  # type: Set[Tuple[int]]\n            if not selected:\n                return False\n            if linkmatrix.shape[0] != linkstruct.shape[0]:\n                return False\n            # check that the linkage matrix structure matches. Use isclose for\n            # the height column to account for inexact floating point math\n            # (e.g. summation order in different ?gemm implementations for\n            # euclidean distances, ...)\n            if np.any(linkstruct[:, :2] != linkmatrix[:, :2]) or \\\n                    not np.all(np.isclose(linkstruct[:, 2], linkstruct[:, 2])):\n                return False\n            selection = []\n            indices = np.array([n.value.index for n in leaves(self.root)],\n                               dtype=int)\n            # mapping from ranges to display (pruned) nodes\n            mapping = {node.value.range: node\n                       for node in postorder(self._displayed_root)}\n            for node in postorder(self.root):  # type: Tree\n                r = tuple(indices[node.value.first: node.value.last])\n                if r in selected:\n                    if node.value.range not in mapping:\n                        # the node was pruned from display and cannot be\n                        # selected\n                        break\n                    selection.append(mapping[node.value.range])\n                    selected.remove(r)\n                if not selected:\n                    break  # found all, nothing more to do\n            if selection and selected:\n                # Could not restore all selected nodes (only partial match)\n                return False\n\n            self._set_selected_nodes(selection)\n            return True\n        return False\n\n    def _set_selected_nodes(self, selection):\n        # type: (List[Tree]) -> None\n        \"\"\"\n        Set the nodes in `selection` to be the current selected nodes.\n\n        The selection nodes must be subtrees of the current `_displayed_root`.\n        \"\"\"\n        self.dendrogram.selectionChanged.disconnect(self._invalidate_output)\n        try:\n            self.dendrogram.set_selected_clusters(selection)\n        finally:\n            self.dendrogram.selectionChanged.connect(self._invalidate_output)\n\n    def _max_depth_return(self):\n        if self.pruning != 1:\n            self.pruning = 1\n            self._invalidate_pruning()\n\n    def _max_depth_changed(self):\n        self.pruning = 1\n        self._invalidate_pruning()\n\n    def _invalidate_clustering(self):\n        self._update()\n        self._update_labels()\n        self._invalidate_output()\n\n    def _invalidate_output(self):\n        self.commit.deferred()\n\n    def _invalidate_pruning(self):\n        if self.root:\n            selection = self.dendrogram.selected_nodes()\n            ranges = [node.value.range for node in selection]\n            if self.pruning:\n                self._set_displayed_root(\n                    prune(self.root, level=self.max_depth))\n            else:\n                self._set_displayed_root(self.root)\n            selected = [node for node in preorder(self._displayed_root)\n                        if node.value.range in ranges]\n\n            self.dendrogram.set_selected_clusters(selected)\n\n        self._apply_selection()\n\n    @gui.deferred\n    def commit(self):\n        items = getattr(self.matrix, \"items\", self.items)\n        if not items:\n            self.Outputs.selected_data.send(None)\n            self.Outputs.annotated_data.send(None)\n            return\n\n        selection = self.dendrogram.selected_nodes()\n        selection = sorted(selection, key=lambda c: c.value.first)\n\n        indices = [leaf.value.index for leaf in leaves(self.root)]\n\n        maps = [indices[node.value.first:node.value.last]\n                for node in selection]\n\n        selected_indices = list(chain(*maps))\n\n        if not selected_indices:\n            self.Outputs.selected_data.send(None)\n            annotated_data = lazy_annotated_table(items, []) \\\n                if self.selection_method == 0 and self.matrix.axis else None\n            self.Outputs.annotated_data.send(annotated_data)\n            return\n\n        selected_data = annotated_data = None\n\n        if isinstance(items, Orange.data.Table) and self.matrix.axis == 1:\n            # Select rows\n            data, domain = items, items.domain\n\n            c = np.full(self.matrix.shape[0], len(maps))\n            for i, indices in enumerate(maps):\n                c[indices] = i\n\n            clust_name = get_unique_names(domain, \"Cluster\")\n            values = [f\"C{i + 1}\" for i in range(len(maps))]\n\n            sel_clust_var = Orange.data.DiscreteVariable(\n                name=clust_name, values=values)\n            sel_domain = add_columns(domain, metas=(sel_clust_var,))\n            selected_data = LazyValue[Table](\n                lambda: items.add_column(\n                    sel_clust_var, c, to_metas=True)[c != len(maps)],\n                domain=sel_domain, length=len(selected_indices))\n\n            ann_clust_var = Orange.data.DiscreteVariable(\n                name=clust_name, values=values + [\"Other\"]\n            )\n            ann_domain = add_columns(\n                domain_with_annotation_column(data)[0], metas=(ann_clust_var, ))\n            annotated_data = LazyValue[Table](\n                lambda: create_annotated_table(\n                    data=items.add_column(ann_clust_var, c, to_metas=True),\n                    selected_indices=selected_indices),\n                domain=ann_domain, length=len(items)\n            )\n\n        elif isinstance(items, Orange.data.Table) and self.matrix.axis == 0:\n            # Select columns\n            attrs = []\n            unselected_indices = sorted(set(range(self.root.value.last)) -\n                                        set(selected_indices))\n            for clust, indices in chain(enumerate(maps, start=1),\n                                        [(0, unselected_indices)]):\n                for i in indices:\n                    attr = items.domain[i].copy()\n                    attr.attributes[\"cluster\"] = clust\n                    attrs.append(attr)\n            all_domain = Orange.data.Domain(\n                # len(unselected_indices) can be 0\n                attrs[:len(attrs) - len(unselected_indices)],\n                items.domain.class_vars, items.domain.metas)\n\n            selected_data = LazyValue[Table](\n                lambda: items.from_table(all_domain, items),\n                domain=all_domain, length=len(items))\n\n            sel_domain = Orange.data.Domain(\n                attrs,\n                items.domain.class_vars, items.domain.metas)\n            annotated_data = LazyValue[Table](\n                lambda: items.from_table(sel_domain, items),\n                domain=sel_domain, length=len(items))\n\n        self.Outputs.selected_data.send(selected_data)\n        self.Outputs.annotated_data.send(annotated_data)\n\n    @Slot(QPointF)\n    def _activate_cut_line(self, pos: QPointF):\n        \"\"\"Activate cut line selection an set cut value to `pos.x()`.\"\"\"\n        self.selection_method = 1\n        self.cut_line.setValue(pos.x())\n        self._selection_method_changed()\n\n    def onDeleteWidget(self):\n        super().onDeleteWidget()\n        self._clear_plot()\n        self.dendrogram.clear()\n        self.dendrogram.deleteLater()\n\n    def _dendrogram_geom_changed(self):\n        pos = self.dendrogram.pos_at_height(self.cutoff_height)\n        geom = self.dendrogram.geometry()\n        self._set_slider_value(pos.x(), geom.width())\n\n        self.cut_line.setLength(\n            self.bottom_axis.geometry().bottom()\n            - self.top_axis.geometry().top()\n        )\n\n        geom = self._main_graphics.geometry()\n        assert geom.topLeft() == QPointF(0, 0)\n\n        def adjustLeft(rect):\n            rect = QRectF(rect)\n            rect.setLeft(geom.left())\n            return rect\n        margin = 3\n        self.scene.setSceneRect(geom)\n        self.view.setSceneRect(geom)\n        self.view.setHeaderSceneRect(\n            adjustLeft(self.top_axis.geometry()).adjusted(0, 0, 0, margin)\n        )\n        self.view.setFooterSceneRect(\n            adjustLeft(self.bottom_axis.geometry()).adjusted(0, -margin, 0, 0)\n        )\n\n    def _dendrogram_slider_changed(self, value):\n        p = QPointF(value, 0)\n        cl_height = self.dendrogram.height_at(p)\n\n        self.set_cutoff_height(cl_height)\n\n    def _set_slider_value(self, value, span):\n        with blocked(self.cut_line):\n            self.cut_line.setRange(0, span)\n            self.cut_line.setValue(value)\n\n    def set_cutoff_height(self, height):\n        self.cutoff_height = height\n        if self.root:\n            self.cut_ratio = 100 * height / self.root.value.height\n        self.select_max_height(height)\n\n    def _set_cut_line_visible(self, visible):\n        self.cut_line.setVisible(visible)\n\n    def select_top_n(self, n):\n        root = self._displayed_root\n        if root:\n            clusters = top_clusters(root, n)\n            self.dendrogram.set_selected_clusters(clusters)\n\n    def select_max_height(self, height):\n        root = self._displayed_root\n        if root:\n            clusters = clusters_at_height(root, height)\n            self.dendrogram.set_selected_clusters(clusters)\n\n    def _cut_ratio_changed(self):\n        self.selection_method = 1\n        self._selection_method_changed()\n\n    def _cut_ratio_return(self):\n        if self.selection_method != 1:\n            self.selection_method = 1\n            self._selection_method_changed()\n\n    def _top_n_changed(self):\n        self.selection_method = 2\n        self._selection_method_changed()\n\n    def _top_n_return(self):\n        if self.selection_method != 2:\n            self.selection_method = 2\n            self._selection_method_changed()\n\n    def _selection_method_changed(self):\n        self._set_cut_line_visible(self.selection_method == 1)\n        if self.root:\n            self._apply_selection()\n\n    def _apply_selection(self):\n        if not self.root:\n            return\n\n        if self.selection_method == 0:\n            pass\n        elif self.selection_method == 1:\n            height = self.cut_ratio * self.root.value.height / 100\n            self.set_cutoff_height(height)\n            pos = self.dendrogram.pos_at_height(height)\n            self._set_slider_value(pos.x(), self.dendrogram.size().width())\n        elif self.selection_method == 2:\n            self.select_top_n(self.top_n)\n\n    def _selection_edited(self):\n        # Selection was edited by clicking on a cluster in the\n        # dendrogram view.\n        self.selection_method = 0\n        self._selection_method_changed()\n        self._invalidate_output()\n\n    def _save_selection(self):\n        # Save the current manual node selection state\n        selection_state = None\n        if self.selection_method == 0 and self.root:\n            assert self.linkmatrix is not None\n            linkmat = [(int(_0), int(_1), _2)\n                       for _0, _1, _2 in self.linkmatrix[:, :3].tolist()]\n            nodes_ = self.dendrogram.selected_nodes()\n            # match the display (pruned) nodes back (by ranges)\n            mapping = {node.value.range: node for node in postorder(self.root)}\n            nodes = [mapping[node.value.range] for node in nodes_]\n            indices = [tuple(node.value.index for node in leaves(node))\n                       for node in nodes]\n            if nodes:\n                selection_state = (indices, linkmat)\n        return selection_state\n\n    def save_state(self):\n        # type: () -> Dict[str, Any]\n        \"\"\"\n        Save state for `set_restore_state`\n        \"\"\"\n        selection = self._save_selection()\n        res = {\"version\": (0, 0, 0)}\n        if selection is not None:\n            res[\"selection_state\"] = selection\n        return res\n\n    def set_restore_state(self, state):\n        # type: (Dict[str, Any]) -> bool\n        \"\"\"\n        Restore session data from a saved state.\n\n        Parameters\n        ----------\n        state : Dict[str, Any]\n\n        NOTE\n        ----\n        This is method called while the instance (self) is being constructed,\n        even before its `__init__` is called. Consider `self` to be only a\n        `QObject` at this stage.\n        \"\"\"\n        if \"selection_state\" in state:\n            selection = state[\"selection_state\"]\n            self.__pending_selection_restore = selection\n        return True\n\n    def __zoom_in(self):\n        def clip(minval, maxval, val):\n            return min(max(val, minval), maxval)\n        self.zoom_factor = clip(self.zoom_slider.minimum(),\n                                self.zoom_slider.maximum(),\n                                self.zoom_factor + 1)\n        self.__update_font_scale()\n\n    def __zoom_out(self):\n        def clip(minval, maxval, val):\n            return min(max(val, minval), maxval)\n        self.zoom_factor = clip(self.zoom_slider.minimum(),\n                                self.zoom_slider.maximum(),\n                                self.zoom_factor - 1)\n        self.__update_font_scale()\n\n    def __zoom_reset(self):\n        self.zoom_factor = 0\n        self.__update_font_scale()\n\n    def __update_font_scale(self):\n        font = self.scene.font()\n        factor = (1.25 ** self.zoom_factor)\n        font = qfont_scaled(font, factor)\n        self._main_graphics.setFont(font)\n        self.label_model.setFont(font)\n\n    def send_report(self):\n        annot = self.label_cb.currentText()\n        if isinstance(self.annotation, str):\n            annot = annot.lower()\n        if self.selection_method == 0:\n            sel = \"manual\"\n        elif self.selection_method == 1:\n            sel = \"at {:.1f} of height\".format(self.cut_ratio)\n        else:\n            sel = f\"top {self.top_n} {pl(self.top_n, 'cluster')}\"\n        self.report_items((\n            (\"Linkage\", LINKAGE[self.linkage]),\n            (\"Annotation\", annot),\n            (\"Pruning\",\n             self.pruning != 0 and \"{} levels\".format(self.max_depth)),\n            (\"Selection\", sel),\n        ))\n        self.report_plot()\n\n    @classmethod\n    def migrate_context(cls, context, version):\n        if version < 2:\n            if context.values[\"annotation\"] == \"None\":\n                context.values[\"annotation\"] = None\n\n\ndef qfont_scaled(font, factor):\n    scaled = QFont(font)\n    if font.pointSizeF() != -1:\n        scaled.setPointSizeF(font.pointSizeF() * factor)\n    elif font.pixelSize() != -1:\n        scaled.setPixelSize(int(font.pixelSize() * factor))\n    return scaled\n\n\nclass AxisItem(AxisItem):\n    mousePressed = Signal(QPointF, Qt.MouseButton)\n    mouseMoved = Signal(QPointF, Qt.MouseButtons)\n    mouseReleased = Signal(QPointF, Qt.MouseButton)\n\n    #: \\reimp\n    def wheelEvent(self, event):\n        event.ignore()  # ignore event to propagate to the view -> scroll\n\n    def mousePressEvent(self, event: QGraphicsSceneMouseEvent) -> None:\n        self.mousePressed.emit(event.pos(), event.button())\n        super().mousePressEvent(event)\n        event.accept()\n\n    def mouseMoveEvent(self, event):\n        self.mouseMoved.emit(event.pos(), event.buttons())\n        super().mouseMoveEvent(event)\n        event.accept()\n\n    def mouseReleaseEvent(self, event):\n        self.mouseReleased.emit(event.pos(), event.button())\n        super().mouseReleaseEvent(event)\n        event.accept()\n\n\nclass SliderLine(QGraphicsWidget):\n    \"\"\"A movable slider line.\"\"\"\n    valueChanged = Signal(float)\n\n    linePressed = Signal()\n    lineMoved = Signal()\n    lineReleased = Signal()\n    rangeChanged = Signal(float, float)\n\n    def __init__(self, parent=None, orientation=Qt.Vertical, value=0.0,\n                 length=10.0, **kwargs):\n        self._orientation = orientation\n        self._value = value\n        self._length = length\n        self._min = 0.0\n        self._max = 1.0\n        self._line: Optional[QLineF] = QLineF()\n        self._pen: Optional[QPen] = None\n        super().__init__(parent, **kwargs)\n        self.setAcceptedMouseButtons(Qt.LeftButton)\n        if self._orientation == Qt.Vertical:\n            self.setCursor(Qt.SizeVerCursor)\n        else:\n            self.setCursor(Qt.SizeHorCursor)\n\n    def setPen(self, pen: Union[QPen, Qt.GlobalColor, Qt.PenStyle]) -> None:\n        pen = QPen(pen)\n        if self._pen != pen:\n            self.prepareGeometryChange()\n            self._pen = pen\n            self._line = None\n            self.update()\n\n    def pen(self) -> QPen:\n        if self._pen is None:\n            return QPen(self.palette().text(), 1.0, Qt.DashLine)\n        else:\n            return QPen(self._pen)\n\n    def setValue(self, value: float):\n        value = min(max(value, self._min), self._max)\n\n        if self._value != value:\n            self.prepareGeometryChange()\n            self._value = value\n            self._line = None\n            self.valueChanged.emit(value)\n\n    def value(self) -> float:\n        return self._value\n\n    def setRange(self, minval: float, maxval: float) -> None:\n        maxval = max(minval, maxval)\n        if minval != self._min or maxval != self._max:\n            self._min = minval\n            self._max = maxval\n            self.rangeChanged.emit(minval, maxval)\n            self.setValue(self._value)\n\n    def setLength(self, length: float):\n        if self._length != length:\n            self.prepareGeometryChange()\n            self._length = length\n            self._line = None\n\n    def length(self) -> float:\n        return self._length\n\n    def setOrientation(self, orientation: Qt.Orientation):\n        if self._orientation != orientation:\n            self.prepareGeometryChange()\n            self._orientation = orientation\n            self._line = None\n            if self._orientation == Qt.Vertical:\n                self.setCursor(Qt.SizeVerCursor)\n            else:\n                self.setCursor(Qt.SizeHorCursor)\n\n    def mousePressEvent(self, event: QGraphicsSceneMouseEvent) -> None:\n        event.accept()\n        self.linePressed.emit()\n\n    def mouseMoveEvent(self, event: QGraphicsSceneMouseEvent) -> None:\n        pos = event.pos()\n        if self._orientation == Qt.Vertical:\n            self.setValue(pos.y())\n        else:\n            self.setValue(pos.x())\n        self.lineMoved.emit()\n        event.accept()\n\n    def mouseReleaseEvent(self, event: QGraphicsSceneMouseEvent) -> None:\n        if self._orientation == Qt.Vertical:\n            self.setValue(event.pos().y())\n        else:\n            self.setValue(event.pos().x())\n        self.lineReleased.emit()\n        event.accept()\n\n    def shape(self) -> QPainterPath:\n        path = QPainterPath()\n        path.addRect(self.boundingRect())\n        return path\n\n    def boundingRect(self) -> QRectF:\n        if self._line is None:\n            if self._orientation == Qt.Vertical:\n                self._line = QLineF(0, self._value, self._length, self._value)\n            else:\n                self._line = QLineF(self._value, 0, self._value, self._length)\n        r = QRectF(self._line.p1(), self._line.p2())\n        penw = self.pen().width()\n        return r.adjusted(-penw, -penw, penw, penw)\n\n    def paint(self, painter, *args):\n        if self._line is None:\n            self.boundingRect()\n\n        painter.save()\n        painter.setPen(self.pen())\n        painter.drawLine(self._line)\n        painter.restore()\n\n\ndef clusters_at_height(root, height):\n    \"\"\"Return a list of clusters by cutting the clustering at `height`.\n    \"\"\"\n    lower = set()\n    cluster_list = []\n    for cl in preorder(root):\n        if cl in lower:\n            continue\n        if cl.value.height < height:\n            cluster_list.append(cl)\n            lower.update(preorder(cl))\n    return cluster_list\n\n\ndef main():\n    # pragma: no cover\n    from Orange import distance  # pylint: disable=import-outside-toplevel\n    data = Orange.data.Table(\"iris\")\n    matrix = distance.Euclidean(distance._preprocess(data))\n    subset = data[10:30]\n    WidgetPreview(OWHierarchicalClustering).run(matrix, set_subset=subset)\n\n\nif __name__ == \"__main__\":  # pragma: no cover\n    main()\n",
            "new_file_content": "from itertools import chain\nfrom contextlib import contextmanager\n\nimport typing\nfrom typing import Any, List, Tuple, Dict, Optional, Set, Union\n\nimport numpy as np\n\nfrom AnyQt.QtWidgets import (\n    QGraphicsWidget, QGraphicsScene, QGridLayout, QSizePolicy,\n    QAction, QComboBox, QGraphicsGridLayout, QGraphicsSceneMouseEvent, QLabel\n)\nfrom AnyQt.QtGui import (QPen, QFont, QKeySequence, QPainterPath, QColor,\n    QFontMetrics)\nfrom AnyQt.QtCore import (\n    Qt, QObject, QSize, QPointF, QRectF, QLineF, QEvent, QModelIndex\n)\nfrom AnyQt.QtCore import pyqtSignal as Signal, pyqtSlot as Slot\n\nfrom Orange.widgets.utils.localization import pl\nfrom orangewidget.utils.itemmodels import PyListModel\nfrom orangewidget.utils.signals import LazyValue\n\nimport Orange.data\nfrom Orange.data.domain import filter_visible\nfrom Orange.data import Domain, DiscreteVariable, ContinuousVariable, \\\n    StringVariable, Table\nimport Orange.misc\nfrom Orange.clustering.hierarchical import \\\n    postorder, preorder, Tree, tree_from_linkage, dist_matrix_linkage, \\\n    leaves, prune, top_clusters\nfrom Orange.data.util import get_unique_names\n\nfrom Orange.widgets import widget, gui, settings\nfrom Orange.widgets.utils import itemmodels, combobox\nfrom Orange.widgets.utils.annotated_data import (lazy_annotated_table,\n                                                 ANNOTATED_DATA_SIGNAL_NAME,\n                                                 domain_with_annotation_column,\n                                                 add_columns,\n                                                 create_annotated_table)\nfrom Orange.widgets.utils.widgetpreview import WidgetPreview\nfrom Orange.widgets.visualize.utils.plotutils import AxisItem\nfrom Orange.widgets.widget import Input, Output, Msg\n\nfrom Orange.widgets.utils.stickygraphicsview import StickyGraphicsView\nfrom Orange.widgets.utils.graphicsview import GraphicsWidgetView\nfrom Orange.widgets.utils.graphicstextlist import TextListView\nfrom Orange.widgets.utils.dendrogram import DendrogramWidget\n\n__all__ = [\"OWHierarchicalClustering\"]\n\n\nLINKAGE = [\"Single\", \"Average\", \"Weighted\", \"Complete\", \"Ward\"]\nLINKAGE_ARGS = [\"single\", \"average\", \"weighted\", \"complete\", \"ward\"]\nDEFAULT_LINKAGE = \"Ward\"\n\n\ndef make_pen(brush=Qt.black, width=1, style=Qt.SolidLine,\n             cap_style=Qt.SquareCap, join_style=Qt.BevelJoin,\n             cosmetic=False):\n    pen = QPen(brush)\n    pen.setWidth(width)\n    pen.setStyle(style)\n    pen.setCapStyle(cap_style)\n    pen.setJoinStyle(join_style)\n    pen.setCosmetic(cosmetic)\n    return pen\n\n\n@contextmanager\ndef blocked(obj):\n    old = obj.signalsBlocked()\n    obj.blockSignals(True)\n    try:\n        yield obj\n    finally:\n        obj.blockSignals(old)\n\n\nclass SaveStateSettingsHandler(settings.SettingsHandler):\n    \"\"\"\n    A settings handler that delegates session data store/restore to the\n    OWWidget instance.\n\n    The OWWidget subclass must implement `save_state() -> Dict[str, Any]` and\n    `set_restore_state(state: Dict[str, Any])` methods.\n    \"\"\"\n    def initialize(self, instance, data=None):\n        super().initialize(instance, data)\n        if data is not None and \"__session_state_data\" in data:\n            session_data = data[\"__session_state_data\"]\n            instance.set_restore_state(session_data)\n\n    def pack_data(self, widget):\n        # type: (widget.OWWidget) -> dict\n        res = super().pack_data(widget)\n        state = widget.save_state()\n        if state:\n            assert \"__session_state_data\" not in res\n            res[\"__session_state_data\"] = state\n        return res\n\n\nclass _DomainContextHandler(settings.DomainContextHandler,\n                            SaveStateSettingsHandler):\n    pass\n\n\nif typing.TYPE_CHECKING:\n    #: Encoded selection state for persistent storage.\n    #: This is a list of tuples of leaf indices in the selection and\n    #: a (N, 3) linkage matrix for validation (the 4-th column from scipy\n    #: is omitted).\n    SelectionState = Tuple[List[Tuple[int]], List[Tuple[int, int, float]]]\n\n\nclass SelectedLabelsModel(PyListModel):\n    def __init__(self):\n        super().__init__([])\n        self.subset = set()\n        self.__font = QFont()\n        self.__colors = None\n\n    def rowCount(self, parent=QModelIndex()):\n        count = super().rowCount()\n        if self.__colors is not None:\n            count = max(count, len(self.__colors))\n        return count\n\n    def _emit_data_changed(self):\n        self.dataChanged.emit(self.index(0, 0), self.index(len(self) - 1, 0))\n\n    def set_subset(self, subset):\n        self.subset = set(subset)\n        self._emit_data_changed()\n\n    def set_colors(self, colors):\n        self.__colors = colors\n        self._emit_data_changed()\n\n    def setFont(self, font):\n        self.__font = font\n        self._emit_data_changed()\n\n    def data(self, index, role=Qt.DisplayRole):\n        if role == Qt.FontRole:\n            font = QFont(self.__font)\n            font.setBold(index.row() in self.subset)\n            return font\n        if role == Qt.BackgroundRole:\n            if self.__colors is not None:\n                return self.__colors[index.row()]\n            elif not any(self) and self.subset:  # no labels, no color, but subset\n                return QColor(0, 0, 0)\n        if role == Qt.UserRole and self.subset:\n            return index.row() in self.subset\n\n        return super().data(index, role)\n\n\nclass GraphicsView(GraphicsWidgetView, StickyGraphicsView):\n    def minimumSizeHint(self) -> QSize:\n        msh = super().minimumSizeHint()\n        w = self.centralWidget()\n        if w is not None:\n            width = w.minimumWidth() + 4 + self.verticalScrollBar().width()\n            msh.setWidth(max(int(width), msh.width()))\n        return msh\n\n    def eventFilter(self, recv: QObject, event: QEvent) -> bool:\n        ret = super().eventFilter(recv, event)\n        if event.type() == QEvent.LayoutRequest and recv is self.centralWidget():\n            self.updateGeometry()\n        return ret\n\n\nclass OWHierarchicalClustering(widget.OWWidget):\n    name = \"Hierarchical Clustering\"\n    description = \"Display a dendrogram of a hierarchical clustering \" \\\n                  \"constructed from the input distance matrix.\"\n    icon = \"icons/HierarchicalClustering.svg\"\n    priority = 2100\n    keywords = \"hierarchical clustering\"\n\n    class Inputs:\n        distances = Input(\"Distances\", Orange.misc.DistMatrix)\n        subset = Input(\"Data Subset\", Orange.data.Table, explicit=True)\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    settings_version = 2\n    settingsHandler = _DomainContextHandler()\n\n    #: Selected linkage\n    linkage = settings.Setting(LINKAGE.index(DEFAULT_LINKAGE))\n    #: Index of the selected annotation item (variable, ...)\n    annotation = settings.ContextSetting(\"Enumeration\")\n    #: Out-of-context setting for the case when the \"Name\" option is available\n    annotation_if_names = settings.Setting(\"Name\")\n    #: Out-of-context setting for the case with just \"Enumeration\" and \"None\"\n    annotation_if_enumerate = settings.Setting(\"Enumeration\")\n    #: Selected tree pruning (none/max depth)\n    pruning = settings.Setting(0)\n    #: Maximum depth when max depth pruning is selected\n    max_depth = settings.Setting(10)\n\n    #: Selected cluster selection method (none, cut distance, top n)\n    selection_method = settings.Setting(0)\n    #: Cut height ratio wrt root height\n    cut_ratio = settings.Setting(75.0)\n    #: Number of top clusters to select\n    top_n = settings.Setting(3)\n    #: Dendrogram zoom factor\n    zoom_factor = settings.Setting(0)\n    #: Show labels only for subset (if present)\n    label_only_subset = settings.Setting(False)\n    #: Color for label decoration\n    color_by: Union[DiscreteVariable, ContinuousVariable, None] = \\\n        settings.ContextSetting(None)\n\n    autocommit = settings.Setting(True)\n\n    graph_name = \"scene\"  # QGraphicsScene\n\n    basic_annotations = [None, \"Enumeration\"]\n\n    class Error(widget.OWWidget.Error):\n        empty_matrix = Msg(\"Distance matrix is empty.\")\n        not_finite_distances = Msg(\"Some distances are infinite\")\n        not_symmetric = widget.Msg(\"Distance matrix is not symmetric.\")\n\n    class Warning(widget.OWWidget.Warning):\n        subset_on_no_table = \\\n            Msg(\"Unused data subset: distances do not refer to data instances\")\n        subset_not_subset = \\\n            Msg(\"Some data from the subset does not appear in distance matrix\")\n        subset_wrong = \\\n            Msg(\"Subset data refers to a different table\")\n        pruning_disables_colors = \\\n            Msg(\"Pruned cluster doesn't show colors and indicate subset\")\n        many_clusters = \\\n            Msg(\"Variables with too many values may \"\n                \"degrade the performance of downstream widgets.\")\n\n    #: Stored (manual) selection state (from a saved workflow) to restore.\n    __pending_selection_restore = None  # type: Optional[SelectionState]\n\n    def __init__(self):\n        super().__init__()\n\n        self.matrix = None\n        self.items = None\n        self.subset = None\n        self.subset_rows = set()\n        self.linkmatrix = None\n        self.root = None\n        self._displayed_root = None\n        self.cutoff_height = 0.0\n\n        spin_width = QFontMetrics(self.font()).horizontalAdvance(\"M\" * 7)\n        gui.comboBox(\n            self.controlArea, self, \"linkage\", items=LINKAGE, box=\"Linkage\",\n            callback=self._invalidate_clustering)\n\n        model = itemmodels.VariableListModel(placeholder=\"None\")\n        model[:] = self.basic_annotations\n\n        grid = QGridLayout()\n        gui.widgetBox(self.controlArea, \"Annotations\", orientation=grid)\n        self.label_cb = cb = combobox.ComboBoxSearch(\n            minimumContentsLength=14,\n            sizeAdjustPolicy=QComboBox.AdjustToMinimumContentsLengthWithIcon\n        )\n        cb.setModel(model)\n        cb.setCurrentIndex(cb.findData(self.annotation, Qt.EditRole))\n\n        def on_annotation_activated():\n            self.annotation = self.label_cb.currentData(Qt.EditRole)\n            self._update_labels()\n        cb.activated.connect(on_annotation_activated)\n\n        def on_annotation_changed(value):\n            self.label_cb.setCurrentIndex(\n                self.label_cb.findData(value, Qt.EditRole))\n        self.connect_control(\"annotation\", on_annotation_changed)\n\n        grid.addWidget(self.label_cb, 0, 0, 1, 2)\n\n        cb = gui.checkBox(\n            None, self, \"label_only_subset\", \"Show labels only for subset\",\n            disabled=True,\n            callback=self._update_labels, stateWhenDisabled=False)\n        grid.addWidget(cb, 1, 0, 1, 2)\n\n        model = itemmodels.DomainModel(\n            valid_types=(DiscreteVariable, ContinuousVariable),\n            placeholder=\"None\")\n        cb = gui.comboBox(\n            None, self, \"color_by\", orientation=Qt.Horizontal,\n            model=model, callback=self._update_labels,\n            sizePolicy=QSizePolicy(QSizePolicy.MinimumExpanding,\n                                   QSizePolicy.Fixed))\n        self.color_by_label = QLabel(\"Color by:\")\n        grid.addWidget(self.color_by_label, 2, 0)\n        grid.addWidget(cb, 2, 1)\n\n        box = gui.radioButtons(\n            self.controlArea, self, \"pruning\", box=\"Pruning\",\n            callback=self._invalidate_pruning)\n        grid = QGridLayout()\n        box.layout().addLayout(grid)\n        grid.addWidget(\n            gui.appendRadioButton(box, \"None\", addToLayout=False),\n            0, 0\n        )\n        self.max_depth_spin = gui.spin(\n            box, self, \"max_depth\", minv=1, maxv=100,\n            controlWidth=spin_width, alignment=Qt.AlignRight,\n            callback=self._max_depth_changed,\n            keyboardTracking=False, addToLayout=False\n        )\n        self.max_depth_spin.lineEdit().returnPressed.connect(\n            self._max_depth_return)\n\n        grid.addWidget(\n            gui.appendRadioButton(box, \"Max depth:\", addToLayout=False),\n            1, 0)\n        grid.addWidget(self.max_depth_spin, 1, 1)\n\n        self.selection_box = gui.radioButtons(\n            self.controlArea, self, \"selection_method\",\n            box=\"Selection\",\n            callback=self._selection_method_changed)\n\n        grid = QGridLayout()\n        self.selection_box.layout().addLayout(grid)\n        grid.addWidget(\n            gui.appendRadioButton(\n                self.selection_box, \"Manual\", addToLayout=False),\n            0, 0\n        )\n        grid.addWidget(\n            gui.appendRadioButton(\n                self.selection_box, \"Height ratio:\", addToLayout=False),\n            1, 0\n        )\n        self.cut_ratio_spin = gui.spin(\n            self.selection_box, self, \"cut_ratio\", 0, 100, step=1e-1,\n            controlWidth=spin_width, alignment = Qt.AlignRight,\n            spinType=float, callback=self._cut_ratio_changed,\n            addToLayout=False\n        )\n        self.cut_ratio_spin.setSuffix(\" %\")\n        self.cut_ratio_spin.lineEdit().returnPressed.connect(\n            self._cut_ratio_return)\n\n        grid.addWidget(self.cut_ratio_spin, 1, 1)\n\n        grid.addWidget(\n            gui.appendRadioButton(\n                self.selection_box, \"Top N:\", addToLayout=False),\n            2, 0\n        )\n        self.top_n_spin = gui.spin(\n            self.selection_box, self, \"top_n\", 1, 1000,\n            controlWidth=spin_width, alignment=Qt.AlignRight,\n            callback=self._top_n_changed, addToLayout=False)\n        self.top_n_spin.lineEdit().returnPressed.connect(self._top_n_return)\n        grid.addWidget(self.top_n_spin, 2, 1)\n\n        self.zoom_slider = gui.hSlider(\n            self.controlArea, self, \"zoom_factor\", box=\"Zoom\",\n            minValue=-6, maxValue=3, step=1, ticks=True, createLabel=False,\n            callback=self.__update_font_scale)\n\n        zoom_in = QAction(\n            \"Zoom in\", self, shortcut=QKeySequence.ZoomIn,\n            triggered=self.__zoom_in\n        )\n        zoom_out = QAction(\n            \"Zoom out\", self, shortcut=QKeySequence.ZoomOut,\n            triggered=self.__zoom_out\n        )\n        zoom_reset = QAction(\n            \"Reset zoom\", self,\n            shortcut=QKeySequence(Qt.ControlModifier | Qt.Key_0),\n            triggered=self.__zoom_reset\n        )\n        self.addActions([zoom_in, zoom_out, zoom_reset])\n\n        self.controlArea.layout().addStretch()\n\n        gui.auto_send(self.buttonsArea, self, \"autocommit\")\n\n        self.scene = QGraphicsScene(self)\n        self.view = GraphicsView(\n            self.scene,\n            horizontalScrollBarPolicy=Qt.ScrollBarAlwaysOff,\n            verticalScrollBarPolicy=Qt.ScrollBarAlwaysOn,\n            alignment=Qt.AlignLeft | Qt.AlignVCenter,\n            widgetResizable=True,\n        )\n        # Disable conflicting action shortcuts. We define our own.\n        for a in self.view.viewActions():\n            a.setEnabled(False)\n        self.mainArea.layout().setSpacing(1)\n        self.mainArea.layout().addWidget(self.view)\n\n        def axis_view(orientation):\n            ax = AxisItem(orientation=orientation, maxTickLength=7)\n            ax.mousePressed.connect(self._activate_cut_line)\n            ax.mouseMoved.connect(self._activate_cut_line)\n            ax.mouseReleased.connect(self._activate_cut_line)\n            ax.setRange(1.0, 0.0)\n            return ax\n\n        self.top_axis = axis_view(\"top\")\n        self.bottom_axis = axis_view(\"bottom\")\n\n        self._main_graphics = QGraphicsWidget(\n            sizePolicy=QSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.Preferred)\n        )\n        scenelayout = QGraphicsGridLayout()\n        scenelayout.setHorizontalSpacing(10)\n        scenelayout.setVerticalSpacing(10)\n\n        self._main_graphics.setLayout(scenelayout)\n        self.scene.addItem(self._main_graphics)\n        self.view.setCentralWidget(self._main_graphics)\n        self.scene.addItem(self._main_graphics)\n\n        self.dendrogram = DendrogramWidget(pen_width=2)\n        self.dendrogram.setSizePolicy(QSizePolicy.MinimumExpanding,\n                                      QSizePolicy.MinimumExpanding)\n        self.dendrogram.selectionChanged.connect(self._invalidate_output)\n        self.dendrogram.selectionEdited.connect(self._selection_edited)\n\n        self.labels = TextListView()\n        self.label_model = SelectedLabelsModel()\n        self.labels.setModel(self.label_model)\n        self.labels.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Preferred)\n        self.labels.setAlignment(Qt.AlignLeft)\n        self.labels.setMaximumWidth(200)\n\n        scenelayout.addItem(self.top_axis, 0, 0,\n                            alignment=Qt.AlignLeft | Qt.AlignVCenter)\n        scenelayout.addItem(self.dendrogram, 1, 0,\n                            alignment=Qt.AlignLeft | Qt.AlignVCenter)\n        scenelayout.addItem(self.labels, 1, 1,\n                            alignment=Qt.AlignLeft | Qt.AlignVCenter)\n        scenelayout.addItem(self.bottom_axis, 2, 0,\n                            alignment=Qt.AlignLeft | Qt.AlignVCenter)\n        self.top_axis.setZValue(self.dendrogram.zValue() + 10)\n        self.bottom_axis.setZValue(self.dendrogram.zValue() + 10)\n        self.cut_line = SliderLine(self.top_axis,\n                                   orientation=Qt.Horizontal)\n        self.cut_line.valueChanged.connect(self._dendrogram_slider_changed)\n        self.dendrogram.geometryChanged.connect(self._dendrogram_geom_changed)\n        self._set_cut_line_visible(self.selection_method == 1)\n        self.__update_font_scale()\n\n    @Inputs.distances\n    def set_distances(self, matrix):\n        self.error()\n        self.Error.clear()\n\n        self.matrix = None\n        self.Error.clear()\n        if matrix is not None:\n            if len(matrix) < 2:\n                self.Error.empty_matrix()\n            elif not matrix.is_symmetric():\n                self.Error.not_symmetric()\n            elif not np.all(np.isfinite(matrix)):\n                self.Error.not_finite_distances()\n            else:\n                self.matrix = matrix\n\n    @Inputs.subset\n    def set_subset(self, subset):\n        self.subset = subset\n        self.controls.label_only_subset.setDisabled(subset is None)\n\n    def handleNewSignals(self):\n        if self.__pending_selection_restore is not None:\n            selection_state = self.__pending_selection_restore\n        else:\n            # save the current selection to (possibly) restore later\n            selection_state = self._save_selection()\n\n        matrix = self.matrix\n        if matrix is not None:\n            self._set_items(matrix.row_items, matrix.axis)\n        else:\n            self._set_items(None)\n        self._update()\n\n        # Can now attempt to restore session state from a saved workflow.\n        if self.root and selection_state is not None:\n            self._restore_selection(selection_state)\n            self.__pending_selection_restore = None\n\n        self.Warning.clear()\n        rows = set()\n        if self.subset:\n            subsetids = set(self.subset.ids)\n            if not isinstance(self.items, Orange.data.Table) \\\n                    or not self.matrix.axis:\n                self.Warning.subset_on_no_table()\n            elif (dataids := set(self.items.ids)) and not subsetids & dataids:\n                self.Warning.subset_wrong()\n            elif not subsetids <= dataids:\n                self.Warning.subset_not_subset()\n            else:\n                indices = [leaf.value.index for leaf in leaves(self.root)]\n                rows = {\n                    row for row, rowid in enumerate(self.items.ids[indices])\n                    if rowid in subsetids\n                }\n\n        self.subset_rows = rows\n        self._update_labels()\n        self.commit.now()\n\n    def _set_items(self, items, axis=1):\n        self.closeContext()\n        self.items = items\n        model = self.label_cb.model()\n        color_model = self.controls.color_by.model()\n        color_model.set_domain(None)\n        self.color_by = None\n        if len(model) == 2 and model[0] is None:\n            if model[1] == \"Name\":\n                self.annotation_if_names = self.annotation\n            if model[1] == self.basic_annotations[1]:\n                self.annotation_if_enumerate = self.annotation\n        if isinstance(items, Orange.data.Table) and axis:\n            metas_class = tuple(\n                filter_visible(chain(items.domain.metas,\n                                     items.domain.class_vars)))\n            visible_attrs = tuple(filter_visible(items.domain.attributes))\n            if not (metas_class or visible_attrs):\n                model[:] = self.basic_annotations\n            else:\n                model[:] = (\n                    (None, )\n                    + metas_class\n                    + (model.Separator, ) * bool(metas_class and visible_attrs)\n                    + visible_attrs)\n            for meta in items.domain.metas:\n                if isinstance(meta, StringVariable):\n                    self.annotation = meta\n                    break\n            else:\n                if items.domain.class_vars:\n                    # No string metas: show class\n                    self.annotation = items.domain.class_vars[0]\n                else:\n                    # No string metas and no class: show the first option\n                    # which is not None (in the worst case, Enumeration)\n                    self.annotation = model[1]\n\n            color_model.set_domain(items.domain)\n            if items.domain.class_vars:\n                self.color_by = items.domain.class_vars[0]\n            self.openContext(items.domain)\n        elif isinstance(items, Orange.data.Table) and not axis \\\n                or (isinstance(items, list) and \\\n                    all(isinstance(var, Orange.data.Variable)\n                        for var in items)):\n            model[:] = (None, \"Name\")\n            self.annotation = self.annotation_if_names\n        else:\n            model[:] = self.basic_annotations\n            self.annotation = self.annotation_if_enumerate\n\n        no_colors = len(color_model) == 1\n        self.controls.color_by.setDisabled(no_colors)\n        self.color_by_label.setDisabled(no_colors)\n\n    def _clear_plot(self):\n        self.dendrogram.set_root(None)\n        self.label_model.clear()\n\n    def _set_displayed_root(self, root):\n        self._clear_plot()\n        self._displayed_root = root\n        self.dendrogram.set_root(root)\n        self._update_labels()\n\n    def _update(self):\n        self._clear_plot()\n\n        distances = self.matrix\n\n        if distances is not None:\n            method = LINKAGE_ARGS[self.linkage]\n            Z = dist_matrix_linkage(distances, linkage=method)\n\n            tree = tree_from_linkage(Z)\n            self.linkmatrix = Z\n            self.root = tree\n\n            self.top_axis.setRange(tree.value.height, 0.0)\n            self.bottom_axis.setRange(tree.value.height, 0.0)\n\n            if self.pruning:\n                self._set_displayed_root(prune(tree, level=self.max_depth))\n            else:\n                self._set_displayed_root(tree)\n        else:\n            self.linkmatrix = None\n            self.root = None\n            self._set_displayed_root(None)\n\n        self._apply_selection()\n\n    def _update_labels(self):\n        if not hasattr(self, \"label_model\"):\n            # This method can be called during widget initialization when\n            # creating check box for label_only_subset, if it's value is\n            # initially True.\n            # See https://github.com/biolab/orange-widget-base/pull/213;\n            # if it's merged, this check can be removed.\n            return\n\n        self.Warning.pruning_disables_colors(\n            shown=self.pruning\n                  and (self.subset_rows or self.color_by is not None))\n        labels = []\n        if self.root and self._displayed_root:\n            indices = [leaf.value.index for leaf in leaves(self.root)]\n\n            if self.annotation is None:\n                if not self.pruning \\\n                        and self.subset_rows and self.color_by is None:\n                    # Model fails if number of labels and of colors mismatch\n                    labels = [\"\"] * len(indices)\n                else:\n                    labels = []\n            elif self.annotation == \"Enumeration\":\n                labels = [str(i+1) for i in indices]\n            elif self.annotation == \"Name\":\n                attr = self.matrix.row_items.domain.attributes\n                labels = [str(attr[i]) for i in indices]\n            elif isinstance(self.annotation, Orange.data.Variable):\n                col_data = self.items.get_column(self.annotation)\n                labels = [self.annotation.str_val(val) for val in col_data]\n                labels = [labels[idx] for idx in indices]\n            else:\n                labels = []\n            if not self.pruning and \\\n                    labels and self.label_only_subset and self.subset_rows:\n                labels = [label if row in self.subset_rows else \"\"\n                          for row, label in enumerate(labels)]\n\n            if labels and self._displayed_root is not self.root:\n                joined = leaves(self._displayed_root)\n                labels = [\", \".join(labels[leaf.value.first: leaf.value.last])\n                          for leaf in joined]\n\n        self.label_model[:] = labels\n        self.label_model.set_subset(set() if self.pruning else self.subset_rows)\n        self.labels.setMinimumWidth(1 if labels else -1)\n\n        if not self.pruning and self.color_by is not None:\n            col = self.items.get_column(self.color_by)\n            self.label_model.set_colors(\n                self.color_by.palette.values_to_qcolors(col[indices]))\n        else:\n            self.label_model.set_colors(None)\n\n    def _restore_selection(self, state):\n        # type: (SelectionState) -> bool\n        \"\"\"\n        Restore the (manual) node selection state.\n\n        Return True if successful; False otherwise.\n        \"\"\"\n        linkmatrix = self.linkmatrix\n        if self.selection_method == 0 and self.root:\n            selected, linksaved = state\n            linkstruct = np.array(linksaved, dtype=float)\n            selected = set(selected)  # type: Set[Tuple[int]]\n            if not selected:\n                return False\n            if linkmatrix.shape[0] != linkstruct.shape[0]:\n                return False\n            # check that the linkage matrix structure matches. Use isclose for\n            # the height column to account for inexact floating point math\n            # (e.g. summation order in different ?gemm implementations for\n            # euclidean distances, ...)\n            if np.any(linkstruct[:, :2] != linkmatrix[:, :2]) or \\\n                    not np.all(np.isclose(linkstruct[:, 2], linkstruct[:, 2])):\n                return False\n            selection = []\n            indices = np.array([n.value.index for n in leaves(self.root)],\n                               dtype=int)\n            # mapping from ranges to display (pruned) nodes\n            mapping = {node.value.range: node\n                       for node in postorder(self._displayed_root)}\n            for node in postorder(self.root):  # type: Tree\n                r = tuple(indices[node.value.first: node.value.last])\n                if r in selected:\n                    if node.value.range not in mapping:\n                        # the node was pruned from display and cannot be\n                        # selected\n                        break\n                    selection.append(mapping[node.value.range])\n                    selected.remove(r)\n                if not selected:\n                    break  # found all, nothing more to do\n            if selection and selected:\n                # Could not restore all selected nodes (only partial match)\n                return False\n\n            self._set_selected_nodes(selection)\n            return True\n        return False\n\n    def _set_selected_nodes(self, selection):\n        # type: (List[Tree]) -> None\n        \"\"\"\n        Set the nodes in `selection` to be the current selected nodes.\n\n        The selection nodes must be subtrees of the current `_displayed_root`.\n        \"\"\"\n        self.dendrogram.selectionChanged.disconnect(self._invalidate_output)\n        try:\n            self.dendrogram.set_selected_clusters(selection)\n        finally:\n            self.dendrogram.selectionChanged.connect(self._invalidate_output)\n\n    def _max_depth_return(self):\n        if self.pruning != 1:\n            self.pruning = 1\n            self._invalidate_pruning()\n\n    def _max_depth_changed(self):\n        self.pruning = 1\n        self._invalidate_pruning()\n\n    def _invalidate_clustering(self):\n        self._update()\n        self._update_labels()\n        self._invalidate_output()\n\n    def _invalidate_output(self):\n        self.commit.deferred()\n\n    def _invalidate_pruning(self):\n        if self.root:\n            selection = self.dendrogram.selected_nodes()\n            ranges = [node.value.range for node in selection]\n            if self.pruning:\n                self._set_displayed_root(\n                    prune(self.root, level=self.max_depth))\n            else:\n                self._set_displayed_root(self.root)\n            selected = [node for node in preorder(self._displayed_root)\n                        if node.value.range in ranges]\n\n            self.dendrogram.set_selected_clusters(selected)\n\n        self._apply_selection()\n\n    @gui.deferred\n    def commit(self):\n        items = getattr(self.matrix, \"items\", self.items)\n        self.Warning.many_clusters.clear()\n        if not items:\n            self.Outputs.selected_data.send(None)\n            self.Outputs.annotated_data.send(None)\n            return\n\n        selection = self.dendrogram.selected_nodes()\n        selection = sorted(selection, key=lambda c: c.value.first)\n\n        indices = [leaf.value.index for leaf in leaves(self.root)]\n\n        maps = [indices[node.value.first:node.value.last]\n                for node in selection]\n        if len(maps) > 20:\n            self.Warning.many_clusters()\n\n        selected_indices = list(chain(*maps))\n\n        if not selected_indices:\n            self.Outputs.selected_data.send(None)\n            annotated_data = lazy_annotated_table(items, []) \\\n                if self.selection_method == 0 and self.matrix.axis else None\n            self.Outputs.annotated_data.send(annotated_data)\n            return\n\n        selected_data = annotated_data = None\n\n        if isinstance(items, Orange.data.Table) and self.matrix.axis == 1:\n            # Select rows\n            data, domain = items, items.domain\n\n            c = np.full(self.matrix.shape[0], len(maps))\n            for i, indices in enumerate(maps):\n                c[indices] = i\n\n            clust_name = get_unique_names(domain, \"Cluster\")\n            values = [f\"C{i + 1}\" for i in range(len(maps))]\n\n            sel_clust_var = Orange.data.DiscreteVariable(\n                name=clust_name, values=values)\n            sel_domain = add_columns(domain, metas=(sel_clust_var,))\n            selected_data = LazyValue[Table](\n                lambda: items.add_column(\n                    sel_clust_var, c, to_metas=True)[c != len(maps)],\n                domain=sel_domain, length=len(selected_indices))\n\n            ann_clust_var = Orange.data.DiscreteVariable(\n                name=clust_name, values=values + [\"Other\"]\n            )\n            ann_domain = add_columns(\n                domain_with_annotation_column(data)[0], metas=(ann_clust_var, ))\n            annotated_data = LazyValue[Table](\n                lambda: create_annotated_table(\n                    data=items.add_column(ann_clust_var, c, to_metas=True),\n                    selected_indices=selected_indices),\n                domain=ann_domain, length=len(items)\n            )\n\n        elif isinstance(items, Orange.data.Table) and self.matrix.axis == 0:\n            # Select columns\n            attrs = []\n            unselected_indices = sorted(set(range(self.root.value.last)) -\n                                        set(selected_indices))\n            for clust, indices in chain(enumerate(maps, start=1),\n                                        [(0, unselected_indices)]):\n                for i in indices:\n                    attr = items.domain[i].copy()\n                    attr.attributes[\"cluster\"] = clust\n                    attrs.append(attr)\n            all_domain = Orange.data.Domain(\n                # len(unselected_indices) can be 0\n                attrs[:len(attrs) - len(unselected_indices)],\n                items.domain.class_vars, items.domain.metas)\n\n            selected_data = LazyValue[Table](\n                lambda: items.from_table(all_domain, items),\n                domain=all_domain, length=len(items))\n\n            sel_domain = Orange.data.Domain(\n                attrs,\n                items.domain.class_vars, items.domain.metas)\n            annotated_data = LazyValue[Table](\n                lambda: items.from_table(sel_domain, items),\n                domain=sel_domain, length=len(items))\n\n        self.Outputs.selected_data.send(selected_data)\n        self.Outputs.annotated_data.send(annotated_data)\n\n    @Slot(QPointF)\n    def _activate_cut_line(self, pos: QPointF):\n        \"\"\"Activate cut line selection an set cut value to `pos.x()`.\"\"\"\n        self.selection_method = 1\n        self.cut_line.setValue(pos.x())\n        self._selection_method_changed()\n\n    def onDeleteWidget(self):\n        super().onDeleteWidget()\n        self._clear_plot()\n        self.dendrogram.clear()\n        self.dendrogram.deleteLater()\n\n    def _dendrogram_geom_changed(self):\n        pos = self.dendrogram.pos_at_height(self.cutoff_height)\n        geom = self.dendrogram.geometry()\n        self._set_slider_value(pos.x(), geom.width())\n\n        self.cut_line.setLength(\n            self.bottom_axis.geometry().bottom()\n            - self.top_axis.geometry().top()\n        )\n\n        geom = self._main_graphics.geometry()\n        assert geom.topLeft() == QPointF(0, 0)\n\n        def adjustLeft(rect):\n            rect = QRectF(rect)\n            rect.setLeft(geom.left())\n            return rect\n        margin = 3\n        self.scene.setSceneRect(geom)\n        self.view.setSceneRect(geom)\n        self.view.setHeaderSceneRect(\n            adjustLeft(self.top_axis.geometry()).adjusted(0, 0, 0, margin)\n        )\n        self.view.setFooterSceneRect(\n            adjustLeft(self.bottom_axis.geometry()).adjusted(0, -margin, 0, 0)\n        )\n\n    def _dendrogram_slider_changed(self, value):\n        p = QPointF(value, 0)\n        cl_height = self.dendrogram.height_at(p)\n\n        self.set_cutoff_height(cl_height)\n\n    def _set_slider_value(self, value, span):\n        with blocked(self.cut_line):\n            self.cut_line.setRange(0, span)\n            self.cut_line.setValue(value)\n\n    def set_cutoff_height(self, height):\n        self.cutoff_height = height\n        if self.root:\n            self.cut_ratio = 100 * height / self.root.value.height\n        self.select_max_height(height)\n\n    def _set_cut_line_visible(self, visible):\n        self.cut_line.setVisible(visible)\n\n    def select_top_n(self, n):\n        root = self._displayed_root\n        if root:\n            clusters = top_clusters(root, n)\n            self.dendrogram.set_selected_clusters(clusters)\n\n    def select_max_height(self, height):\n        root = self._displayed_root\n        if root:\n            clusters = clusters_at_height(root, height)\n            self.dendrogram.set_selected_clusters(clusters)\n\n    def _cut_ratio_changed(self):\n        self.selection_method = 1\n        self._selection_method_changed()\n\n    def _cut_ratio_return(self):\n        if self.selection_method != 1:\n            self.selection_method = 1\n            self._selection_method_changed()\n\n    def _top_n_changed(self):\n        self.selection_method = 2\n        self._selection_method_changed()\n\n    def _top_n_return(self):\n        if self.selection_method != 2:\n            self.selection_method = 2\n            self._selection_method_changed()\n\n    def _selection_method_changed(self):\n        self._set_cut_line_visible(self.selection_method == 1)\n        if self.root:\n            self._apply_selection()\n\n    def _apply_selection(self):\n        if not self.root:\n            return\n\n        if self.selection_method == 0:\n            pass\n        elif self.selection_method == 1:\n            height = self.cut_ratio * self.root.value.height / 100\n            self.set_cutoff_height(height)\n            pos = self.dendrogram.pos_at_height(height)\n            self._set_slider_value(pos.x(), self.dendrogram.size().width())\n        elif self.selection_method == 2:\n            self.select_top_n(self.top_n)\n\n    def _selection_edited(self):\n        # Selection was edited by clicking on a cluster in the\n        # dendrogram view.\n        self.selection_method = 0\n        self._selection_method_changed()\n        self._invalidate_output()\n\n    def _save_selection(self):\n        # Save the current manual node selection state\n        selection_state = None\n        if self.selection_method == 0 and self.root:\n            assert self.linkmatrix is not None\n            linkmat = [(int(_0), int(_1), _2)\n                       for _0, _1, _2 in self.linkmatrix[:, :3].tolist()]\n            nodes_ = self.dendrogram.selected_nodes()\n            # match the display (pruned) nodes back (by ranges)\n            mapping = {node.value.range: node for node in postorder(self.root)}\n            nodes = [mapping[node.value.range] for node in nodes_]\n            indices = [tuple(node.value.index for node in leaves(node))\n                       for node in nodes]\n            if nodes:\n                selection_state = (indices, linkmat)\n        return selection_state\n\n    def save_state(self):\n        # type: () -> Dict[str, Any]\n        \"\"\"\n        Save state for `set_restore_state`\n        \"\"\"\n        selection = self._save_selection()\n        res = {\"version\": (0, 0, 0)}\n        if selection is not None:\n            res[\"selection_state\"] = selection\n        return res\n\n    def set_restore_state(self, state):\n        # type: (Dict[str, Any]) -> bool\n        \"\"\"\n        Restore session data from a saved state.\n\n        Parameters\n        ----------\n        state : Dict[str, Any]\n\n        NOTE\n        ----\n        This is method called while the instance (self) is being constructed,\n        even before its `__init__` is called. Consider `self` to be only a\n        `QObject` at this stage.\n        \"\"\"\n        if \"selection_state\" in state:\n            selection = state[\"selection_state\"]\n            self.__pending_selection_restore = selection\n        return True\n\n    def __zoom_in(self):\n        def clip(minval, maxval, val):\n            return min(max(val, minval), maxval)\n        self.zoom_factor = clip(self.zoom_slider.minimum(),\n                                self.zoom_slider.maximum(),\n                                self.zoom_factor + 1)\n        self.__update_font_scale()\n\n    def __zoom_out(self):\n        def clip(minval, maxval, val):\n            return min(max(val, minval), maxval)\n        self.zoom_factor = clip(self.zoom_slider.minimum(),\n                                self.zoom_slider.maximum(),\n                                self.zoom_factor - 1)\n        self.__update_font_scale()\n\n    def __zoom_reset(self):\n        self.zoom_factor = 0\n        self.__update_font_scale()\n\n    def __update_font_scale(self):\n        font = self.scene.font()\n        factor = (1.25 ** self.zoom_factor)\n        font = qfont_scaled(font, factor)\n        self._main_graphics.setFont(font)\n        self.label_model.setFont(font)\n\n    def send_report(self):\n        annot = self.label_cb.currentText()\n        if isinstance(self.annotation, str):\n            annot = annot.lower()\n        if self.selection_method == 0:\n            sel = \"manual\"\n        elif self.selection_method == 1:\n            sel = \"at {:.1f} of height\".format(self.cut_ratio)\n        else:\n            sel = f\"top {self.top_n} {pl(self.top_n, 'cluster')}\"\n        self.report_items((\n            (\"Linkage\", LINKAGE[self.linkage]),\n            (\"Annotation\", annot),\n            (\"Pruning\",\n             self.pruning != 0 and \"{} levels\".format(self.max_depth)),\n            (\"Selection\", sel),\n        ))\n        self.report_plot()\n\n    @classmethod\n    def migrate_context(cls, context, version):\n        if version < 2:\n            if context.values[\"annotation\"] == \"None\":\n                context.values[\"annotation\"] = None\n\n\ndef qfont_scaled(font, factor):\n    scaled = QFont(font)\n    if font.pointSizeF() != -1:\n        scaled.setPointSizeF(font.pointSizeF() * factor)\n    elif font.pixelSize() != -1:\n        scaled.setPixelSize(int(font.pixelSize() * factor))\n    return scaled\n\n\nclass AxisItem(AxisItem):\n    mousePressed = Signal(QPointF, Qt.MouseButton)\n    mouseMoved = Signal(QPointF, Qt.MouseButtons)\n    mouseReleased = Signal(QPointF, Qt.MouseButton)\n\n    #: \\reimp\n    def wheelEvent(self, event):\n        event.ignore()  # ignore event to propagate to the view -> scroll\n\n    def mousePressEvent(self, event: QGraphicsSceneMouseEvent) -> None:\n        self.mousePressed.emit(event.pos(), event.button())\n        super().mousePressEvent(event)\n        event.accept()\n\n    def mouseMoveEvent(self, event):\n        self.mouseMoved.emit(event.pos(), event.buttons())\n        super().mouseMoveEvent(event)\n        event.accept()\n\n    def mouseReleaseEvent(self, event):\n        self.mouseReleased.emit(event.pos(), event.button())\n        super().mouseReleaseEvent(event)\n        event.accept()\n\n\nclass SliderLine(QGraphicsWidget):\n    \"\"\"A movable slider line.\"\"\"\n    valueChanged = Signal(float)\n\n    linePressed = Signal()\n    lineMoved = Signal()\n    lineReleased = Signal()\n    rangeChanged = Signal(float, float)\n\n    def __init__(self, parent=None, orientation=Qt.Vertical, value=0.0,\n                 length=10.0, **kwargs):\n        self._orientation = orientation\n        self._value = value\n        self._length = length\n        self._min = 0.0\n        self._max = 1.0\n        self._line: Optional[QLineF] = QLineF()\n        self._pen: Optional[QPen] = None\n        super().__init__(parent, **kwargs)\n        self.setAcceptedMouseButtons(Qt.LeftButton)\n        if self._orientation == Qt.Vertical:\n            self.setCursor(Qt.SizeVerCursor)\n        else:\n            self.setCursor(Qt.SizeHorCursor)\n\n    def setPen(self, pen: Union[QPen, Qt.GlobalColor, Qt.PenStyle]) -> None:\n        pen = QPen(pen)\n        if self._pen != pen:\n            self.prepareGeometryChange()\n            self._pen = pen\n            self._line = None\n            self.update()\n\n    def pen(self) -> QPen:\n        if self._pen is None:\n            return QPen(self.palette().text(), 1.0, Qt.DashLine)\n        else:\n            return QPen(self._pen)\n\n    def setValue(self, value: float):\n        value = min(max(value, self._min), self._max)\n\n        if self._value != value:\n            self.prepareGeometryChange()\n            self._value = value\n            self._line = None\n            self.valueChanged.emit(value)\n\n    def value(self) -> float:\n        return self._value\n\n    def setRange(self, minval: float, maxval: float) -> None:\n        maxval = max(minval, maxval)\n        if minval != self._min or maxval != self._max:\n            self._min = minval\n            self._max = maxval\n            self.rangeChanged.emit(minval, maxval)\n            self.setValue(self._value)\n\n    def setLength(self, length: float):\n        if self._length != length:\n            self.prepareGeometryChange()\n            self._length = length\n            self._line = None\n\n    def length(self) -> float:\n        return self._length\n\n    def setOrientation(self, orientation: Qt.Orientation):\n        if self._orientation != orientation:\n            self.prepareGeometryChange()\n            self._orientation = orientation\n            self._line = None\n            if self._orientation == Qt.Vertical:\n                self.setCursor(Qt.SizeVerCursor)\n            else:\n                self.setCursor(Qt.SizeHorCursor)\n\n    def mousePressEvent(self, event: QGraphicsSceneMouseEvent) -> None:\n        event.accept()\n        self.linePressed.emit()\n\n    def mouseMoveEvent(self, event: QGraphicsSceneMouseEvent) -> None:\n        pos = event.pos()\n        if self._orientation == Qt.Vertical:\n            self.setValue(pos.y())\n        else:\n            self.setValue(pos.x())\n        self.lineMoved.emit()\n        event.accept()\n\n    def mouseReleaseEvent(self, event: QGraphicsSceneMouseEvent) -> None:\n        if self._orientation == Qt.Vertical:\n            self.setValue(event.pos().y())\n        else:\n            self.setValue(event.pos().x())\n        self.lineReleased.emit()\n        event.accept()\n\n    def shape(self) -> QPainterPath:\n        path = QPainterPath()\n        path.addRect(self.boundingRect())\n        return path\n\n    def boundingRect(self) -> QRectF:\n        if self._line is None:\n            if self._orientation == Qt.Vertical:\n                self._line = QLineF(0, self._value, self._length, self._value)\n            else:\n                self._line = QLineF(self._value, 0, self._value, self._length)\n        r = QRectF(self._line.p1(), self._line.p2())\n        penw = self.pen().width()\n        return r.adjusted(-penw, -penw, penw, penw)\n\n    def paint(self, painter, *args):\n        if self._line is None:\n            self.boundingRect()\n\n        painter.save()\n        painter.setPen(self.pen())\n        painter.drawLine(self._line)\n        painter.restore()\n\n\ndef clusters_at_height(root, height):\n    \"\"\"Return a list of clusters by cutting the clustering at `height`.\n    \"\"\"\n    lower = set()\n    cluster_list = []\n    for cl in preorder(root):\n        if cl in lower:\n            continue\n        if cl.value.height < height:\n            cluster_list.append(cl)\n            lower.update(preorder(cl))\n    return cluster_list\n\n\ndef main():\n    # pragma: no cover\n    from Orange import distance  # pylint: disable=import-outside-toplevel\n    data = Orange.data.Table(\"iris\")\n    matrix = distance.Euclidean(distance._preprocess(data))\n    subset = data[10:30]\n    WidgetPreview(OWHierarchicalClustering).run(matrix, set_subset=subset)\n\n\nif __name__ == \"__main__\":  # pragma: no cover\n    main()\n",
            "header": {
                "file": {
                    "path": "Orange/widgets/unsupervised/owhierarchicalclustering.py"
                },
                "misc_line": null
            },
            "index_line": {
                "old_commit_hash": "f71cc12ba",
                "new_commit_hash": "dd73e5a70",
                "mode": "100644"
            },
            "is_binary_file": false,
            "binary_line": null,
            "minus_file": {
                "path": "a/Orange/widgets/unsupervised/owhierarchicalclustering.py"
            },
            "plus_file": {
                "path": "b/Orange/widgets/unsupervised/owhierarchicalclustering.py"
            },
            "hunks": [
                {
                    "descriptor": {
                        "old_range": {
                            "start": 240,
                            "length": 6
                        },
                        "new_range": {
                            "start": 240,
                            "length": 9
                        },
                        "section": "class OWHierarchicalClustering(widget.OWWidget):"
                    },
                    "line_group": {
                        "all_lines": [
                            {
                                "content": "            Msg(\"Subset data refers to a different table\")",
                                "type": "context"
                            },
                            {
                                "content": "        pruning_disables_colors = \\",
                                "type": "context"
                            },
                            {
                                "content": "            Msg(\"Pruned cluster doesn't show colors and indicate subset\")",
                                "type": "context"
                            },
                            {
                                "content": "        many_clusters = \\",
                                "type": "added"
                            },
                            {
                                "content": "            Msg(\"Variables with too many values may \"",
                                "type": "added"
                            },
                            {
                                "content": "                \"degrade the performance of downstream widgets.\")",
                                "type": "added"
                            },
                            {
                                "content": "",
                                "type": "context"
                            },
                            {
                                "content": "    #: Stored (manual) selection state (from a saved workflow) to restore.",
                                "type": "context"
                            },
                            {
                                "content": "    __pending_selection_restore = None  # type: Optional[SelectionState]",
                                "type": "context"
                            }
                        ]
                    },
                    "modified_entities": [
                        {
                            "file_name": "Orange/widgets/unsupervised/owhierarchicalclustering.py",
                            "type": "class",
                            "name": "OWHierarchicalClustering",
                            "content": "class OWHierarchicalClustering(widget.OWWidget):\n    name = \"Hierarchical Clustering\"\n    description = \"Display a dendrogram of a hierarchical clustering \" \\\n                  \"constructed from the input distance matrix.\"\n    icon = \"icons/HierarchicalClustering.svg\"\n    priority = 2100\n    keywords = \"hierarchical clustering\"\n\n    class Inputs:\n        distances = Input(\"Distances\", Orange.misc.DistMatrix)\n        subset = Input(\"Data Subset\", Orange.data.Table, explicit=True)\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    settings_version = 2\n    settingsHandler = _DomainContextHandler()\n\n    #: Selected linkage\n    linkage = settings.Setting(LINKAGE.index(DEFAULT_LINKAGE))\n    #: Index of the selected annotation item (variable, ...)\n    annotation = settings.ContextSetting(\"Enumeration\")\n    #: Out-of-context setting for the case when the \"Name\" option is available\n    annotation_if_names = settings.Setting(\"Name\")\n    #: Out-of-context setting for the case with just \"Enumeration\" and \"None\"\n    annotation_if_enumerate = settings.Setting(\"Enumeration\")\n    #: Selected tree pruning (none/max depth)\n    pruning = settings.Setting(0)\n    #: Maximum depth when max depth pruning is selected\n    max_depth = settings.Setting(10)\n\n    #: Selected cluster selection method (none, cut distance, top n)\n    selection_method = settings.Setting(0)\n    #: Cut height ratio wrt root height\n    cut_ratio = settings.Setting(75.0)\n    #: Number of top clusters to select\n    top_n = settings.Setting(3)\n    #: Dendrogram zoom factor\n    zoom_factor = settings.Setting(0)\n    #: Show labels only for subset (if present)\n    label_only_subset = settings.Setting(False)\n    #: Color for label decoration\n    color_by: Union[DiscreteVariable, ContinuousVariable, None] = \\\n        settings.ContextSetting(None)\n\n    autocommit = settings.Setting(True)\n\n    graph_name = \"scene\"  # QGraphicsScene\n\n    basic_annotations = [None, \"Enumeration\"]\n\n    class Error(widget.OWWidget.Error):\n        empty_matrix = Msg(\"Distance matrix is empty.\")\n        not_finite_distances = Msg(\"Some distances are infinite\")\n        not_symmetric = widget.Msg(\"Distance matrix is not symmetric.\")\n\n    class Warning(widget.OWWidget.Warning):\n        subset_on_no_table = \\\n            Msg(\"Unused data subset: distances do not refer to data instances\")\n        subset_not_subset = \\\n            Msg(\"Some data from the subset does not appear in distance matrix\")\n        subset_wrong = \\\n            Msg(\"Subset data refers to a different table\")\n        pruning_disables_colors = \\\n            Msg(\"Pruned cluster doesn't show colors and indicate subset\")\n        many_clusters = \\\n            Msg(\"Variables with too many values may \"\n                \"degrade the performance of downstream widgets.\")\n\n    #: Stored (manual) selection state (from a saved workflow) to restore.\n    __pending_selection_restore = None  # type: Optional[SelectionState]\n\n    def __init__(self):\n        super().__init__()\n\n        self.matrix = None\n        self.items = None\n        self.subset = None\n        self.subset_rows = set()\n        self.linkmatrix = None\n        self.root = None\n        self._displayed_root = None\n        self.cutoff_height = 0.0\n\n        spin_width = QFontMetrics(self.font()).horizontalAdvance(\"M\" * 7)\n        gui.comboBox(\n            self.controlArea, self, \"linkage\", items=LINKAGE, box=\"Linkage\",\n            callback=self._invalidate_clustering)\n\n        model = itemmodels.VariableListModel(placeholder=\"None\")\n        model[:] = self.basic_annotations\n\n        grid = QGridLayout()\n        gui.widgetBox(self.controlArea, \"Annotations\", orientation=grid)\n        self.label_cb = cb = combobox.ComboBoxSearch(\n            minimumContentsLength=14,\n            sizeAdjustPolicy=QComboBox.AdjustToMinimumContentsLengthWithIcon\n        )\n        cb.setModel(model)\n        cb.setCurrentIndex(cb.findData(self.annotation, Qt.EditRole))\n\n        def on_annotation_activated():\n            self.annotation = self.label_cb.currentData(Qt.EditRole)\n            self._update_labels()\n        cb.activated.connect(on_annotation_activated)\n\n        def on_annotation_changed(value):\n            self.label_cb.setCurrentIndex(\n                self.label_cb.findData(value, Qt.EditRole))\n        self.connect_control(\"annotation\", on_annotation_changed)\n\n        grid.addWidget(self.label_cb, 0, 0, 1, 2)\n\n        cb = gui.checkBox(\n            None, self, \"label_only_subset\", \"Show labels only for subset\",\n            disabled=True,\n            callback=self._update_labels, stateWhenDisabled=False)\n        grid.addWidget(cb, 1, 0, 1, 2)\n\n        model = itemmodels.DomainModel(\n            valid_types=(DiscreteVariable, ContinuousVariable),\n            placeholder=\"None\")\n        cb = gui.comboBox(\n            None, self, \"color_by\", orientation=Qt.Horizontal,\n            model=model, callback=self._update_labels,\n            sizePolicy=QSizePolicy(QSizePolicy.MinimumExpanding,\n                                   QSizePolicy.Fixed))\n        self.color_by_label = QLabel(\"Color by:\")\n        grid.addWidget(self.color_by_label, 2, 0)\n        grid.addWidget(cb, 2, 1)\n\n        box = gui.radioButtons(\n            self.controlArea, self, \"pruning\", box=\"Pruning\",\n            callback=self._invalidate_pruning)\n        grid = QGridLayout()\n        box.layout().addLayout(grid)\n        grid.addWidget(\n            gui.appendRadioButton(box, \"None\", addToLayout=False),\n            0, 0\n        )\n        self.max_depth_spin = gui.spin(\n            box, self, \"max_depth\", minv=1, maxv=100,\n            controlWidth=spin_width, alignment=Qt.AlignRight,\n            callback=self._max_depth_changed,\n            keyboardTracking=False, addToLayout=False\n        )\n        self.max_depth_spin.lineEdit().returnPressed.connect(\n            self._max_depth_return)\n\n        grid.addWidget(\n            gui.appendRadioButton(box, \"Max depth:\", addToLayout=False),\n            1, 0)\n        grid.addWidget(self.max_depth_spin, 1, 1)\n\n        self.selection_box = gui.radioButtons(\n            self.controlArea, self, \"selection_method\",\n            box=\"Selection\",\n            callback=self._selection_method_changed)\n\n        grid = QGridLayout()\n        self.selection_box.layout().addLayout(grid)\n        grid.addWidget(\n            gui.appendRadioButton(\n                self.selection_box, \"Manual\", addToLayout=False),\n            0, 0\n        )\n        grid.addWidget(\n            gui.appendRadioButton(\n                self.selection_box, \"Height ratio:\", addToLayout=False),\n            1, 0\n        )\n        self.cut_ratio_spin = gui.spin(\n            self.selection_box, self, \"cut_ratio\", 0, 100, step=1e-1,\n            controlWidth=spin_width, alignment = Qt.AlignRight,\n            spinType=float, callback=self._cut_ratio_changed,\n            addToLayout=False\n        )\n        self.cut_ratio_spin.setSuffix(\" %\")\n        self.cut_ratio_spin.lineEdit().returnPressed.connect(\n            self._cut_ratio_return)\n\n        grid.addWidget(self.cut_ratio_spin, 1, 1)\n\n        grid.addWidget(\n            gui.appendRadioButton(\n                self.selection_box, \"Top N:\", addToLayout=False),\n            2, 0\n        )\n        self.top_n_spin = gui.spin(\n            self.selection_box, self, \"top_n\", 1, 1000,\n            controlWidth=spin_width, alignment=Qt.AlignRight,\n            callback=self._top_n_changed, addToLayout=False)\n        self.top_n_spin.lineEdit().returnPressed.connect(self._top_n_return)\n        grid.addWidget(self.top_n_spin, 2, 1)\n\n        self.zoom_slider = gui.hSlider(\n            self.controlArea, self, \"zoom_factor\", box=\"Zoom\",\n            minValue=-6, maxValue=3, step=1, ticks=True, createLabel=False,\n            callback=self.__update_font_scale)\n\n        zoom_in = QAction(\n            \"Zoom in\", self, shortcut=QKeySequence.ZoomIn,\n            triggered=self.__zoom_in\n        )\n        zoom_out = QAction(\n            \"Zoom out\", self, shortcut=QKeySequence.ZoomOut,\n            triggered=self.__zoom_out\n        )\n        zoom_reset = QAction(\n            \"Reset zoom\", self,\n            shortcut=QKeySequence(Qt.ControlModifier | Qt.Key_0),\n            triggered=self.__zoom_reset\n        )\n        self.addActions([zoom_in, zoom_out, zoom_reset])\n\n        self.controlArea.layout().addStretch()\n\n        gui.auto_send(self.buttonsArea, self, \"autocommit\")\n\n        self.scene = QGraphicsScene(self)\n        self.view = GraphicsView(\n            self.scene,\n            horizontalScrollBarPolicy=Qt.ScrollBarAlwaysOff,\n            verticalScrollBarPolicy=Qt.ScrollBarAlwaysOn,\n            alignment=Qt.AlignLeft | Qt.AlignVCenter,\n            widgetResizable=True,\n        )\n        # Disable conflicting action shortcuts. We define our own.\n        for a in self.view.viewActions():\n            a.setEnabled(False)\n        self.mainArea.layout().setSpacing(1)\n        self.mainArea.layout().addWidget(self.view)\n\n        def axis_view(orientation):\n            ax = AxisItem(orientation=orientation, maxTickLength=7)\n            ax.mousePressed.connect(self._activate_cut_line)\n            ax.mouseMoved.connect(self._activate_cut_line)\n            ax.mouseReleased.connect(self._activate_cut_line)\n            ax.setRange(1.0, 0.0)\n            return ax\n\n        self.top_axis = axis_view(\"top\")\n        self.bottom_axis = axis_view(\"bottom\")\n\n        self._main_graphics = QGraphicsWidget(\n            sizePolicy=QSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.Preferred)\n        )\n        scenelayout = QGraphicsGridLayout()\n        scenelayout.setHorizontalSpacing(10)\n        scenelayout.setVerticalSpacing(10)\n\n        self._main_graphics.setLayout(scenelayout)\n        self.scene.addItem(self._main_graphics)\n        self.view.setCentralWidget(self._main_graphics)\n        self.scene.addItem(self._main_graphics)\n\n        self.dendrogram = DendrogramWidget(pen_width=2)\n        self.dendrogram.setSizePolicy(QSizePolicy.MinimumExpanding,\n                                      QSizePolicy.MinimumExpanding)\n        self.dendrogram.selectionChanged.connect(self._invalidate_output)\n        self.dendrogram.selectionEdited.connect(self._selection_edited)\n\n        self.labels = TextListView()\n        self.label_model = SelectedLabelsModel()\n        self.labels.setModel(self.label_model)\n        self.labels.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Preferred)\n        self.labels.setAlignment(Qt.AlignLeft)\n        self.labels.setMaximumWidth(200)\n\n        scenelayout.addItem(self.top_axis, 0, 0,\n                            alignment=Qt.AlignLeft | Qt.AlignVCenter)\n        scenelayout.addItem(self.dendrogram, 1, 0,\n                            alignment=Qt.AlignLeft | Qt.AlignVCenter)\n        scenelayout.addItem(self.labels, 1, 1,\n                            alignment=Qt.AlignLeft | Qt.AlignVCenter)\n        scenelayout.addItem(self.bottom_axis, 2, 0,\n                            alignment=Qt.AlignLeft | Qt.AlignVCenter)\n        self.top_axis.setZValue(self.dendrogram.zValue() + 10)\n        self.bottom_axis.setZValue(self.dendrogram.zValue() + 10)\n        self.cut_line = SliderLine(self.top_axis,\n                                   orientation=Qt.Horizontal)\n        self.cut_line.valueChanged.connect(self._dendrogram_slider_changed)\n        self.dendrogram.geometryChanged.connect(self._dendrogram_geom_changed)\n        self._set_cut_line_visible(self.selection_method == 1)\n        self.__update_font_scale()\n\n    @Inputs.distances\n    def set_distances(self, matrix):\n        self.error()\n        self.Error.clear()\n\n        self.matrix = None\n        self.Error.clear()\n        if matrix is not None:\n            if len(matrix) < 2:\n                self.Error.empty_matrix()\n            elif not matrix.is_symmetric():\n                self.Error.not_symmetric()\n            elif not np.all(np.isfinite(matrix)):\n                self.Error.not_finite_distances()\n            else:\n                self.matrix = matrix\n\n    @Inputs.subset\n    def set_subset(self, subset):\n        self.subset = subset\n        self.controls.label_only_subset.setDisabled(subset is None)\n\n    def handleNewSignals(self):\n        if self.__pending_selection_restore is not None:\n            selection_state = self.__pending_selection_restore\n        else:\n            # save the current selection to (possibly) restore later\n            selection_state = self._save_selection()\n\n        matrix = self.matrix\n        if matrix is not None:\n            self._set_items(matrix.row_items, matrix.axis)\n        else:\n            self._set_items(None)\n        self._update()\n\n        # Can now attempt to restore session state from a saved workflow.\n        if self.root and selection_state is not None:\n            self._restore_selection(selection_state)\n            self.__pending_selection_restore = None\n\n        self.Warning.clear()\n        rows = set()\n        if self.subset:\n            subsetids = set(self.subset.ids)\n            if not isinstance(self.items, Orange.data.Table) \\\n                    or not self.matrix.axis:\n                self.Warning.subset_on_no_table()\n            elif (dataids := set(self.items.ids)) and not subsetids & dataids:\n                self.Warning.subset_wrong()\n            elif not subsetids <= dataids:\n                self.Warning.subset_not_subset()\n            else:\n                indices = [leaf.value.index for leaf in leaves(self.root)]\n                rows = {\n                    row for row, rowid in enumerate(self.items.ids[indices])\n                    if rowid in subsetids\n                }\n\n        self.subset_rows = rows\n        self._update_labels()\n        self.commit.now()\n\n    def _set_items(self, items, axis=1):\n        self.closeContext()\n        self.items = items\n        model = self.label_cb.model()\n        color_model = self.controls.color_by.model()\n        color_model.set_domain(None)\n        self.color_by = None\n        if len(model) == 2 and model[0] is None:\n            if model[1] == \"Name\":\n                self.annotation_if_names = self.annotation\n            if model[1] == self.basic_annotations[1]:\n                self.annotation_if_enumerate = self.annotation\n        if isinstance(items, Orange.data.Table) and axis:\n            metas_class = tuple(\n                filter_visible(chain(items.domain.metas,\n                                     items.domain.class_vars)))\n            visible_attrs = tuple(filter_visible(items.domain.attributes))\n            if not (metas_class or visible_attrs):\n                model[:] = self.basic_annotations\n            else:\n                model[:] = (\n                    (None, )\n                    + metas_class\n                    + (model.Separator, ) * bool(metas_class and visible_attrs)\n                    + visible_attrs)\n            for meta in items.domain.metas:\n                if isinstance(meta, StringVariable):\n                    self.annotation = meta\n                    break\n            else:\n                if items.domain.class_vars:\n                    # No string metas: show class\n                    self.annotation = items.domain.class_vars[0]\n                else:\n                    # No string metas and no class: show the first option\n                    # which is not None (in the worst case, Enumeration)\n                    self.annotation = model[1]\n\n            color_model.set_domain(items.domain)\n            if items.domain.class_vars:\n                self.color_by = items.domain.class_vars[0]\n            self.openContext(items.domain)\n        elif isinstance(items, Orange.data.Table) and not axis \\\n                or (isinstance(items, list) and \\\n                    all(isinstance(var, Orange.data.Variable)\n                        for var in items)):\n            model[:] = (None, \"Name\")\n            self.annotation = self.annotation_if_names\n        else:\n            model[:] = self.basic_annotations\n            self.annotation = self.annotation_if_enumerate\n\n        no_colors = len(color_model) == 1\n        self.controls.color_by.setDisabled(no_colors)\n        self.color_by_label.setDisabled(no_colors)\n\n    def _clear_plot(self):\n        self.dendrogram.set_root(None)\n        self.label_model.clear()\n\n    def _set_displayed_root(self, root):\n        self._clear_plot()\n        self._displayed_root = root\n        self.dendrogram.set_root(root)\n        self._update_labels()\n\n    def _update(self):\n        self._clear_plot()\n\n        distances = self.matrix\n\n        if distances is not None:\n            method = LINKAGE_ARGS[self.linkage]\n            Z = dist_matrix_linkage(distances, linkage=method)\n\n            tree = tree_from_linkage(Z)\n            self.linkmatrix = Z\n            self.root = tree\n\n            self.top_axis.setRange(tree.value.height, 0.0)\n            self.bottom_axis.setRange(tree.value.height, 0.0)\n\n            if self.pruning:\n                self._set_displayed_root(prune(tree, level=self.max_depth))\n            else:\n                self._set_displayed_root(tree)\n        else:\n            self.linkmatrix = None\n            self.root = None\n            self._set_displayed_root(None)\n\n        self._apply_selection()\n\n    def _update_labels(self):\n        if not hasattr(self, \"label_model\"):\n            # This method can be called during widget initialization when\n            # creating check box for label_only_subset, if it's value is\n            # initially True.\n            # See https://github.com/biolab/orange-widget-base/pull/213;\n            # if it's merged, this check can be removed.\n            return\n\n        self.Warning.pruning_disables_colors(\n            shown=self.pruning\n                  and (self.subset_rows or self.color_by is not None))\n        labels = []\n        if self.root and self._displayed_root:\n            indices = [leaf.value.index for leaf in leaves(self.root)]\n\n            if self.annotation is None:\n                if not self.pruning \\\n                        and self.subset_rows and self.color_by is None:\n                    # Model fails if number of labels and of colors mismatch\n                    labels = [\"\"] * len(indices)\n                else:\n                    labels = []\n            elif self.annotation == \"Enumeration\":\n                labels = [str(i+1) for i in indices]\n            elif self.annotation == \"Name\":\n                attr = self.matrix.row_items.domain.attributes\n                labels = [str(attr[i]) for i in indices]\n            elif isinstance(self.annotation, Orange.data.Variable):\n                col_data = self.items.get_column(self.annotation)\n                labels = [self.annotation.str_val(val) for val in col_data]\n                labels = [labels[idx] for idx in indices]\n            else:\n                labels = []\n            if not self.pruning and \\\n                    labels and self.label_only_subset and self.subset_rows:\n                labels = [label if row in self.subset_rows else \"\"\n                          for row, label in enumerate(labels)]\n\n            if labels and self._displayed_root is not self.root:\n                joined = leaves(self._displayed_root)\n                labels = [\", \".join(labels[leaf.value.first: leaf.value.last])\n                          for leaf in joined]\n\n        self.label_model[:] = labels\n        self.label_model.set_subset(set() if self.pruning else self.subset_rows)\n        self.labels.setMinimumWidth(1 if labels else -1)\n\n        if not self.pruning and self.color_by is not None:\n            col = self.items.get_column(self.color_by)\n            self.label_model.set_colors(\n                self.color_by.palette.values_to_qcolors(col[indices]))\n        else:\n            self.label_model.set_colors(None)\n\n    def _restore_selection(self, state):\n        # type: (SelectionState) -> bool\n        \"\"\"\n        Restore the (manual) node selection state.\n\n        Return True if successful; False otherwise.\n        \"\"\"\n        linkmatrix = self.linkmatrix\n        if self.selection_method == 0 and self.root:\n            selected, linksaved = state\n            linkstruct = np.array(linksaved, dtype=float)\n            selected = set(selected)  # type: Set[Tuple[int]]\n            if not selected:\n                return False\n            if linkmatrix.shape[0] != linkstruct.shape[0]:\n                return False\n            # check that the linkage matrix structure matches. Use isclose for\n            # the height column to account for inexact floating point math\n            # (e.g. summation order in different ?gemm implementations for\n            # euclidean distances, ...)\n            if np.any(linkstruct[:, :2] != linkmatrix[:, :2]) or \\\n                    not np.all(np.isclose(linkstruct[:, 2], linkstruct[:, 2])):\n                return False\n            selection = []\n            indices = np.array([n.value.index for n in leaves(self.root)],\n                               dtype=int)\n            # mapping from ranges to display (pruned) nodes\n            mapping = {node.value.range: node\n                       for node in postorder(self._displayed_root)}\n            for node in postorder(self.root):  # type: Tree\n                r = tuple(indices[node.value.first: node.value.last])\n                if r in selected:\n                    if node.value.range not in mapping:\n                        # the node was pruned from display and cannot be\n                        # selected\n                        break\n                    selection.append(mapping[node.value.range])\n                    selected.remove(r)\n                if not selected:\n                    break  # found all, nothing more to do\n            if selection and selected:\n                # Could not restore all selected nodes (only partial match)\n                return False\n\n            self._set_selected_nodes(selection)\n            return True\n        return False\n\n    def _set_selected_nodes(self, selection):\n        # type: (List[Tree]) -> None\n        \"\"\"\n        Set the nodes in `selection` to be the current selected nodes.\n\n        The selection nodes must be subtrees of the current `_displayed_root`.\n        \"\"\"\n        self.dendrogram.selectionChanged.disconnect(self._invalidate_output)\n        try:\n            self.dendrogram.set_selected_clusters(selection)\n        finally:\n            self.dendrogram.selectionChanged.connect(self._invalidate_output)\n\n    def _max_depth_return(self):\n        if self.pruning != 1:\n            self.pruning = 1\n            self._invalidate_pruning()\n\n    def _max_depth_changed(self):\n        self.pruning = 1\n        self._invalidate_pruning()\n\n    def _invalidate_clustering(self):\n        self._update()\n        self._update_labels()\n        self._invalidate_output()\n\n    def _invalidate_output(self):\n        self.commit.deferred()\n\n    def _invalidate_pruning(self):\n        if self.root:\n            selection = self.dendrogram.selected_nodes()\n            ranges = [node.value.range for node in selection]\n            if self.pruning:\n                self._set_displayed_root(\n                    prune(self.root, level=self.max_depth))\n            else:\n                self._set_displayed_root(self.root)\n            selected = [node for node in preorder(self._displayed_root)\n                        if node.value.range in ranges]\n\n            self.dendrogram.set_selected_clusters(selected)\n\n        self._apply_selection()\n\n    @gui.deferred\n    def commit(self):\n        items = getattr(self.matrix, \"items\", self.items)\n        self.Warning.many_clusters.clear()\n        if not items:\n            self.Outputs.selected_data.send(None)\n            self.Outputs.annotated_data.send(None)\n            return\n\n        selection = self.dendrogram.selected_nodes()\n        selection = sorted(selection, key=lambda c: c.value.first)\n\n        indices = [leaf.value.index for leaf in leaves(self.root)]\n\n        maps = [indices[node.value.first:node.value.last]\n                for node in selection]\n        if len(maps) > 20:\n            self.Warning.many_clusters()\n\n        selected_indices = list(chain(*maps))\n\n        if not selected_indices:\n            self.Outputs.selected_data.send(None)\n            annotated_data = lazy_annotated_table(items, []) \\\n                if self.selection_method == 0 and self.matrix.axis else None\n            self.Outputs.annotated_data.send(annotated_data)\n            return\n\n        selected_data = annotated_data = None\n\n        if isinstance(items, Orange.data.Table) and self.matrix.axis == 1:\n            # Select rows\n            data, domain = items, items.domain\n\n            c = np.full(self.matrix.shape[0], len(maps))\n            for i, indices in enumerate(maps):\n                c[indices] = i\n\n            clust_name = get_unique_names(domain, \"Cluster\")\n            values = [f\"C{i + 1}\" for i in range(len(maps))]\n\n            sel_clust_var = Orange.data.DiscreteVariable(\n                name=clust_name, values=values)\n            sel_domain = add_columns(domain, metas=(sel_clust_var,))\n            selected_data = LazyValue[Table](\n                lambda: items.add_column(\n                    sel_clust_var, c, to_metas=True)[c != len(maps)],\n                domain=sel_domain, length=len(selected_indices))\n\n            ann_clust_var = Orange.data.DiscreteVariable(\n                name=clust_name, values=values + [\"Other\"]\n            )\n            ann_domain = add_columns(\n                domain_with_annotation_column(data)[0], metas=(ann_clust_var, ))\n            annotated_data = LazyValue[Table](\n                lambda: create_annotated_table(\n                    data=items.add_column(ann_clust_var, c, to_metas=True),\n                    selected_indices=selected_indices),\n                domain=ann_domain, length=len(items)\n            )\n\n        elif isinstance(items, Orange.data.Table) and self.matrix.axis == 0:\n            # Select columns\n            attrs = []\n            unselected_indices = sorted(set(range(self.root.value.last)) -\n                                        set(selected_indices))\n            for clust, indices in chain(enumerate(maps, start=1),\n                                        [(0, unselected_indices)]):\n                for i in indices:\n                    attr = items.domain[i].copy()\n                    attr.attributes[\"cluster\"] = clust\n                    attrs.append(attr)\n            all_domain = Orange.data.Domain(\n                # len(unselected_indices) can be 0\n                attrs[:len(attrs) - len(unselected_indices)],\n                items.domain.class_vars, items.domain.metas)\n\n            selected_data = LazyValue[Table](\n                lambda: items.from_table(all_domain, items),\n                domain=all_domain, length=len(items))\n\n            sel_domain = Orange.data.Domain(\n                attrs,\n                items.domain.class_vars, items.domain.metas)\n            annotated_data = LazyValue[Table](\n                lambda: items.from_table(sel_domain, items),\n                domain=sel_domain, length=len(items))\n\n        self.Outputs.selected_data.send(selected_data)\n        self.Outputs.annotated_data.send(annotated_data)\n\n    @Slot(QPointF)\n    def _activate_cut_line(self, pos: QPointF):\n        \"\"\"Activate cut line selection an set cut value to `pos.x()`.\"\"\"\n        self.selection_method = 1\n        self.cut_line.setValue(pos.x())\n        self._selection_method_changed()\n\n    def onDeleteWidget(self):\n        super().onDeleteWidget()\n        self._clear_plot()\n        self.dendrogram.clear()\n        self.dendrogram.deleteLater()\n\n    def _dendrogram_geom_changed(self):\n        pos = self.dendrogram.pos_at_height(self.cutoff_height)\n        geom = self.dendrogram.geometry()\n        self._set_slider_value(pos.x(), geom.width())\n\n        self.cut_line.setLength(\n            self.bottom_axis.geometry().bottom()\n            - self.top_axis.geometry().top()\n        )\n\n        geom = self._main_graphics.geometry()\n        assert geom.topLeft() == QPointF(0, 0)\n\n        def adjustLeft(rect):\n            rect = QRectF(rect)\n            rect.setLeft(geom.left())\n            return rect\n        margin = 3\n        self.scene.setSceneRect(geom)\n        self.view.setSceneRect(geom)\n        self.view.setHeaderSceneRect(\n            adjustLeft(self.top_axis.geometry()).adjusted(0, 0, 0, margin)\n        )\n        self.view.setFooterSceneRect(\n            adjustLeft(self.bottom_axis.geometry()).adjusted(0, -margin, 0, 0)\n        )\n\n    def _dendrogram_slider_changed(self, value):\n        p = QPointF(value, 0)\n        cl_height = self.dendrogram.height_at(p)\n\n        self.set_cutoff_height(cl_height)\n\n    def _set_slider_value(self, value, span):\n        with blocked(self.cut_line):\n            self.cut_line.setRange(0, span)\n            self.cut_line.setValue(value)\n\n    def set_cutoff_height(self, height):\n        self.cutoff_height = height\n        if self.root:\n            self.cut_ratio = 100 * height / self.root.value.height\n        self.select_max_height(height)\n\n    def _set_cut_line_visible(self, visible):\n        self.cut_line.setVisible(visible)\n\n    def select_top_n(self, n):\n        root = self._displayed_root\n        if root:\n            clusters = top_clusters(root, n)\n            self.dendrogram.set_selected_clusters(clusters)\n\n    def select_max_height(self, height):\n        root = self._displayed_root\n        if root:\n            clusters = clusters_at_height(root, height)\n            self.dendrogram.set_selected_clusters(clusters)\n\n    def _cut_ratio_changed(self):\n        self.selection_method = 1\n        self._selection_method_changed()\n\n    def _cut_ratio_return(self):\n        if self.selection_method != 1:\n            self.selection_method = 1\n            self._selection_method_changed()\n\n    def _top_n_changed(self):\n        self.selection_method = 2\n        self._selection_method_changed()\n\n    def _top_n_return(self):\n        if self.selection_method != 2:\n            self.selection_method = 2\n            self._selection_method_changed()\n\n    def _selection_method_changed(self):\n        self._set_cut_line_visible(self.selection_method == 1)\n        if self.root:\n            self._apply_selection()\n\n    def _apply_selection(self):\n        if not self.root:\n            return\n\n        if self.selection_method == 0:\n            pass\n        elif self.selection_method == 1:\n            height = self.cut_ratio * self.root.value.height / 100\n            self.set_cutoff_height(height)\n            pos = self.dendrogram.pos_at_height(height)\n            self._set_slider_value(pos.x(), self.dendrogram.size().width())\n        elif self.selection_method == 2:\n            self.select_top_n(self.top_n)\n\n    def _selection_edited(self):\n        # Selection was edited by clicking on a cluster in the\n        # dendrogram view.\n        self.selection_method = 0\n        self._selection_method_changed()\n        self._invalidate_output()\n\n    def _save_selection(self):\n        # Save the current manual node selection state\n        selection_state = None\n        if self.selection_method == 0 and self.root:\n            assert self.linkmatrix is not None\n            linkmat = [(int(_0), int(_1), _2)\n                       for _0, _1, _2 in self.linkmatrix[:, :3].tolist()]\n            nodes_ = self.dendrogram.selected_nodes()\n            # match the display (pruned) nodes back (by ranges)\n            mapping = {node.value.range: node for node in postorder(self.root)}\n            nodes = [mapping[node.value.range] for node in nodes_]\n            indices = [tuple(node.value.index for node in leaves(node))\n                       for node in nodes]\n            if nodes:\n                selection_state = (indices, linkmat)\n        return selection_state\n\n    def save_state(self):\n        # type: () -> Dict[str, Any]\n        \"\"\"\n        Save state for `set_restore_state`\n        \"\"\"\n        selection = self._save_selection()\n        res = {\"version\": (0, 0, 0)}\n        if selection is not None:\n            res[\"selection_state\"] = selection\n        return res\n\n    def set_restore_state(self, state):\n        # type: (Dict[str, Any]) -> bool\n        \"\"\"\n        Restore session data from a saved state.\n\n        Parameters\n        ----------\n        state : Dict[str, Any]\n\n        NOTE\n        ----\n        This is method called while the instance (self) is being constructed,\n        even before its `__init__` is called. Consider `self` to be only a\n        `QObject` at this stage.\n        \"\"\"\n        if \"selection_state\" in state:\n            selection = state[\"selection_state\"]\n            self.__pending_selection_restore = selection\n        return True\n\n    def __zoom_in(self):\n        def clip(minval, maxval, val):\n            return min(max(val, minval), maxval)\n        self.zoom_factor = clip(self.zoom_slider.minimum(),\n                                self.zoom_slider.maximum(),\n                                self.zoom_factor + 1)\n        self.__update_font_scale()\n\n    def __zoom_out(self):\n        def clip(minval, maxval, val):\n            return min(max(val, minval), maxval)\n        self.zoom_factor = clip(self.zoom_slider.minimum(),\n                                self.zoom_slider.maximum(),\n                                self.zoom_factor - 1)\n        self.__update_font_scale()\n\n    def __zoom_reset(self):\n        self.zoom_factor = 0\n        self.__update_font_scale()\n\n    def __update_font_scale(self):\n        font = self.scene.font()\n        factor = (1.25 ** self.zoom_factor)\n        font = qfont_scaled(font, factor)\n        self._main_graphics.setFont(font)\n        self.label_model.setFont(font)\n\n    def send_report(self):\n        annot = self.label_cb.currentText()\n        if isinstance(self.annotation, str):\n            annot = annot.lower()\n        if self.selection_method == 0:\n            sel = \"manual\"\n        elif self.selection_method == 1:\n            sel = \"at {:.1f} of height\".format(self.cut_ratio)\n        else:\n            sel = f\"top {self.top_n} {pl(self.top_n, 'cluster')}\"\n        self.report_items((\n            (\"Linkage\", LINKAGE[self.linkage]),\n            (\"Annotation\", annot),\n            (\"Pruning\",\n             self.pruning != 0 and \"{} levels\".format(self.max_depth)),\n            (\"Selection\", sel),\n        ))\n        self.report_plot()\n\n    @classmethod\n    def migrate_context(cls, context, version):\n        if version < 2:\n            if context.values[\"annotation\"] == \"None\":\n                context.values[\"annotation\"] = None",
                            "ast_type_str": "ClassDef",
                            "start_lineno": 177,
                            "end_lineno": 1074,
                            "parent": null
                        }
                    ],
                    "added_entities": [],
                    "deleted_entities": []
                },
                {
                    "descriptor": {
                        "old_range": {
                            "start": 361,
                            "length": 7
                        },
                        "new_range": {
                            "start": 364,
                            "length": 7
                        },
                        "section": "class OWHierarchicalClustering(widget.OWWidget):"
                    },
                    "line_group": {
                        "all_lines": [
                            {
                                "content": "            2, 0",
                                "type": "context"
                            },
                            {
                                "content": "        )",
                                "type": "context"
                            },
                            {
                                "content": "        self.top_n_spin = gui.spin(",
                                "type": "context"
                            },
                            {
                                "content": "            self.selection_box, self, \"top_n\", 1, 20,",
                                "type": "deleted"
                            },
                            {
                                "content": "            self.selection_box, self, \"top_n\", 1, 1000,",
                                "type": "added"
                            },
                            {
                                "content": "            controlWidth=spin_width, alignment=Qt.AlignRight,",
                                "type": "context"
                            },
                            {
                                "content": "            callback=self._top_n_changed, addToLayout=False)",
                                "type": "context"
                            },
                            {
                                "content": "        self.top_n_spin.lineEdit().returnPressed.connect(self._top_n_return)",
                                "type": "context"
                            }
                        ]
                    },
                    "modified_entities": [
                        {
                            "file_name": "Orange/widgets/unsupervised/owhierarchicalclustering.py",
                            "type": "method",
                            "name": "OWHierarchicalClustering.__init__",
                            "content": "class OWHierarchicalClustering(widget.OWWidget):\n    name = \"Hierarchical Clustering\"\n    description = \"Display a dendrogram of a hierarchical clustering \" \\\n                  \"constructed from the input distance matrix.\"\n    icon = \"icons/HierarchicalClustering.svg\"\n    priority = 2100\n    keywords = \"hierarchical clustering\"\n\n    class Inputs:\n        distances = Input(\"Distances\", Orange.misc.DistMatrix)\n        subset = Input(\"Data Subset\", Orange.data.Table, explicit=True)\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    settings_version = 2\n    settingsHandler = _DomainContextHandler()\n\n    #: Selected linkage\n    linkage = settings.Setting(LINKAGE.index(DEFAULT_LINKAGE))\n    #: Index of the selected annotation item (variable, ...)\n    annotation = settings.ContextSetting(\"Enumeration\")\n    #: Out-of-context setting for the case when the \"Name\" option is available\n    annotation_if_names = settings.Setting(\"Name\")\n    #: Out-of-context setting for the case with just \"Enumeration\" and \"None\"\n    annotation_if_enumerate = settings.Setting(\"Enumeration\")\n    #: Selected tree pruning (none/max depth)\n    pruning = settings.Setting(0)\n    #: Maximum depth when max depth pruning is selected\n    max_depth = settings.Setting(10)\n\n    #: Selected cluster selection method (none, cut distance, top n)\n    selection_method = settings.Setting(0)\n    #: Cut height ratio wrt root height\n    cut_ratio = settings.Setting(75.0)\n    #: Number of top clusters to select\n    top_n = settings.Setting(3)\n    #: Dendrogram zoom factor\n    zoom_factor = settings.Setting(0)\n    #: Show labels only for subset (if present)\n    label_only_subset = settings.Setting(False)\n    #: Color for label decoration\n    color_by: Union[DiscreteVariable, ContinuousVariable, None] = \\\n        settings.ContextSetting(None)\n\n    autocommit = settings.Setting(True)\n\n    graph_name = \"scene\"  # QGraphicsScene\n\n    basic_annotations = [None, \"Enumeration\"]\n\n    class Error(widget.OWWidget.Error):\n        empty_matrix = Msg(\"Distance matrix is empty.\")\n        not_finite_distances = Msg(\"Some distances are infinite\")\n        not_symmetric = widget.Msg(\"Distance matrix is not symmetric.\")\n\n    class Warning(widget.OWWidget.Warning):\n        subset_on_no_table = \\\n            Msg(\"Unused data subset: distances do not refer to data instances\")\n        subset_not_subset = \\\n            Msg(\"Some data from the subset does not appear in distance matrix\")\n        subset_wrong = \\\n            Msg(\"Subset data refers to a different table\")\n        pruning_disables_colors = \\\n            Msg(\"Pruned cluster doesn't show colors and indicate subset\")\n\n    #: Stored (manual) selection state (from a saved workflow) to restore.\n    __pending_selection_restore = None  # type: Optional[SelectionState]\n\n    def __init__(self):\n        super().__init__()\n\n        self.matrix = None\n        self.items = None\n        self.subset = None\n        self.subset_rows = set()\n        self.linkmatrix = None\n        self.root = None\n        self._displayed_root = None\n        self.cutoff_height = 0.0\n\n        spin_width = QFontMetrics(self.font()).horizontalAdvance(\"M\" * 7)\n        gui.comboBox(\n            self.controlArea, self, \"linkage\", items=LINKAGE, box=\"Linkage\",\n            callback=self._invalidate_clustering)\n\n        model = itemmodels.VariableListModel(placeholder=\"None\")\n        model[:] = self.basic_annotations\n\n        grid = QGridLayout()\n        gui.widgetBox(self.controlArea, \"Annotations\", orientation=grid)\n        self.label_cb = cb = combobox.ComboBoxSearch(\n            minimumContentsLength=14,\n            sizeAdjustPolicy=QComboBox.AdjustToMinimumContentsLengthWithIcon\n        )\n        cb.setModel(model)\n        cb.setCurrentIndex(cb.findData(self.annotation, Qt.EditRole))\n\n        def on_annotation_activated():\n            self.annotation = self.label_cb.currentData(Qt.EditRole)\n            self._update_labels()\n        cb.activated.connect(on_annotation_activated)\n\n        def on_annotation_changed(value):\n            self.label_cb.setCurrentIndex(\n                self.label_cb.findData(value, Qt.EditRole))\n        self.connect_control(\"annotation\", on_annotation_changed)\n\n        grid.addWidget(self.label_cb, 0, 0, 1, 2)\n\n        cb = gui.checkBox(\n            None, self, \"label_only_subset\", \"Show labels only for subset\",\n            disabled=True,\n            callback=self._update_labels, stateWhenDisabled=False)\n        grid.addWidget(cb, 1, 0, 1, 2)\n\n        model = itemmodels.DomainModel(\n            valid_types=(DiscreteVariable, ContinuousVariable),\n            placeholder=\"None\")\n        cb = gui.comboBox(\n            None, self, \"color_by\", orientation=Qt.Horizontal,\n            model=model, callback=self._update_labels,\n            sizePolicy=QSizePolicy(QSizePolicy.MinimumExpanding,\n                                   QSizePolicy.Fixed))\n        self.color_by_label = QLabel(\"Color by:\")\n        grid.addWidget(self.color_by_label, 2, 0)\n        grid.addWidget(cb, 2, 1)\n\n        box = gui.radioButtons(\n            self.controlArea, self, \"pruning\", box=\"Pruning\",\n            callback=self._invalidate_pruning)\n        grid = QGridLayout()\n        box.layout().addLayout(grid)\n        grid.addWidget(\n            gui.appendRadioButton(box, \"None\", addToLayout=False),\n            0, 0\n        )\n        self.max_depth_spin = gui.spin(\n            box, self, \"max_depth\", minv=1, maxv=100,\n            controlWidth=spin_width, alignment=Qt.AlignRight,\n            callback=self._max_depth_changed,\n            keyboardTracking=False, addToLayout=False\n        )\n        self.max_depth_spin.lineEdit().returnPressed.connect(\n            self._max_depth_return)\n\n        grid.addWidget(\n            gui.appendRadioButton(box, \"Max depth:\", addToLayout=False),\n            1, 0)\n        grid.addWidget(self.max_depth_spin, 1, 1)\n\n        self.selection_box = gui.radioButtons(\n            self.controlArea, self, \"selection_method\",\n            box=\"Selection\",\n            callback=self._selection_method_changed)\n\n        grid = QGridLayout()\n        self.selection_box.layout().addLayout(grid)\n        grid.addWidget(\n            gui.appendRadioButton(\n                self.selection_box, \"Manual\", addToLayout=False),\n            0, 0\n        )\n        grid.addWidget(\n            gui.appendRadioButton(\n                self.selection_box, \"Height ratio:\", addToLayout=False),\n            1, 0\n        )\n        self.cut_ratio_spin = gui.spin(\n            self.selection_box, self, \"cut_ratio\", 0, 100, step=1e-1,\n            controlWidth=spin_width, alignment = Qt.AlignRight,\n            spinType=float, callback=self._cut_ratio_changed,\n            addToLayout=False\n        )\n        self.cut_ratio_spin.setSuffix(\" %\")\n        self.cut_ratio_spin.lineEdit().returnPressed.connect(\n            self._cut_ratio_return)\n\n        grid.addWidget(self.cut_ratio_spin, 1, 1)\n\n        grid.addWidget(\n            gui.appendRadioButton(\n                self.selection_box, \"Top N:\", addToLayout=False),\n            2, 0\n        )\n        self.top_n_spin = gui.spin(\n            self.selection_box, self, \"top_n\", 1, 20,\n            controlWidth=spin_width, alignment=Qt.AlignRight,\n            callback=self._top_n_changed, addToLayout=False)\n        self.top_n_spin.lineEdit().returnPressed.connect(self._top_n_return)\n        grid.addWidget(self.top_n_spin, 2, 1)\n\n        self.zoom_slider = gui.hSlider(\n            self.controlArea, self, \"zoom_factor\", box=\"Zoom\",\n            minValue=-6, maxValue=3, step=1, ticks=True, createLabel=False,\n            callback=self.__update_font_scale)\n\n        zoom_in = QAction(\n            \"Zoom in\", self, shortcut=QKeySequence.ZoomIn,\n            triggered=self.__zoom_in\n        )\n        zoom_out = QAction(\n            \"Zoom out\", self, shortcut=QKeySequence.ZoomOut,\n            triggered=self.__zoom_out\n        )\n        zoom_reset = QAction(\n            \"Reset zoom\", self,\n            shortcut=QKeySequence(Qt.ControlModifier | Qt.Key_0),\n            triggered=self.__zoom_reset\n        )\n        self.addActions([zoom_in, zoom_out, zoom_reset])\n\n        self.controlArea.layout().addStretch()\n\n        gui.auto_send(self.buttonsArea, self, \"autocommit\")\n\n        self.scene = QGraphicsScene(self)\n        self.view = GraphicsView(\n            self.scene,\n            horizontalScrollBarPolicy=Qt.ScrollBarAlwaysOff,\n            verticalScrollBarPolicy=Qt.ScrollBarAlwaysOn,\n            alignment=Qt.AlignLeft | Qt.AlignVCenter,\n            widgetResizable=True,\n        )\n        # Disable conflicting action shortcuts. We define our own.\n        for a in self.view.viewActions():\n            a.setEnabled(False)\n        self.mainArea.layout().setSpacing(1)\n        self.mainArea.layout().addWidget(self.view)\n\n        def axis_view(orientation):\n            ax = AxisItem(orientation=orientation, maxTickLength=7)\n            ax.mousePressed.connect(self._activate_cut_line)\n            ax.mouseMoved.connect(self._activate_cut_line)\n            ax.mouseReleased.connect(self._activate_cut_line)\n            ax.setRange(1.0, 0.0)\n            return ax\n\n        self.top_axis = axis_view(\"top\")\n        self.bottom_axis = axis_view(\"bottom\")\n\n        self._main_graphics = QGraphicsWidget(\n            sizePolicy=QSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.Preferred)\n        )\n        scenelayout = QGraphicsGridLayout()\n        scenelayout.setHorizontalSpacing(10)\n        scenelayout.setVerticalSpacing(10)\n\n        self._main_graphics.setLayout(scenelayout)\n        self.scene.addItem(self._main_graphics)\n        self.view.setCentralWidget(self._main_graphics)\n        self.scene.addItem(self._main_graphics)\n\n        self.dendrogram = DendrogramWidget(pen_width=2)\n        self.dendrogram.setSizePolicy(QSizePolicy.MinimumExpanding,\n                                      QSizePolicy.MinimumExpanding)\n        self.dendrogram.selectionChanged.connect(self._invalidate_output)\n        self.dendrogram.selectionEdited.connect(self._selection_edited)\n\n        self.labels = TextListView()\n        self.label_model = SelectedLabelsModel()\n        self.labels.setModel(self.label_model)\n        self.labels.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Preferred)\n        self.labels.setAlignment(Qt.AlignLeft)\n        self.labels.setMaximumWidth(200)\n\n        scenelayout.addItem(self.top_axis, 0, 0,\n                            alignment=Qt.AlignLeft | Qt.AlignVCenter)\n        scenelayout.addItem(self.dendrogram, 1, 0,\n                            alignment=Qt.AlignLeft | Qt.AlignVCenter)\n        scenelayout.addItem(self.labels, 1, 1,\n                            alignment=Qt.AlignLeft | Qt.AlignVCenter)\n        scenelayout.addItem(self.bottom_axis, 2, 0,\n                            alignment=Qt.AlignLeft | Qt.AlignVCenter)\n        self.top_axis.setZValue(self.dendrogram.zValue() + 10)\n        self.bottom_axis.setZValue(self.dendrogram.zValue() + 10)\n        self.cut_line = SliderLine(self.top_axis,\n                                   orientation=Qt.Horizontal)\n        self.cut_line.valueChanged.connect(self._dendrogram_slider_changed)\n        self.dendrogram.geometryChanged.connect(self._dendrogram_geom_changed)\n        self._set_cut_line_visible(self.selection_method == 1)\n        self.__update_font_scale()\n\n    @Inputs.distances\n    def set_distances(self, matrix):\n        self.error()\n        self.Error.clear()\n\n        self.matrix = None\n        self.Error.clear()\n        if matrix is not None:\n            if len(matrix) < 2:\n                self.Error.empty_matrix()\n            elif not matrix.is_symmetric():\n                self.Error.not_symmetric()\n            elif not np.all(np.isfinite(matrix)):\n                self.Error.not_finite_distances()\n            else:\n                self.matrix = matrix\n\n    @Inputs.subset\n    def set_subset(self, subset):\n        self.subset = subset\n        self.controls.label_only_subset.setDisabled(subset is None)\n\n    def handleNewSignals(self):\n        if self.__pending_selection_restore is not None:\n            selection_state = self.__pending_selection_restore\n        else:\n            # save the current selection to (possibly) restore later\n            selection_state = self._save_selection()\n\n        matrix = self.matrix\n        if matrix is not None:\n            self._set_items(matrix.row_items, matrix.axis)\n        else:\n            self._set_items(None)\n        self._update()\n\n        # Can now attempt to restore session state from a saved workflow.\n        if self.root and selection_state is not None:\n            self._restore_selection(selection_state)\n            self.__pending_selection_restore = None\n\n        self.Warning.clear()\n        rows = set()\n        if self.subset:\n            subsetids = set(self.subset.ids)\n            if not isinstance(self.items, Orange.data.Table) \\\n                    or not self.matrix.axis:\n                self.Warning.subset_on_no_table()\n            elif (dataids := set(self.items.ids)) and not subsetids & dataids:\n                self.Warning.subset_wrong()\n            elif not subsetids <= dataids:\n                self.Warning.subset_not_subset()\n            else:\n                indices = [leaf.value.index for leaf in leaves(self.root)]\n                rows = {\n                    row for row, rowid in enumerate(self.items.ids[indices])\n                    if rowid in subsetids\n                }\n\n        self.subset_rows = rows\n        self._update_labels()\n        self.commit.now()\n\n    def _set_items(self, items, axis=1):\n        self.closeContext()\n        self.items = items\n        model = self.label_cb.model()\n        color_model = self.controls.color_by.model()\n        color_model.set_domain(None)\n        self.color_by = None\n        if len(model) == 2 and model[0] is None:\n            if model[1] == \"Name\":\n                self.annotation_if_names = self.annotation\n            if model[1] == self.basic_annotations[1]:\n                self.annotation_if_enumerate = self.annotation\n        if isinstance(items, Orange.data.Table) and axis:\n            metas_class = tuple(\n                filter_visible(chain(items.domain.metas,\n                                     items.domain.class_vars)))\n            visible_attrs = tuple(filter_visible(items.domain.attributes))\n            if not (metas_class or visible_attrs):\n                model[:] = self.basic_annotations\n            else:\n                model[:] = (\n                    (None, )\n                    + metas_class\n                    + (model.Separator, ) * bool(metas_class and visible_attrs)\n                    + visible_attrs)\n            for meta in items.domain.metas:\n                if isinstance(meta, StringVariable):\n                    self.annotation = meta\n                    break\n            else:\n                if items.domain.class_vars:\n                    # No string metas: show class\n                    self.annotation = items.domain.class_vars[0]\n                else:\n                    # No string metas and no class: show the first option\n                    # which is not None (in the worst case, Enumeration)\n                    self.annotation = model[1]\n\n            color_model.set_domain(items.domain)\n            if items.domain.class_vars:\n                self.color_by = items.domain.class_vars[0]\n            self.openContext(items.domain)\n        elif isinstance(items, Orange.data.Table) and not axis \\\n                or (isinstance(items, list) and \\\n                    all(isinstance(var, Orange.data.Variable)\n                        for var in items)):\n            model[:] = (None, \"Name\")\n            self.annotation = self.annotation_if_names\n        else:\n            model[:] = self.basic_annotations\n            self.annotation = self.annotation_if_enumerate\n\n        no_colors = len(color_model) == 1\n        self.controls.color_by.setDisabled(no_colors)\n        self.color_by_label.setDisabled(no_colors)\n\n    def _clear_plot(self):\n        self.dendrogram.set_root(None)\n        self.label_model.clear()\n\n    def _set_displayed_root(self, root):\n        self._clear_plot()\n        self._displayed_root = root\n        self.dendrogram.set_root(root)\n        self._update_labels()\n\n    def _update(self):\n        self._clear_plot()\n\n        distances = self.matrix\n\n        if distances is not None:\n            method = LINKAGE_ARGS[self.linkage]\n            Z = dist_matrix_linkage(distances, linkage=method)\n\n            tree = tree_from_linkage(Z)\n            self.linkmatrix = Z\n            self.root = tree\n\n            self.top_axis.setRange(tree.value.height, 0.0)\n            self.bottom_axis.setRange(tree.value.height, 0.0)\n\n            if self.pruning:\n                self._set_displayed_root(prune(tree, level=self.max_depth))\n            else:\n                self._set_displayed_root(tree)\n        else:\n            self.linkmatrix = None\n            self.root = None\n            self._set_displayed_root(None)\n\n        self._apply_selection()\n\n    def _update_labels(self):\n        if not hasattr(self, \"label_model\"):\n            # This method can be called during widget initialization when\n            # creating check box for label_only_subset, if it's value is\n            # initially True.\n            # See https://github.com/biolab/orange-widget-base/pull/213;\n            # if it's merged, this check can be removed.\n            return\n\n        self.Warning.pruning_disables_colors(\n            shown=self.pruning\n                  and (self.subset_rows or self.color_by is not None))\n        labels = []\n        if self.root and self._displayed_root:\n            indices = [leaf.value.index for leaf in leaves(self.root)]\n\n            if self.annotation is None:\n                if not self.pruning \\\n                        and self.subset_rows and self.color_by is None:\n                    # Model fails if number of labels and of colors mismatch\n                    labels = [\"\"] * len(indices)\n                else:\n                    labels = []\n            elif self.annotation == \"Enumeration\":\n                labels = [str(i+1) for i in indices]\n            elif self.annotation == \"Name\":\n                attr = self.matrix.row_items.domain.attributes\n                labels = [str(attr[i]) for i in indices]\n            elif isinstance(self.annotation, Orange.data.Variable):\n                col_data = self.items.get_column(self.annotation)\n                labels = [self.annotation.str_val(val) for val in col_data]\n                labels = [labels[idx] for idx in indices]\n            else:\n                labels = []\n            if not self.pruning and \\\n                    labels and self.label_only_subset and self.subset_rows:\n                labels = [label if row in self.subset_rows else \"\"\n                          for row, label in enumerate(labels)]\n\n            if labels and self._displayed_root is not self.root:\n                joined = leaves(self._displayed_root)\n                labels = [\", \".join(labels[leaf.value.first: leaf.value.last])\n                          for leaf in joined]\n\n        self.label_model[:] = labels\n        self.label_model.set_subset(set() if self.pruning else self.subset_rows)\n        self.labels.setMinimumWidth(1 if labels else -1)\n\n        if not self.pruning and self.color_by is not None:\n            col = self.items.get_column(self.color_by)\n            self.label_model.set_colors(\n                self.color_by.palette.values_to_qcolors(col[indices]))\n        else:\n            self.label_model.set_colors(None)\n\n    def _restore_selection(self, state):\n        # type: (SelectionState) -> bool\n        \"\"\"\n        Restore the (manual) node selection state.\n\n        Return True if successful; False otherwise.\n        \"\"\"\n        linkmatrix = self.linkmatrix\n        if self.selection_method == 0 and self.root:\n            selected, linksaved = state\n            linkstruct = np.array(linksaved, dtype=float)\n            selected = set(selected)  # type: Set[Tuple[int]]\n            if not selected:\n                return False\n            if linkmatrix.shape[0] != linkstruct.shape[0]:\n                return False\n            # check that the linkage matrix structure matches. Use isclose for\n            # the height column to account for inexact floating point math\n            # (e.g. summation order in different ?gemm implementations for\n            # euclidean distances, ...)\n            if np.any(linkstruct[:, :2] != linkmatrix[:, :2]) or \\\n                    not np.all(np.isclose(linkstruct[:, 2], linkstruct[:, 2])):\n                return False\n            selection = []\n            indices = np.array([n.value.index for n in leaves(self.root)],\n                               dtype=int)\n            # mapping from ranges to display (pruned) nodes\n            mapping = {node.value.range: node\n                       for node in postorder(self._displayed_root)}\n            for node in postorder(self.root):  # type: Tree\n                r = tuple(indices[node.value.first: node.value.last])\n                if r in selected:\n                    if node.value.range not in mapping:\n                        # the node was pruned from display and cannot be\n                        # selected\n                        break\n                    selection.append(mapping[node.value.range])\n                    selected.remove(r)\n                if not selected:\n                    break  # found all, nothing more to do\n            if selection and selected:\n                # Could not restore all selected nodes (only partial match)\n                return False\n\n            self._set_selected_nodes(selection)\n            return True\n        return False\n\n    def _set_selected_nodes(self, selection):\n        # type: (List[Tree]) -> None\n        \"\"\"\n        Set the nodes in `selection` to be the current selected nodes.\n\n        The selection nodes must be subtrees of the current `_displayed_root`.\n        \"\"\"\n        self.dendrogram.selectionChanged.disconnect(self._invalidate_output)\n        try:\n            self.dendrogram.set_selected_clusters(selection)\n        finally:\n            self.dendrogram.selectionChanged.connect(self._invalidate_output)\n\n    def _max_depth_return(self):\n        if self.pruning != 1:\n            self.pruning = 1\n            self._invalidate_pruning()\n\n    def _max_depth_changed(self):\n        self.pruning = 1\n        self._invalidate_pruning()\n\n    def _invalidate_clustering(self):\n        self._update()\n        self._update_labels()\n        self._invalidate_output()\n\n    def _invalidate_output(self):\n        self.commit.deferred()\n\n    def _invalidate_pruning(self):\n        if self.root:\n            selection = self.dendrogram.selected_nodes()\n            ranges = [node.value.range for node in selection]\n            if self.pruning:\n                self._set_displayed_root(\n                    prune(self.root, level=self.max_depth))\n            else:\n                self._set_displayed_root(self.root)\n            selected = [node for node in preorder(self._displayed_root)\n                        if node.value.range in ranges]\n\n            self.dendrogram.set_selected_clusters(selected)\n\n        self._apply_selection()\n\n    @gui.deferred\n    def commit(self):\n        items = getattr(self.matrix, \"items\", self.items)\n        if not items:\n            self.Outputs.selected_data.send(None)\n            self.Outputs.annotated_data.send(None)\n            return\n\n        selection = self.dendrogram.selected_nodes()\n        selection = sorted(selection, key=lambda c: c.value.first)\n\n        indices = [leaf.value.index for leaf in leaves(self.root)]\n\n        maps = [indices[node.value.first:node.value.last]\n                for node in selection]\n\n        selected_indices = list(chain(*maps))\n\n        if not selected_indices:\n            self.Outputs.selected_data.send(None)\n            annotated_data = lazy_annotated_table(items, []) \\\n                if self.selection_method == 0 and self.matrix.axis else None\n            self.Outputs.annotated_data.send(annotated_data)\n            return\n\n        selected_data = annotated_data = None\n\n        if isinstance(items, Orange.data.Table) and self.matrix.axis == 1:\n            # Select rows\n            data, domain = items, items.domain\n\n            c = np.full(self.matrix.shape[0], len(maps))\n            for i, indices in enumerate(maps):\n                c[indices] = i\n\n            clust_name = get_unique_names(domain, \"Cluster\")\n            values = [f\"C{i + 1}\" for i in range(len(maps))]\n\n            sel_clust_var = Orange.data.DiscreteVariable(\n                name=clust_name, values=values)\n            sel_domain = add_columns(domain, metas=(sel_clust_var,))\n            selected_data = LazyValue[Table](\n                lambda: items.add_column(\n                    sel_clust_var, c, to_metas=True)[c != len(maps)],\n                domain=sel_domain, length=len(selected_indices))\n\n            ann_clust_var = Orange.data.DiscreteVariable(\n                name=clust_name, values=values + [\"Other\"]\n            )\n            ann_domain = add_columns(\n                domain_with_annotation_column(data)[0], metas=(ann_clust_var, ))\n            annotated_data = LazyValue[Table](\n                lambda: create_annotated_table(\n                    data=items.add_column(ann_clust_var, c, to_metas=True),\n                    selected_indices=selected_indices),\n                domain=ann_domain, length=len(items)\n            )\n\n        elif isinstance(items, Orange.data.Table) and self.matrix.axis == 0:\n            # Select columns\n            attrs = []\n            unselected_indices = sorted(set(range(self.root.value.last)) -\n                                        set(selected_indices))\n            for clust, indices in chain(enumerate(maps, start=1),\n                                        [(0, unselected_indices)]):\n                for i in indices:\n                    attr = items.domain[i].copy()\n                    attr.attributes[\"cluster\"] = clust\n                    attrs.append(attr)\n            all_domain = Orange.data.Domain(\n                # len(unselected_indices) can be 0\n                attrs[:len(attrs) - len(unselected_indices)],\n                items.domain.class_vars, items.domain.metas)\n\n            selected_data = LazyValue[Table](\n                lambda: items.from_table(all_domain, items),\n                domain=all_domain, length=len(items))\n\n            sel_domain = Orange.data.Domain(\n                attrs,\n                items.domain.class_vars, items.domain.metas)\n            annotated_data = LazyValue[Table](\n                lambda: items.from_table(sel_domain, items),\n                domain=sel_domain, length=len(items))\n\n        self.Outputs.selected_data.send(selected_data)\n        self.Outputs.annotated_data.send(annotated_data)\n\n    @Slot(QPointF)\n    def _activate_cut_line(self, pos: QPointF):\n        \"\"\"Activate cut line selection an set cut value to `pos.x()`.\"\"\"\n        self.selection_method = 1\n        self.cut_line.setValue(pos.x())\n        self._selection_method_changed()\n\n    def onDeleteWidget(self):\n        super().onDeleteWidget()\n        self._clear_plot()\n        self.dendrogram.clear()\n        self.dendrogram.deleteLater()\n\n    def _dendrogram_geom_changed(self):\n        pos = self.dendrogram.pos_at_height(self.cutoff_height)\n        geom = self.dendrogram.geometry()\n        self._set_slider_value(pos.x(), geom.width())\n\n        self.cut_line.setLength(\n            self.bottom_axis.geometry().bottom()\n            - self.top_axis.geometry().top()\n        )\n\n        geom = self._main_graphics.geometry()\n        assert geom.topLeft() == QPointF(0, 0)\n\n        def adjustLeft(rect):\n            rect = QRectF(rect)\n            rect.setLeft(geom.left())\n            return rect\n        margin = 3\n        self.scene.setSceneRect(geom)\n        self.view.setSceneRect(geom)\n        self.view.setHeaderSceneRect(\n            adjustLeft(self.top_axis.geometry()).adjusted(0, 0, 0, margin)\n        )\n        self.view.setFooterSceneRect(\n            adjustLeft(self.bottom_axis.geometry()).adjusted(0, -margin, 0, 0)\n        )\n\n    def _dendrogram_slider_changed(self, value):\n        p = QPointF(value, 0)\n        cl_height = self.dendrogram.height_at(p)\n\n        self.set_cutoff_height(cl_height)\n\n    def _set_slider_value(self, value, span):\n        with blocked(self.cut_line):\n            self.cut_line.setRange(0, span)\n            self.cut_line.setValue(value)\n\n    def set_cutoff_height(self, height):\n        self.cutoff_height = height\n        if self.root:\n            self.cut_ratio = 100 * height / self.root.value.height\n        self.select_max_height(height)\n\n    def _set_cut_line_visible(self, visible):\n        self.cut_line.setVisible(visible)\n\n    def select_top_n(self, n):\n        root = self._displayed_root\n        if root:\n            clusters = top_clusters(root, n)\n            self.dendrogram.set_selected_clusters(clusters)\n\n    def select_max_height(self, height):\n        root = self._displayed_root\n        if root:\n            clusters = clusters_at_height(root, height)\n            self.dendrogram.set_selected_clusters(clusters)\n\n    def _cut_ratio_changed(self):\n        self.selection_method = 1\n        self._selection_method_changed()\n\n    def _cut_ratio_return(self):\n        if self.selection_method != 1:\n            self.selection_method = 1\n            self._selection_method_changed()\n\n    def _top_n_changed(self):\n        self.selection_method = 2\n        self._selection_method_changed()\n\n    def _top_n_return(self):\n        if self.selection_method != 2:\n            self.selection_method = 2\n            self._selection_method_changed()\n\n    def _selection_method_changed(self):\n        self._set_cut_line_visible(self.selection_method == 1)\n        if self.root:\n            self._apply_selection()\n\n    def _apply_selection(self):\n        if not self.root:\n            return\n\n        if self.selection_method == 0:\n            pass\n        elif self.selection_method == 1:\n            height = self.cut_ratio * self.root.value.height / 100\n            self.set_cutoff_height(height)\n            pos = self.dendrogram.pos_at_height(height)\n            self._set_slider_value(pos.x(), self.dendrogram.size().width())\n        elif self.selection_method == 2:\n            self.select_top_n(self.top_n)\n\n    def _selection_edited(self):\n        # Selection was edited by clicking on a cluster in the\n        # dendrogram view.\n        self.selection_method = 0\n        self._selection_method_changed()\n        self._invalidate_output()\n\n    def _save_selection(self):\n        # Save the current manual node selection state\n        selection_state = None\n        if self.selection_method == 0 and self.root:\n            assert self.linkmatrix is not None\n            linkmat = [(int(_0), int(_1), _2)\n                       for _0, _1, _2 in self.linkmatrix[:, :3].tolist()]\n            nodes_ = self.dendrogram.selected_nodes()\n            # match the display (pruned) nodes back (by ranges)\n            mapping = {node.value.range: node for node in postorder(self.root)}\n            nodes = [mapping[node.value.range] for node in nodes_]\n            indices = [tuple(node.value.index for node in leaves(node))\n                       for node in nodes]\n            if nodes:\n                selection_state = (indices, linkmat)\n        return selection_state\n\n    def save_state(self):\n        # type: () -> Dict[str, Any]\n        \"\"\"\n        Save state for `set_restore_state`\n        \"\"\"\n        selection = self._save_selection()\n        res = {\"version\": (0, 0, 0)}\n        if selection is not None:\n            res[\"selection_state\"] = selection\n        return res\n\n    def set_restore_state(self, state):\n        # type: (Dict[str, Any]) -> bool\n        \"\"\"\n        Restore session data from a saved state.\n\n        Parameters\n        ----------\n        state : Dict[str, Any]\n\n        NOTE\n        ----\n        This is method called while the instance (self) is being constructed,\n        even before its `__init__` is called. Consider `self` to be only a\n        `QObject` at this stage.\n        \"\"\"\n        if \"selection_state\" in state:\n            selection = state[\"selection_state\"]\n            self.__pending_selection_restore = selection\n        return True\n\n    def __zoom_in(self):\n        def clip(minval, maxval, val):\n            return min(max(val, minval), maxval)\n        self.zoom_factor = clip(self.zoom_slider.minimum(),\n                                self.zoom_slider.maximum(),\n                                self.zoom_factor + 1)\n        self.__update_font_scale()\n\n    def __zoom_out(self):\n        def clip(minval, maxval, val):\n            return min(max(val, minval), maxval)\n        self.zoom_factor = clip(self.zoom_slider.minimum(),\n                                self.zoom_slider.maximum(),\n                                self.zoom_factor - 1)\n        self.__update_font_scale()\n\n    def __zoom_reset(self):\n        self.zoom_factor = 0\n        self.__update_font_scale()\n\n    def __update_font_scale(self):\n        font = self.scene.font()\n        factor = (1.25 ** self.zoom_factor)\n        font = qfont_scaled(font, factor)\n        self._main_graphics.setFont(font)\n        self.label_model.setFont(font)\n\n    def send_report(self):\n        annot = self.label_cb.currentText()\n        if isinstance(self.annotation, str):\n            annot = annot.lower()\n        if self.selection_method == 0:\n            sel = \"manual\"\n        elif self.selection_method == 1:\n            sel = \"at {:.1f} of height\".format(self.cut_ratio)\n        else:\n            sel = f\"top {self.top_n} {pl(self.top_n, 'cluster')}\"\n        self.report_items((\n            (\"Linkage\", LINKAGE[self.linkage]),\n            (\"Annotation\", annot),\n            (\"Pruning\",\n             self.pruning != 0 and \"{} levels\".format(self.max_depth)),\n            (\"Selection\", sel),\n        ))\n        self.report_plot()\n\n    @classmethod\n    def migrate_context(cls, context, version):\n        if version < 2:\n            if context.values[\"annotation\"] == \"None\":\n                context.values[\"annotation\"] = None",
                            "ast_type_str": "ClassDef",
                            "start_lineno": 247,
                            "end_lineno": 459,
                            "parent": {
                                "file_name": "Orange/widgets/unsupervised/owhierarchicalclustering.py",
                                "type": "class",
                                "name": "OWHierarchicalClustering",
                                "content": "class OWHierarchicalClustering(widget.OWWidget):\n    name = \"Hierarchical Clustering\"\n    description = \"Display a dendrogram of a hierarchical clustering \" \\\n                  \"constructed from the input distance matrix.\"\n    icon = \"icons/HierarchicalClustering.svg\"\n    priority = 2100\n    keywords = \"hierarchical clustering\"\n\n    class Inputs:\n        distances = Input(\"Distances\", Orange.misc.DistMatrix)\n        subset = Input(\"Data Subset\", Orange.data.Table, explicit=True)\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    settings_version = 2\n    settingsHandler = _DomainContextHandler()\n\n    #: Selected linkage\n    linkage = settings.Setting(LINKAGE.index(DEFAULT_LINKAGE))\n    #: Index of the selected annotation item (variable, ...)\n    annotation = settings.ContextSetting(\"Enumeration\")\n    #: Out-of-context setting for the case when the \"Name\" option is available\n    annotation_if_names = settings.Setting(\"Name\")\n    #: Out-of-context setting for the case with just \"Enumeration\" and \"None\"\n    annotation_if_enumerate = settings.Setting(\"Enumeration\")\n    #: Selected tree pruning (none/max depth)\n    pruning = settings.Setting(0)\n    #: Maximum depth when max depth pruning is selected\n    max_depth = settings.Setting(10)\n\n    #: Selected cluster selection method (none, cut distance, top n)\n    selection_method = settings.Setting(0)\n    #: Cut height ratio wrt root height\n    cut_ratio = settings.Setting(75.0)\n    #: Number of top clusters to select\n    top_n = settings.Setting(3)\n    #: Dendrogram zoom factor\n    zoom_factor = settings.Setting(0)\n    #: Show labels only for subset (if present)\n    label_only_subset = settings.Setting(False)\n    #: Color for label decoration\n    color_by: Union[DiscreteVariable, ContinuousVariable, None] = \\\n        settings.ContextSetting(None)\n\n    autocommit = settings.Setting(True)\n\n    graph_name = \"scene\"  # QGraphicsScene\n\n    basic_annotations = [None, \"Enumeration\"]\n\n    class Error(widget.OWWidget.Error):\n        empty_matrix = Msg(\"Distance matrix is empty.\")\n        not_finite_distances = Msg(\"Some distances are infinite\")\n        not_symmetric = widget.Msg(\"Distance matrix is not symmetric.\")\n\n    class Warning(widget.OWWidget.Warning):\n        subset_on_no_table = \\\n            Msg(\"Unused data subset: distances do not refer to data instances\")\n        subset_not_subset = \\\n            Msg(\"Some data from the subset does not appear in distance matrix\")\n        subset_wrong = \\\n            Msg(\"Subset data refers to a different table\")\n        pruning_disables_colors = \\\n            Msg(\"Pruned cluster doesn't show colors and indicate subset\")\n\n    #: Stored (manual) selection state (from a saved workflow) to restore.\n    __pending_selection_restore = None  # type: Optional[SelectionState]\n\n    def __init__(self):\n        super().__init__()\n\n        self.matrix = None\n        self.items = None\n        self.subset = None\n        self.subset_rows = set()\n        self.linkmatrix = None\n        self.root = None\n        self._displayed_root = None\n        self.cutoff_height = 0.0\n\n        spin_width = QFontMetrics(self.font()).horizontalAdvance(\"M\" * 7)\n        gui.comboBox(\n            self.controlArea, self, \"linkage\", items=LINKAGE, box=\"Linkage\",\n            callback=self._invalidate_clustering)\n\n        model = itemmodels.VariableListModel(placeholder=\"None\")\n        model[:] = self.basic_annotations\n\n        grid = QGridLayout()\n        gui.widgetBox(self.controlArea, \"Annotations\", orientation=grid)\n        self.label_cb = cb = combobox.ComboBoxSearch(\n            minimumContentsLength=14,\n            sizeAdjustPolicy=QComboBox.AdjustToMinimumContentsLengthWithIcon\n        )\n        cb.setModel(model)\n        cb.setCurrentIndex(cb.findData(self.annotation, Qt.EditRole))\n\n        def on_annotation_activated():\n            self.annotation = self.label_cb.currentData(Qt.EditRole)\n            self._update_labels()\n        cb.activated.connect(on_annotation_activated)\n\n        def on_annotation_changed(value):\n            self.label_cb.setCurrentIndex(\n                self.label_cb.findData(value, Qt.EditRole))\n        self.connect_control(\"annotation\", on_annotation_changed)\n\n        grid.addWidget(self.label_cb, 0, 0, 1, 2)\n\n        cb = gui.checkBox(\n            None, self, \"label_only_subset\", \"Show labels only for subset\",\n            disabled=True,\n            callback=self._update_labels, stateWhenDisabled=False)\n        grid.addWidget(cb, 1, 0, 1, 2)\n\n        model = itemmodels.DomainModel(\n            valid_types=(DiscreteVariable, ContinuousVariable),\n            placeholder=\"None\")\n        cb = gui.comboBox(\n            None, self, \"color_by\", orientation=Qt.Horizontal,\n            model=model, callback=self._update_labels,\n            sizePolicy=QSizePolicy(QSizePolicy.MinimumExpanding,\n                                   QSizePolicy.Fixed))\n        self.color_by_label = QLabel(\"Color by:\")\n        grid.addWidget(self.color_by_label, 2, 0)\n        grid.addWidget(cb, 2, 1)\n\n        box = gui.radioButtons(\n            self.controlArea, self, \"pruning\", box=\"Pruning\",\n            callback=self._invalidate_pruning)\n        grid = QGridLayout()\n        box.layout().addLayout(grid)\n        grid.addWidget(\n            gui.appendRadioButton(box, \"None\", addToLayout=False),\n            0, 0\n        )\n        self.max_depth_spin = gui.spin(\n            box, self, \"max_depth\", minv=1, maxv=100,\n            controlWidth=spin_width, alignment=Qt.AlignRight,\n            callback=self._max_depth_changed,\n            keyboardTracking=False, addToLayout=False\n        )\n        self.max_depth_spin.lineEdit().returnPressed.connect(\n            self._max_depth_return)\n\n        grid.addWidget(\n            gui.appendRadioButton(box, \"Max depth:\", addToLayout=False),\n            1, 0)\n        grid.addWidget(self.max_depth_spin, 1, 1)\n\n        self.selection_box = gui.radioButtons(\n            self.controlArea, self, \"selection_method\",\n            box=\"Selection\",\n            callback=self._selection_method_changed)\n\n        grid = QGridLayout()\n        self.selection_box.layout().addLayout(grid)\n        grid.addWidget(\n            gui.appendRadioButton(\n                self.selection_box, \"Manual\", addToLayout=False),\n            0, 0\n        )\n        grid.addWidget(\n            gui.appendRadioButton(\n                self.selection_box, \"Height ratio:\", addToLayout=False),\n            1, 0\n        )\n        self.cut_ratio_spin = gui.spin(\n            self.selection_box, self, \"cut_ratio\", 0, 100, step=1e-1,\n            controlWidth=spin_width, alignment = Qt.AlignRight,\n            spinType=float, callback=self._cut_ratio_changed,\n            addToLayout=False\n        )\n        self.cut_ratio_spin.setSuffix(\" %\")\n        self.cut_ratio_spin.lineEdit().returnPressed.connect(\n            self._cut_ratio_return)\n\n        grid.addWidget(self.cut_ratio_spin, 1, 1)\n\n        grid.addWidget(\n            gui.appendRadioButton(\n                self.selection_box, \"Top N:\", addToLayout=False),\n            2, 0\n        )\n        self.top_n_spin = gui.spin(\n            self.selection_box, self, \"top_n\", 1, 20,\n            controlWidth=spin_width, alignment=Qt.AlignRight,\n            callback=self._top_n_changed, addToLayout=False)\n        self.top_n_spin.lineEdit().returnPressed.connect(self._top_n_return)\n        grid.addWidget(self.top_n_spin, 2, 1)\n\n        self.zoom_slider = gui.hSlider(\n            self.controlArea, self, \"zoom_factor\", box=\"Zoom\",\n            minValue=-6, maxValue=3, step=1, ticks=True, createLabel=False,\n            callback=self.__update_font_scale)\n\n        zoom_in = QAction(\n            \"Zoom in\", self, shortcut=QKeySequence.ZoomIn,\n            triggered=self.__zoom_in\n        )\n        zoom_out = QAction(\n            \"Zoom out\", self, shortcut=QKeySequence.ZoomOut,\n            triggered=self.__zoom_out\n        )\n        zoom_reset = QAction(\n            \"Reset zoom\", self,\n            shortcut=QKeySequence(Qt.ControlModifier | Qt.Key_0),\n            triggered=self.__zoom_reset\n        )\n        self.addActions([zoom_in, zoom_out, zoom_reset])\n\n        self.controlArea.layout().addStretch()\n\n        gui.auto_send(self.buttonsArea, self, \"autocommit\")\n\n        self.scene = QGraphicsScene(self)\n        self.view = GraphicsView(\n            self.scene,\n            horizontalScrollBarPolicy=Qt.ScrollBarAlwaysOff,\n            verticalScrollBarPolicy=Qt.ScrollBarAlwaysOn,\n            alignment=Qt.AlignLeft | Qt.AlignVCenter,\n            widgetResizable=True,\n        )\n        # Disable conflicting action shortcuts. We define our own.\n        for a in self.view.viewActions():\n            a.setEnabled(False)\n        self.mainArea.layout().setSpacing(1)\n        self.mainArea.layout().addWidget(self.view)\n\n        def axis_view(orientation):\n            ax = AxisItem(orientation=orientation, maxTickLength=7)\n            ax.mousePressed.connect(self._activate_cut_line)\n            ax.mouseMoved.connect(self._activate_cut_line)\n            ax.mouseReleased.connect(self._activate_cut_line)\n            ax.setRange(1.0, 0.0)\n            return ax\n\n        self.top_axis = axis_view(\"top\")\n        self.bottom_axis = axis_view(\"bottom\")\n\n        self._main_graphics = QGraphicsWidget(\n            sizePolicy=QSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.Preferred)\n        )\n        scenelayout = QGraphicsGridLayout()\n        scenelayout.setHorizontalSpacing(10)\n        scenelayout.setVerticalSpacing(10)\n\n        self._main_graphics.setLayout(scenelayout)\n        self.scene.addItem(self._main_graphics)\n        self.view.setCentralWidget(self._main_graphics)\n        self.scene.addItem(self._main_graphics)\n\n        self.dendrogram = DendrogramWidget(pen_width=2)\n        self.dendrogram.setSizePolicy(QSizePolicy.MinimumExpanding,\n                                      QSizePolicy.MinimumExpanding)\n        self.dendrogram.selectionChanged.connect(self._invalidate_output)\n        self.dendrogram.selectionEdited.connect(self._selection_edited)\n\n        self.labels = TextListView()\n        self.label_model = SelectedLabelsModel()\n        self.labels.setModel(self.label_model)\n        self.labels.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Preferred)\n        self.labels.setAlignment(Qt.AlignLeft)\n        self.labels.setMaximumWidth(200)\n\n        scenelayout.addItem(self.top_axis, 0, 0,\n                            alignment=Qt.AlignLeft | Qt.AlignVCenter)\n        scenelayout.addItem(self.dendrogram, 1, 0,\n                            alignment=Qt.AlignLeft | Qt.AlignVCenter)\n        scenelayout.addItem(self.labels, 1, 1,\n                            alignment=Qt.AlignLeft | Qt.AlignVCenter)\n        scenelayout.addItem(self.bottom_axis, 2, 0,\n                            alignment=Qt.AlignLeft | Qt.AlignVCenter)\n        self.top_axis.setZValue(self.dendrogram.zValue() + 10)\n        self.bottom_axis.setZValue(self.dendrogram.zValue() + 10)\n        self.cut_line = SliderLine(self.top_axis,\n                                   orientation=Qt.Horizontal)\n        self.cut_line.valueChanged.connect(self._dendrogram_slider_changed)\n        self.dendrogram.geometryChanged.connect(self._dendrogram_geom_changed)\n        self._set_cut_line_visible(self.selection_method == 1)\n        self.__update_font_scale()\n\n    @Inputs.distances\n    def set_distances(self, matrix):\n        self.error()\n        self.Error.clear()\n\n        self.matrix = None\n        self.Error.clear()\n        if matrix is not None:\n            if len(matrix) < 2:\n                self.Error.empty_matrix()\n            elif not matrix.is_symmetric():\n                self.Error.not_symmetric()\n            elif not np.all(np.isfinite(matrix)):\n                self.Error.not_finite_distances()\n            else:\n                self.matrix = matrix\n\n    @Inputs.subset\n    def set_subset(self, subset):\n        self.subset = subset\n        self.controls.label_only_subset.setDisabled(subset is None)\n\n    def handleNewSignals(self):\n        if self.__pending_selection_restore is not None:\n            selection_state = self.__pending_selection_restore\n        else:\n            # save the current selection to (possibly) restore later\n            selection_state = self._save_selection()\n\n        matrix = self.matrix\n        if matrix is not None:\n            self._set_items(matrix.row_items, matrix.axis)\n        else:\n            self._set_items(None)\n        self._update()\n\n        # Can now attempt to restore session state from a saved workflow.\n        if self.root and selection_state is not None:\n            self._restore_selection(selection_state)\n            self.__pending_selection_restore = None\n\n        self.Warning.clear()\n        rows = set()\n        if self.subset:\n            subsetids = set(self.subset.ids)\n            if not isinstance(self.items, Orange.data.Table) \\\n                    or not self.matrix.axis:\n                self.Warning.subset_on_no_table()\n            elif (dataids := set(self.items.ids)) and not subsetids & dataids:\n                self.Warning.subset_wrong()\n            elif not subsetids <= dataids:\n                self.Warning.subset_not_subset()\n            else:\n                indices = [leaf.value.index for leaf in leaves(self.root)]\n                rows = {\n                    row for row, rowid in enumerate(self.items.ids[indices])\n                    if rowid in subsetids\n                }\n\n        self.subset_rows = rows\n        self._update_labels()\n        self.commit.now()\n\n    def _set_items(self, items, axis=1):\n        self.closeContext()\n        self.items = items\n        model = self.label_cb.model()\n        color_model = self.controls.color_by.model()\n        color_model.set_domain(None)\n        self.color_by = None\n        if len(model) == 2 and model[0] is None:\n            if model[1] == \"Name\":\n                self.annotation_if_names = self.annotation\n            if model[1] == self.basic_annotations[1]:\n                self.annotation_if_enumerate = self.annotation\n        if isinstance(items, Orange.data.Table) and axis:\n            metas_class = tuple(\n                filter_visible(chain(items.domain.metas,\n                                     items.domain.class_vars)))\n            visible_attrs = tuple(filter_visible(items.domain.attributes))\n            if not (metas_class or visible_attrs):\n                model[:] = self.basic_annotations\n            else:\n                model[:] = (\n                    (None, )\n                    + metas_class\n                    + (model.Separator, ) * bool(metas_class and visible_attrs)\n                    + visible_attrs)\n            for meta in items.domain.metas:\n                if isinstance(meta, StringVariable):\n                    self.annotation = meta\n                    break\n            else:\n                if items.domain.class_vars:\n                    # No string metas: show class\n                    self.annotation = items.domain.class_vars[0]\n                else:\n                    # No string metas and no class: show the first option\n                    # which is not None (in the worst case, Enumeration)\n                    self.annotation = model[1]\n\n            color_model.set_domain(items.domain)\n            if items.domain.class_vars:\n                self.color_by = items.domain.class_vars[0]\n            self.openContext(items.domain)\n        elif isinstance(items, Orange.data.Table) and not axis \\\n                or (isinstance(items, list) and \\\n                    all(isinstance(var, Orange.data.Variable)\n                        for var in items)):\n            model[:] = (None, \"Name\")\n            self.annotation = self.annotation_if_names\n        else:\n            model[:] = self.basic_annotations\n            self.annotation = self.annotation_if_enumerate\n\n        no_colors = len(color_model) == 1\n        self.controls.color_by.setDisabled(no_colors)\n        self.color_by_label.setDisabled(no_colors)\n\n    def _clear_plot(self):\n        self.dendrogram.set_root(None)\n        self.label_model.clear()\n\n    def _set_displayed_root(self, root):\n        self._clear_plot()\n        self._displayed_root = root\n        self.dendrogram.set_root(root)\n        self._update_labels()\n\n    def _update(self):\n        self._clear_plot()\n\n        distances = self.matrix\n\n        if distances is not None:\n            method = LINKAGE_ARGS[self.linkage]\n            Z = dist_matrix_linkage(distances, linkage=method)\n\n            tree = tree_from_linkage(Z)\n            self.linkmatrix = Z\n            self.root = tree\n\n            self.top_axis.setRange(tree.value.height, 0.0)\n            self.bottom_axis.setRange(tree.value.height, 0.0)\n\n            if self.pruning:\n                self._set_displayed_root(prune(tree, level=self.max_depth))\n            else:\n                self._set_displayed_root(tree)\n        else:\n            self.linkmatrix = None\n            self.root = None\n            self._set_displayed_root(None)\n\n        self._apply_selection()\n\n    def _update_labels(self):\n        if not hasattr(self, \"label_model\"):\n            # This method can be called during widget initialization when\n            # creating check box for label_only_subset, if it's value is\n            # initially True.\n            # See https://github.com/biolab/orange-widget-base/pull/213;\n            # if it's merged, this check can be removed.\n            return\n\n        self.Warning.pruning_disables_colors(\n            shown=self.pruning\n                  and (self.subset_rows or self.color_by is not None))\n        labels = []\n        if self.root and self._displayed_root:\n            indices = [leaf.value.index for leaf in leaves(self.root)]\n\n            if self.annotation is None:\n                if not self.pruning \\\n                        and self.subset_rows and self.color_by is None:\n                    # Model fails if number of labels and of colors mismatch\n                    labels = [\"\"] * len(indices)\n                else:\n                    labels = []\n            elif self.annotation == \"Enumeration\":\n                labels = [str(i+1) for i in indices]\n            elif self.annotation == \"Name\":\n                attr = self.matrix.row_items.domain.attributes\n                labels = [str(attr[i]) for i in indices]\n            elif isinstance(self.annotation, Orange.data.Variable):\n                col_data = self.items.get_column(self.annotation)\n                labels = [self.annotation.str_val(val) for val in col_data]\n                labels = [labels[idx] for idx in indices]\n            else:\n                labels = []\n            if not self.pruning and \\\n                    labels and self.label_only_subset and self.subset_rows:\n                labels = [label if row in self.subset_rows else \"\"\n                          for row, label in enumerate(labels)]\n\n            if labels and self._displayed_root is not self.root:\n                joined = leaves(self._displayed_root)\n                labels = [\", \".join(labels[leaf.value.first: leaf.value.last])\n                          for leaf in joined]\n\n        self.label_model[:] = labels\n        self.label_model.set_subset(set() if self.pruning else self.subset_rows)\n        self.labels.setMinimumWidth(1 if labels else -1)\n\n        if not self.pruning and self.color_by is not None:\n            col = self.items.get_column(self.color_by)\n            self.label_model.set_colors(\n                self.color_by.palette.values_to_qcolors(col[indices]))\n        else:\n            self.label_model.set_colors(None)\n\n    def _restore_selection(self, state):\n        # type: (SelectionState) -> bool\n        \"\"\"\n        Restore the (manual) node selection state.\n\n        Return True if successful; False otherwise.\n        \"\"\"\n        linkmatrix = self.linkmatrix\n        if self.selection_method == 0 and self.root:\n            selected, linksaved = state\n            linkstruct = np.array(linksaved, dtype=float)\n            selected = set(selected)  # type: Set[Tuple[int]]\n            if not selected:\n                return False\n            if linkmatrix.shape[0] != linkstruct.shape[0]:\n                return False\n            # check that the linkage matrix structure matches. Use isclose for\n            # the height column to account for inexact floating point math\n            # (e.g. summation order in different ?gemm implementations for\n            # euclidean distances, ...)\n            if np.any(linkstruct[:, :2] != linkmatrix[:, :2]) or \\\n                    not np.all(np.isclose(linkstruct[:, 2], linkstruct[:, 2])):\n                return False\n            selection = []\n            indices = np.array([n.value.index for n in leaves(self.root)],\n                               dtype=int)\n            # mapping from ranges to display (pruned) nodes\n            mapping = {node.value.range: node\n                       for node in postorder(self._displayed_root)}\n            for node in postorder(self.root):  # type: Tree\n                r = tuple(indices[node.value.first: node.value.last])\n                if r in selected:\n                    if node.value.range not in mapping:\n                        # the node was pruned from display and cannot be\n                        # selected\n                        break\n                    selection.append(mapping[node.value.range])\n                    selected.remove(r)\n                if not selected:\n                    break  # found all, nothing more to do\n            if selection and selected:\n                # Could not restore all selected nodes (only partial match)\n                return False\n\n            self._set_selected_nodes(selection)\n            return True\n        return False\n\n    def _set_selected_nodes(self, selection):\n        # type: (List[Tree]) -> None\n        \"\"\"\n        Set the nodes in `selection` to be the current selected nodes.\n\n        The selection nodes must be subtrees of the current `_displayed_root`.\n        \"\"\"\n        self.dendrogram.selectionChanged.disconnect(self._invalidate_output)\n        try:\n            self.dendrogram.set_selected_clusters(selection)\n        finally:\n            self.dendrogram.selectionChanged.connect(self._invalidate_output)\n\n    def _max_depth_return(self):\n        if self.pruning != 1:\n            self.pruning = 1\n            self._invalidate_pruning()\n\n    def _max_depth_changed(self):\n        self.pruning = 1\n        self._invalidate_pruning()\n\n    def _invalidate_clustering(self):\n        self._update()\n        self._update_labels()\n        self._invalidate_output()\n\n    def _invalidate_output(self):\n        self.commit.deferred()\n\n    def _invalidate_pruning(self):\n        if self.root:\n            selection = self.dendrogram.selected_nodes()\n            ranges = [node.value.range for node in selection]\n            if self.pruning:\n                self._set_displayed_root(\n                    prune(self.root, level=self.max_depth))\n            else:\n                self._set_displayed_root(self.root)\n            selected = [node for node in preorder(self._displayed_root)\n                        if node.value.range in ranges]\n\n            self.dendrogram.set_selected_clusters(selected)\n\n        self._apply_selection()\n\n    @gui.deferred\n    def commit(self):\n        items = getattr(self.matrix, \"items\", self.items)\n        if not items:\n            self.Outputs.selected_data.send(None)\n            self.Outputs.annotated_data.send(None)\n            return\n\n        selection = self.dendrogram.selected_nodes()\n        selection = sorted(selection, key=lambda c: c.value.first)\n\n        indices = [leaf.value.index for leaf in leaves(self.root)]\n\n        maps = [indices[node.value.first:node.value.last]\n                for node in selection]\n\n        selected_indices = list(chain(*maps))\n\n        if not selected_indices:\n            self.Outputs.selected_data.send(None)\n            annotated_data = lazy_annotated_table(items, []) \\\n                if self.selection_method == 0 and self.matrix.axis else None\n            self.Outputs.annotated_data.send(annotated_data)\n            return\n\n        selected_data = annotated_data = None\n\n        if isinstance(items, Orange.data.Table) and self.matrix.axis == 1:\n            # Select rows\n            data, domain = items, items.domain\n\n            c = np.full(self.matrix.shape[0], len(maps))\n            for i, indices in enumerate(maps):\n                c[indices] = i\n\n            clust_name = get_unique_names(domain, \"Cluster\")\n            values = [f\"C{i + 1}\" for i in range(len(maps))]\n\n            sel_clust_var = Orange.data.DiscreteVariable(\n                name=clust_name, values=values)\n            sel_domain = add_columns(domain, metas=(sel_clust_var,))\n            selected_data = LazyValue[Table](\n                lambda: items.add_column(\n                    sel_clust_var, c, to_metas=True)[c != len(maps)],\n                domain=sel_domain, length=len(selected_indices))\n\n            ann_clust_var = Orange.data.DiscreteVariable(\n                name=clust_name, values=values + [\"Other\"]\n            )\n            ann_domain = add_columns(\n                domain_with_annotation_column(data)[0], metas=(ann_clust_var, ))\n            annotated_data = LazyValue[Table](\n                lambda: create_annotated_table(\n                    data=items.add_column(ann_clust_var, c, to_metas=True),\n                    selected_indices=selected_indices),\n                domain=ann_domain, length=len(items)\n            )\n\n        elif isinstance(items, Orange.data.Table) and self.matrix.axis == 0:\n            # Select columns\n            attrs = []\n            unselected_indices = sorted(set(range(self.root.value.last)) -\n                                        set(selected_indices))\n            for clust, indices in chain(enumerate(maps, start=1),\n                                        [(0, unselected_indices)]):\n                for i in indices:\n                    attr = items.domain[i].copy()\n                    attr.attributes[\"cluster\"] = clust\n                    attrs.append(attr)\n            all_domain = Orange.data.Domain(\n                # len(unselected_indices) can be 0\n                attrs[:len(attrs) - len(unselected_indices)],\n                items.domain.class_vars, items.domain.metas)\n\n            selected_data = LazyValue[Table](\n                lambda: items.from_table(all_domain, items),\n                domain=all_domain, length=len(items))\n\n            sel_domain = Orange.data.Domain(\n                attrs,\n                items.domain.class_vars, items.domain.metas)\n            annotated_data = LazyValue[Table](\n                lambda: items.from_table(sel_domain, items),\n                domain=sel_domain, length=len(items))\n\n        self.Outputs.selected_data.send(selected_data)\n        self.Outputs.annotated_data.send(annotated_data)\n\n    @Slot(QPointF)\n    def _activate_cut_line(self, pos: QPointF):\n        \"\"\"Activate cut line selection an set cut value to `pos.x()`.\"\"\"\n        self.selection_method = 1\n        self.cut_line.setValue(pos.x())\n        self._selection_method_changed()\n\n    def onDeleteWidget(self):\n        super().onDeleteWidget()\n        self._clear_plot()\n        self.dendrogram.clear()\n        self.dendrogram.deleteLater()\n\n    def _dendrogram_geom_changed(self):\n        pos = self.dendrogram.pos_at_height(self.cutoff_height)\n        geom = self.dendrogram.geometry()\n        self._set_slider_value(pos.x(), geom.width())\n\n        self.cut_line.setLength(\n            self.bottom_axis.geometry().bottom()\n            - self.top_axis.geometry().top()\n        )\n\n        geom = self._main_graphics.geometry()\n        assert geom.topLeft() == QPointF(0, 0)\n\n        def adjustLeft(rect):\n            rect = QRectF(rect)\n            rect.setLeft(geom.left())\n            return rect\n        margin = 3\n        self.scene.setSceneRect(geom)\n        self.view.setSceneRect(geom)\n        self.view.setHeaderSceneRect(\n            adjustLeft(self.top_axis.geometry()).adjusted(0, 0, 0, margin)\n        )\n        self.view.setFooterSceneRect(\n            adjustLeft(self.bottom_axis.geometry()).adjusted(0, -margin, 0, 0)\n        )\n\n    def _dendrogram_slider_changed(self, value):\n        p = QPointF(value, 0)\n        cl_height = self.dendrogram.height_at(p)\n\n        self.set_cutoff_height(cl_height)\n\n    def _set_slider_value(self, value, span):\n        with blocked(self.cut_line):\n            self.cut_line.setRange(0, span)\n            self.cut_line.setValue(value)\n\n    def set_cutoff_height(self, height):\n        self.cutoff_height = height\n        if self.root:\n            self.cut_ratio = 100 * height / self.root.value.height\n        self.select_max_height(height)\n\n    def _set_cut_line_visible(self, visible):\n        self.cut_line.setVisible(visible)\n\n    def select_top_n(self, n):\n        root = self._displayed_root\n        if root:\n            clusters = top_clusters(root, n)\n            self.dendrogram.set_selected_clusters(clusters)\n\n    def select_max_height(self, height):\n        root = self._displayed_root\n        if root:\n            clusters = clusters_at_height(root, height)\n            self.dendrogram.set_selected_clusters(clusters)\n\n    def _cut_ratio_changed(self):\n        self.selection_method = 1\n        self._selection_method_changed()\n\n    def _cut_ratio_return(self):\n        if self.selection_method != 1:\n            self.selection_method = 1\n            self._selection_method_changed()\n\n    def _top_n_changed(self):\n        self.selection_method = 2\n        self._selection_method_changed()\n\n    def _top_n_return(self):\n        if self.selection_method != 2:\n            self.selection_method = 2\n            self._selection_method_changed()\n\n    def _selection_method_changed(self):\n        self._set_cut_line_visible(self.selection_method == 1)\n        if self.root:\n            self._apply_selection()\n\n    def _apply_selection(self):\n        if not self.root:\n            return\n\n        if self.selection_method == 0:\n            pass\n        elif self.selection_method == 1:\n            height = self.cut_ratio * self.root.value.height / 100\n            self.set_cutoff_height(height)\n            pos = self.dendrogram.pos_at_height(height)\n            self._set_slider_value(pos.x(), self.dendrogram.size().width())\n        elif self.selection_method == 2:\n            self.select_top_n(self.top_n)\n\n    def _selection_edited(self):\n        # Selection was edited by clicking on a cluster in the\n        # dendrogram view.\n        self.selection_method = 0\n        self._selection_method_changed()\n        self._invalidate_output()\n\n    def _save_selection(self):\n        # Save the current manual node selection state\n        selection_state = None\n        if self.selection_method == 0 and self.root:\n            assert self.linkmatrix is not None\n            linkmat = [(int(_0), int(_1), _2)\n                       for _0, _1, _2 in self.linkmatrix[:, :3].tolist()]\n            nodes_ = self.dendrogram.selected_nodes()\n            # match the display (pruned) nodes back (by ranges)\n            mapping = {node.value.range: node for node in postorder(self.root)}\n            nodes = [mapping[node.value.range] for node in nodes_]\n            indices = [tuple(node.value.index for node in leaves(node))\n                       for node in nodes]\n            if nodes:\n                selection_state = (indices, linkmat)\n        return selection_state\n\n    def save_state(self):\n        # type: () -> Dict[str, Any]\n        \"\"\"\n        Save state for `set_restore_state`\n        \"\"\"\n        selection = self._save_selection()\n        res = {\"version\": (0, 0, 0)}\n        if selection is not None:\n            res[\"selection_state\"] = selection\n        return res\n\n    def set_restore_state(self, state):\n        # type: (Dict[str, Any]) -> bool\n        \"\"\"\n        Restore session data from a saved state.\n\n        Parameters\n        ----------\n        state : Dict[str, Any]\n\n        NOTE\n        ----\n        This is method called while the instance (self) is being constructed,\n        even before its `__init__` is called. Consider `self` to be only a\n        `QObject` at this stage.\n        \"\"\"\n        if \"selection_state\" in state:\n            selection = state[\"selection_state\"]\n            self.__pending_selection_restore = selection\n        return True\n\n    def __zoom_in(self):\n        def clip(minval, maxval, val):\n            return min(max(val, minval), maxval)\n        self.zoom_factor = clip(self.zoom_slider.minimum(),\n                                self.zoom_slider.maximum(),\n                                self.zoom_factor + 1)\n        self.__update_font_scale()\n\n    def __zoom_out(self):\n        def clip(minval, maxval, val):\n            return min(max(val, minval), maxval)\n        self.zoom_factor = clip(self.zoom_slider.minimum(),\n                                self.zoom_slider.maximum(),\n                                self.zoom_factor - 1)\n        self.__update_font_scale()\n\n    def __zoom_reset(self):\n        self.zoom_factor = 0\n        self.__update_font_scale()\n\n    def __update_font_scale(self):\n        font = self.scene.font()\n        factor = (1.25 ** self.zoom_factor)\n        font = qfont_scaled(font, factor)\n        self._main_graphics.setFont(font)\n        self.label_model.setFont(font)\n\n    def send_report(self):\n        annot = self.label_cb.currentText()\n        if isinstance(self.annotation, str):\n            annot = annot.lower()\n        if self.selection_method == 0:\n            sel = \"manual\"\n        elif self.selection_method == 1:\n            sel = \"at {:.1f} of height\".format(self.cut_ratio)\n        else:\n            sel = f\"top {self.top_n} {pl(self.top_n, 'cluster')}\"\n        self.report_items((\n            (\"Linkage\", LINKAGE[self.linkage]),\n            (\"Annotation\", annot),\n            (\"Pruning\",\n             self.pruning != 0 and \"{} levels\".format(self.max_depth)),\n            (\"Selection\", sel),\n        ))\n        self.report_plot()\n\n    @classmethod\n    def migrate_context(cls, context, version):\n        if version < 2:\n            if context.values[\"annotation\"] == \"None\":\n                context.values[\"annotation\"] = None",
                                "ast_type_str": "ClassDef",
                                "start_lineno": 177,
                                "end_lineno": 1068,
                                "parent": null
                            }
                        },
                        {
                            "file_name": "Orange/widgets/unsupervised/owhierarchicalclustering.py",
                            "type": "class",
                            "name": "OWHierarchicalClustering",
                            "content": "class OWHierarchicalClustering(widget.OWWidget):\n    name = \"Hierarchical Clustering\"\n    description = \"Display a dendrogram of a hierarchical clustering \" \\\n                  \"constructed from the input distance matrix.\"\n    icon = \"icons/HierarchicalClustering.svg\"\n    priority = 2100\n    keywords = \"hierarchical clustering\"\n\n    class Inputs:\n        distances = Input(\"Distances\", Orange.misc.DistMatrix)\n        subset = Input(\"Data Subset\", Orange.data.Table, explicit=True)\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    settings_version = 2\n    settingsHandler = _DomainContextHandler()\n\n    #: Selected linkage\n    linkage = settings.Setting(LINKAGE.index(DEFAULT_LINKAGE))\n    #: Index of the selected annotation item (variable, ...)\n    annotation = settings.ContextSetting(\"Enumeration\")\n    #: Out-of-context setting for the case when the \"Name\" option is available\n    annotation_if_names = settings.Setting(\"Name\")\n    #: Out-of-context setting for the case with just \"Enumeration\" and \"None\"\n    annotation_if_enumerate = settings.Setting(\"Enumeration\")\n    #: Selected tree pruning (none/max depth)\n    pruning = settings.Setting(0)\n    #: Maximum depth when max depth pruning is selected\n    max_depth = settings.Setting(10)\n\n    #: Selected cluster selection method (none, cut distance, top n)\n    selection_method = settings.Setting(0)\n    #: Cut height ratio wrt root height\n    cut_ratio = settings.Setting(75.0)\n    #: Number of top clusters to select\n    top_n = settings.Setting(3)\n    #: Dendrogram zoom factor\n    zoom_factor = settings.Setting(0)\n    #: Show labels only for subset (if present)\n    label_only_subset = settings.Setting(False)\n    #: Color for label decoration\n    color_by: Union[DiscreteVariable, ContinuousVariable, None] = \\\n        settings.ContextSetting(None)\n\n    autocommit = settings.Setting(True)\n\n    graph_name = \"scene\"  # QGraphicsScene\n\n    basic_annotations = [None, \"Enumeration\"]\n\n    class Error(widget.OWWidget.Error):\n        empty_matrix = Msg(\"Distance matrix is empty.\")\n        not_finite_distances = Msg(\"Some distances are infinite\")\n        not_symmetric = widget.Msg(\"Distance matrix is not symmetric.\")\n\n    class Warning(widget.OWWidget.Warning):\n        subset_on_no_table = \\\n            Msg(\"Unused data subset: distances do not refer to data instances\")\n        subset_not_subset = \\\n            Msg(\"Some data from the subset does not appear in distance matrix\")\n        subset_wrong = \\\n            Msg(\"Subset data refers to a different table\")\n        pruning_disables_colors = \\\n            Msg(\"Pruned cluster doesn't show colors and indicate subset\")\n\n    #: Stored (manual) selection state (from a saved workflow) to restore.\n    __pending_selection_restore = None  # type: Optional[SelectionState]\n\n    def __init__(self):\n        super().__init__()\n\n        self.matrix = None\n        self.items = None\n        self.subset = None\n        self.subset_rows = set()\n        self.linkmatrix = None\n        self.root = None\n        self._displayed_root = None\n        self.cutoff_height = 0.0\n\n        spin_width = QFontMetrics(self.font()).horizontalAdvance(\"M\" * 7)\n        gui.comboBox(\n            self.controlArea, self, \"linkage\", items=LINKAGE, box=\"Linkage\",\n            callback=self._invalidate_clustering)\n\n        model = itemmodels.VariableListModel(placeholder=\"None\")\n        model[:] = self.basic_annotations\n\n        grid = QGridLayout()\n        gui.widgetBox(self.controlArea, \"Annotations\", orientation=grid)\n        self.label_cb = cb = combobox.ComboBoxSearch(\n            minimumContentsLength=14,\n            sizeAdjustPolicy=QComboBox.AdjustToMinimumContentsLengthWithIcon\n        )\n        cb.setModel(model)\n        cb.setCurrentIndex(cb.findData(self.annotation, Qt.EditRole))\n\n        def on_annotation_activated():\n            self.annotation = self.label_cb.currentData(Qt.EditRole)\n            self._update_labels()\n        cb.activated.connect(on_annotation_activated)\n\n        def on_annotation_changed(value):\n            self.label_cb.setCurrentIndex(\n                self.label_cb.findData(value, Qt.EditRole))\n        self.connect_control(\"annotation\", on_annotation_changed)\n\n        grid.addWidget(self.label_cb, 0, 0, 1, 2)\n\n        cb = gui.checkBox(\n            None, self, \"label_only_subset\", \"Show labels only for subset\",\n            disabled=True,\n            callback=self._update_labels, stateWhenDisabled=False)\n        grid.addWidget(cb, 1, 0, 1, 2)\n\n        model = itemmodels.DomainModel(\n            valid_types=(DiscreteVariable, ContinuousVariable),\n            placeholder=\"None\")\n        cb = gui.comboBox(\n            None, self, \"color_by\", orientation=Qt.Horizontal,\n            model=model, callback=self._update_labels,\n            sizePolicy=QSizePolicy(QSizePolicy.MinimumExpanding,\n                                   QSizePolicy.Fixed))\n        self.color_by_label = QLabel(\"Color by:\")\n        grid.addWidget(self.color_by_label, 2, 0)\n        grid.addWidget(cb, 2, 1)\n\n        box = gui.radioButtons(\n            self.controlArea, self, \"pruning\", box=\"Pruning\",\n            callback=self._invalidate_pruning)\n        grid = QGridLayout()\n        box.layout().addLayout(grid)\n        grid.addWidget(\n            gui.appendRadioButton(box, \"None\", addToLayout=False),\n            0, 0\n        )\n        self.max_depth_spin = gui.spin(\n            box, self, \"max_depth\", minv=1, maxv=100,\n            controlWidth=spin_width, alignment=Qt.AlignRight,\n            callback=self._max_depth_changed,\n            keyboardTracking=False, addToLayout=False\n        )\n        self.max_depth_spin.lineEdit().returnPressed.connect(\n            self._max_depth_return)\n\n        grid.addWidget(\n            gui.appendRadioButton(box, \"Max depth:\", addToLayout=False),\n            1, 0)\n        grid.addWidget(self.max_depth_spin, 1, 1)\n\n        self.selection_box = gui.radioButtons(\n            self.controlArea, self, \"selection_method\",\n            box=\"Selection\",\n            callback=self._selection_method_changed)\n\n        grid = QGridLayout()\n        self.selection_box.layout().addLayout(grid)\n        grid.addWidget(\n            gui.appendRadioButton(\n                self.selection_box, \"Manual\", addToLayout=False),\n            0, 0\n        )\n        grid.addWidget(\n            gui.appendRadioButton(\n                self.selection_box, \"Height ratio:\", addToLayout=False),\n            1, 0\n        )\n        self.cut_ratio_spin = gui.spin(\n            self.selection_box, self, \"cut_ratio\", 0, 100, step=1e-1,\n            controlWidth=spin_width, alignment = Qt.AlignRight,\n            spinType=float, callback=self._cut_ratio_changed,\n            addToLayout=False\n        )\n        self.cut_ratio_spin.setSuffix(\" %\")\n        self.cut_ratio_spin.lineEdit().returnPressed.connect(\n            self._cut_ratio_return)\n\n        grid.addWidget(self.cut_ratio_spin, 1, 1)\n\n        grid.addWidget(\n            gui.appendRadioButton(\n                self.selection_box, \"Top N:\", addToLayout=False),\n            2, 0\n        )\n        self.top_n_spin = gui.spin(\n            self.selection_box, self, \"top_n\", 1, 20,\n            controlWidth=spin_width, alignment=Qt.AlignRight,\n            callback=self._top_n_changed, addToLayout=False)\n        self.top_n_spin.lineEdit().returnPressed.connect(self._top_n_return)\n        grid.addWidget(self.top_n_spin, 2, 1)\n\n        self.zoom_slider = gui.hSlider(\n            self.controlArea, self, \"zoom_factor\", box=\"Zoom\",\n            minValue=-6, maxValue=3, step=1, ticks=True, createLabel=False,\n            callback=self.__update_font_scale)\n\n        zoom_in = QAction(\n            \"Zoom in\", self, shortcut=QKeySequence.ZoomIn,\n            triggered=self.__zoom_in\n        )\n        zoom_out = QAction(\n            \"Zoom out\", self, shortcut=QKeySequence.ZoomOut,\n            triggered=self.__zoom_out\n        )\n        zoom_reset = QAction(\n            \"Reset zoom\", self,\n            shortcut=QKeySequence(Qt.ControlModifier | Qt.Key_0),\n            triggered=self.__zoom_reset\n        )\n        self.addActions([zoom_in, zoom_out, zoom_reset])\n\n        self.controlArea.layout().addStretch()\n\n        gui.auto_send(self.buttonsArea, self, \"autocommit\")\n\n        self.scene = QGraphicsScene(self)\n        self.view = GraphicsView(\n            self.scene,\n            horizontalScrollBarPolicy=Qt.ScrollBarAlwaysOff,\n            verticalScrollBarPolicy=Qt.ScrollBarAlwaysOn,\n            alignment=Qt.AlignLeft | Qt.AlignVCenter,\n            widgetResizable=True,\n        )\n        # Disable conflicting action shortcuts. We define our own.\n        for a in self.view.viewActions():\n            a.setEnabled(False)\n        self.mainArea.layout().setSpacing(1)\n        self.mainArea.layout().addWidget(self.view)\n\n        def axis_view(orientation):\n            ax = AxisItem(orientation=orientation, maxTickLength=7)\n            ax.mousePressed.connect(self._activate_cut_line)\n            ax.mouseMoved.connect(self._activate_cut_line)\n            ax.mouseReleased.connect(self._activate_cut_line)\n            ax.setRange(1.0, 0.0)\n            return ax\n\n        self.top_axis = axis_view(\"top\")\n        self.bottom_axis = axis_view(\"bottom\")\n\n        self._main_graphics = QGraphicsWidget(\n            sizePolicy=QSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.Preferred)\n        )\n        scenelayout = QGraphicsGridLayout()\n        scenelayout.setHorizontalSpacing(10)\n        scenelayout.setVerticalSpacing(10)\n\n        self._main_graphics.setLayout(scenelayout)\n        self.scene.addItem(self._main_graphics)\n        self.view.setCentralWidget(self._main_graphics)\n        self.scene.addItem(self._main_graphics)\n\n        self.dendrogram = DendrogramWidget(pen_width=2)\n        self.dendrogram.setSizePolicy(QSizePolicy.MinimumExpanding,\n                                      QSizePolicy.MinimumExpanding)\n        self.dendrogram.selectionChanged.connect(self._invalidate_output)\n        self.dendrogram.selectionEdited.connect(self._selection_edited)\n\n        self.labels = TextListView()\n        self.label_model = SelectedLabelsModel()\n        self.labels.setModel(self.label_model)\n        self.labels.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Preferred)\n        self.labels.setAlignment(Qt.AlignLeft)\n        self.labels.setMaximumWidth(200)\n\n        scenelayout.addItem(self.top_axis, 0, 0,\n                            alignment=Qt.AlignLeft | Qt.AlignVCenter)\n        scenelayout.addItem(self.dendrogram, 1, 0,\n                            alignment=Qt.AlignLeft | Qt.AlignVCenter)\n        scenelayout.addItem(self.labels, 1, 1,\n                            alignment=Qt.AlignLeft | Qt.AlignVCenter)\n        scenelayout.addItem(self.bottom_axis, 2, 0,\n                            alignment=Qt.AlignLeft | Qt.AlignVCenter)\n        self.top_axis.setZValue(self.dendrogram.zValue() + 10)\n        self.bottom_axis.setZValue(self.dendrogram.zValue() + 10)\n        self.cut_line = SliderLine(self.top_axis,\n                                   orientation=Qt.Horizontal)\n        self.cut_line.valueChanged.connect(self._dendrogram_slider_changed)\n        self.dendrogram.geometryChanged.connect(self._dendrogram_geom_changed)\n        self._set_cut_line_visible(self.selection_method == 1)\n        self.__update_font_scale()\n\n    @Inputs.distances\n    def set_distances(self, matrix):\n        self.error()\n        self.Error.clear()\n\n        self.matrix = None\n        self.Error.clear()\n        if matrix is not None:\n            if len(matrix) < 2:\n                self.Error.empty_matrix()\n            elif not matrix.is_symmetric():\n                self.Error.not_symmetric()\n            elif not np.all(np.isfinite(matrix)):\n                self.Error.not_finite_distances()\n            else:\n                self.matrix = matrix\n\n    @Inputs.subset\n    def set_subset(self, subset):\n        self.subset = subset\n        self.controls.label_only_subset.setDisabled(subset is None)\n\n    def handleNewSignals(self):\n        if self.__pending_selection_restore is not None:\n            selection_state = self.__pending_selection_restore\n        else:\n            # save the current selection to (possibly) restore later\n            selection_state = self._save_selection()\n\n        matrix = self.matrix\n        if matrix is not None:\n            self._set_items(matrix.row_items, matrix.axis)\n        else:\n            self._set_items(None)\n        self._update()\n\n        # Can now attempt to restore session state from a saved workflow.\n        if self.root and selection_state is not None:\n            self._restore_selection(selection_state)\n            self.__pending_selection_restore = None\n\n        self.Warning.clear()\n        rows = set()\n        if self.subset:\n            subsetids = set(self.subset.ids)\n            if not isinstance(self.items, Orange.data.Table) \\\n                    or not self.matrix.axis:\n                self.Warning.subset_on_no_table()\n            elif (dataids := set(self.items.ids)) and not subsetids & dataids:\n                self.Warning.subset_wrong()\n            elif not subsetids <= dataids:\n                self.Warning.subset_not_subset()\n            else:\n                indices = [leaf.value.index for leaf in leaves(self.root)]\n                rows = {\n                    row for row, rowid in enumerate(self.items.ids[indices])\n                    if rowid in subsetids\n                }\n\n        self.subset_rows = rows\n        self._update_labels()\n        self.commit.now()\n\n    def _set_items(self, items, axis=1):\n        self.closeContext()\n        self.items = items\n        model = self.label_cb.model()\n        color_model = self.controls.color_by.model()\n        color_model.set_domain(None)\n        self.color_by = None\n        if len(model) == 2 and model[0] is None:\n            if model[1] == \"Name\":\n                self.annotation_if_names = self.annotation\n            if model[1] == self.basic_annotations[1]:\n                self.annotation_if_enumerate = self.annotation\n        if isinstance(items, Orange.data.Table) and axis:\n            metas_class = tuple(\n                filter_visible(chain(items.domain.metas,\n                                     items.domain.class_vars)))\n            visible_attrs = tuple(filter_visible(items.domain.attributes))\n            if not (metas_class or visible_attrs):\n                model[:] = self.basic_annotations\n            else:\n                model[:] = (\n                    (None, )\n                    + metas_class\n                    + (model.Separator, ) * bool(metas_class and visible_attrs)\n                    + visible_attrs)\n            for meta in items.domain.metas:\n                if isinstance(meta, StringVariable):\n                    self.annotation = meta\n                    break\n            else:\n                if items.domain.class_vars:\n                    # No string metas: show class\n                    self.annotation = items.domain.class_vars[0]\n                else:\n                    # No string metas and no class: show the first option\n                    # which is not None (in the worst case, Enumeration)\n                    self.annotation = model[1]\n\n            color_model.set_domain(items.domain)\n            if items.domain.class_vars:\n                self.color_by = items.domain.class_vars[0]\n            self.openContext(items.domain)\n        elif isinstance(items, Orange.data.Table) and not axis \\\n                or (isinstance(items, list) and \\\n                    all(isinstance(var, Orange.data.Variable)\n                        for var in items)):\n            model[:] = (None, \"Name\")\n            self.annotation = self.annotation_if_names\n        else:\n            model[:] = self.basic_annotations\n            self.annotation = self.annotation_if_enumerate\n\n        no_colors = len(color_model) == 1\n        self.controls.color_by.setDisabled(no_colors)\n        self.color_by_label.setDisabled(no_colors)\n\n    def _clear_plot(self):\n        self.dendrogram.set_root(None)\n        self.label_model.clear()\n\n    def _set_displayed_root(self, root):\n        self._clear_plot()\n        self._displayed_root = root\n        self.dendrogram.set_root(root)\n        self._update_labels()\n\n    def _update(self):\n        self._clear_plot()\n\n        distances = self.matrix\n\n        if distances is not None:\n            method = LINKAGE_ARGS[self.linkage]\n            Z = dist_matrix_linkage(distances, linkage=method)\n\n            tree = tree_from_linkage(Z)\n            self.linkmatrix = Z\n            self.root = tree\n\n            self.top_axis.setRange(tree.value.height, 0.0)\n            self.bottom_axis.setRange(tree.value.height, 0.0)\n\n            if self.pruning:\n                self._set_displayed_root(prune(tree, level=self.max_depth))\n            else:\n                self._set_displayed_root(tree)\n        else:\n            self.linkmatrix = None\n            self.root = None\n            self._set_displayed_root(None)\n\n        self._apply_selection()\n\n    def _update_labels(self):\n        if not hasattr(self, \"label_model\"):\n            # This method can be called during widget initialization when\n            # creating check box for label_only_subset, if it's value is\n            # initially True.\n            # See https://github.com/biolab/orange-widget-base/pull/213;\n            # if it's merged, this check can be removed.\n            return\n\n        self.Warning.pruning_disables_colors(\n            shown=self.pruning\n                  and (self.subset_rows or self.color_by is not None))\n        labels = []\n        if self.root and self._displayed_root:\n            indices = [leaf.value.index for leaf in leaves(self.root)]\n\n            if self.annotation is None:\n                if not self.pruning \\\n                        and self.subset_rows and self.color_by is None:\n                    # Model fails if number of labels and of colors mismatch\n                    labels = [\"\"] * len(indices)\n                else:\n                    labels = []\n            elif self.annotation == \"Enumeration\":\n                labels = [str(i+1) for i in indices]\n            elif self.annotation == \"Name\":\n                attr = self.matrix.row_items.domain.attributes\n                labels = [str(attr[i]) for i in indices]\n            elif isinstance(self.annotation, Orange.data.Variable):\n                col_data = self.items.get_column(self.annotation)\n                labels = [self.annotation.str_val(val) for val in col_data]\n                labels = [labels[idx] for idx in indices]\n            else:\n                labels = []\n            if not self.pruning and \\\n                    labels and self.label_only_subset and self.subset_rows:\n                labels = [label if row in self.subset_rows else \"\"\n                          for row, label in enumerate(labels)]\n\n            if labels and self._displayed_root is not self.root:\n                joined = leaves(self._displayed_root)\n                labels = [\", \".join(labels[leaf.value.first: leaf.value.last])\n                          for leaf in joined]\n\n        self.label_model[:] = labels\n        self.label_model.set_subset(set() if self.pruning else self.subset_rows)\n        self.labels.setMinimumWidth(1 if labels else -1)\n\n        if not self.pruning and self.color_by is not None:\n            col = self.items.get_column(self.color_by)\n            self.label_model.set_colors(\n                self.color_by.palette.values_to_qcolors(col[indices]))\n        else:\n            self.label_model.set_colors(None)\n\n    def _restore_selection(self, state):\n        # type: (SelectionState) -> bool\n        \"\"\"\n        Restore the (manual) node selection state.\n\n        Return True if successful; False otherwise.\n        \"\"\"\n        linkmatrix = self.linkmatrix\n        if self.selection_method == 0 and self.root:\n            selected, linksaved = state\n            linkstruct = np.array(linksaved, dtype=float)\n            selected = set(selected)  # type: Set[Tuple[int]]\n            if not selected:\n                return False\n            if linkmatrix.shape[0] != linkstruct.shape[0]:\n                return False\n            # check that the linkage matrix structure matches. Use isclose for\n            # the height column to account for inexact floating point math\n            # (e.g. summation order in different ?gemm implementations for\n            # euclidean distances, ...)\n            if np.any(linkstruct[:, :2] != linkmatrix[:, :2]) or \\\n                    not np.all(np.isclose(linkstruct[:, 2], linkstruct[:, 2])):\n                return False\n            selection = []\n            indices = np.array([n.value.index for n in leaves(self.root)],\n                               dtype=int)\n            # mapping from ranges to display (pruned) nodes\n            mapping = {node.value.range: node\n                       for node in postorder(self._displayed_root)}\n            for node in postorder(self.root):  # type: Tree\n                r = tuple(indices[node.value.first: node.value.last])\n                if r in selected:\n                    if node.value.range not in mapping:\n                        # the node was pruned from display and cannot be\n                        # selected\n                        break\n                    selection.append(mapping[node.value.range])\n                    selected.remove(r)\n                if not selected:\n                    break  # found all, nothing more to do\n            if selection and selected:\n                # Could not restore all selected nodes (only partial match)\n                return False\n\n            self._set_selected_nodes(selection)\n            return True\n        return False\n\n    def _set_selected_nodes(self, selection):\n        # type: (List[Tree]) -> None\n        \"\"\"\n        Set the nodes in `selection` to be the current selected nodes.\n\n        The selection nodes must be subtrees of the current `_displayed_root`.\n        \"\"\"\n        self.dendrogram.selectionChanged.disconnect(self._invalidate_output)\n        try:\n            self.dendrogram.set_selected_clusters(selection)\n        finally:\n            self.dendrogram.selectionChanged.connect(self._invalidate_output)\n\n    def _max_depth_return(self):\n        if self.pruning != 1:\n            self.pruning = 1\n            self._invalidate_pruning()\n\n    def _max_depth_changed(self):\n        self.pruning = 1\n        self._invalidate_pruning()\n\n    def _invalidate_clustering(self):\n        self._update()\n        self._update_labels()\n        self._invalidate_output()\n\n    def _invalidate_output(self):\n        self.commit.deferred()\n\n    def _invalidate_pruning(self):\n        if self.root:\n            selection = self.dendrogram.selected_nodes()\n            ranges = [node.value.range for node in selection]\n            if self.pruning:\n                self._set_displayed_root(\n                    prune(self.root, level=self.max_depth))\n            else:\n                self._set_displayed_root(self.root)\n            selected = [node for node in preorder(self._displayed_root)\n                        if node.value.range in ranges]\n\n            self.dendrogram.set_selected_clusters(selected)\n\n        self._apply_selection()\n\n    @gui.deferred\n    def commit(self):\n        items = getattr(self.matrix, \"items\", self.items)\n        if not items:\n            self.Outputs.selected_data.send(None)\n            self.Outputs.annotated_data.send(None)\n            return\n\n        selection = self.dendrogram.selected_nodes()\n        selection = sorted(selection, key=lambda c: c.value.first)\n\n        indices = [leaf.value.index for leaf in leaves(self.root)]\n\n        maps = [indices[node.value.first:node.value.last]\n                for node in selection]\n\n        selected_indices = list(chain(*maps))\n\n        if not selected_indices:\n            self.Outputs.selected_data.send(None)\n            annotated_data = lazy_annotated_table(items, []) \\\n                if self.selection_method == 0 and self.matrix.axis else None\n            self.Outputs.annotated_data.send(annotated_data)\n            return\n\n        selected_data = annotated_data = None\n\n        if isinstance(items, Orange.data.Table) and self.matrix.axis == 1:\n            # Select rows\n            data, domain = items, items.domain\n\n            c = np.full(self.matrix.shape[0], len(maps))\n            for i, indices in enumerate(maps):\n                c[indices] = i\n\n            clust_name = get_unique_names(domain, \"Cluster\")\n            values = [f\"C{i + 1}\" for i in range(len(maps))]\n\n            sel_clust_var = Orange.data.DiscreteVariable(\n                name=clust_name, values=values)\n            sel_domain = add_columns(domain, metas=(sel_clust_var,))\n            selected_data = LazyValue[Table](\n                lambda: items.add_column(\n                    sel_clust_var, c, to_metas=True)[c != len(maps)],\n                domain=sel_domain, length=len(selected_indices))\n\n            ann_clust_var = Orange.data.DiscreteVariable(\n                name=clust_name, values=values + [\"Other\"]\n            )\n            ann_domain = add_columns(\n                domain_with_annotation_column(data)[0], metas=(ann_clust_var, ))\n            annotated_data = LazyValue[Table](\n                lambda: create_annotated_table(\n                    data=items.add_column(ann_clust_var, c, to_metas=True),\n                    selected_indices=selected_indices),\n                domain=ann_domain, length=len(items)\n            )\n\n        elif isinstance(items, Orange.data.Table) and self.matrix.axis == 0:\n            # Select columns\n            attrs = []\n            unselected_indices = sorted(set(range(self.root.value.last)) -\n                                        set(selected_indices))\n            for clust, indices in chain(enumerate(maps, start=1),\n                                        [(0, unselected_indices)]):\n                for i in indices:\n                    attr = items.domain[i].copy()\n                    attr.attributes[\"cluster\"] = clust\n                    attrs.append(attr)\n            all_domain = Orange.data.Domain(\n                # len(unselected_indices) can be 0\n                attrs[:len(attrs) - len(unselected_indices)],\n                items.domain.class_vars, items.domain.metas)\n\n            selected_data = LazyValue[Table](\n                lambda: items.from_table(all_domain, items),\n                domain=all_domain, length=len(items))\n\n            sel_domain = Orange.data.Domain(\n                attrs,\n                items.domain.class_vars, items.domain.metas)\n            annotated_data = LazyValue[Table](\n                lambda: items.from_table(sel_domain, items),\n                domain=sel_domain, length=len(items))\n\n        self.Outputs.selected_data.send(selected_data)\n        self.Outputs.annotated_data.send(annotated_data)\n\n    @Slot(QPointF)\n    def _activate_cut_line(self, pos: QPointF):\n        \"\"\"Activate cut line selection an set cut value to `pos.x()`.\"\"\"\n        self.selection_method = 1\n        self.cut_line.setValue(pos.x())\n        self._selection_method_changed()\n\n    def onDeleteWidget(self):\n        super().onDeleteWidget()\n        self._clear_plot()\n        self.dendrogram.clear()\n        self.dendrogram.deleteLater()\n\n    def _dendrogram_geom_changed(self):\n        pos = self.dendrogram.pos_at_height(self.cutoff_height)\n        geom = self.dendrogram.geometry()\n        self._set_slider_value(pos.x(), geom.width())\n\n        self.cut_line.setLength(\n            self.bottom_axis.geometry().bottom()\n            - self.top_axis.geometry().top()\n        )\n\n        geom = self._main_graphics.geometry()\n        assert geom.topLeft() == QPointF(0, 0)\n\n        def adjustLeft(rect):\n            rect = QRectF(rect)\n            rect.setLeft(geom.left())\n            return rect\n        margin = 3\n        self.scene.setSceneRect(geom)\n        self.view.setSceneRect(geom)\n        self.view.setHeaderSceneRect(\n            adjustLeft(self.top_axis.geometry()).adjusted(0, 0, 0, margin)\n        )\n        self.view.setFooterSceneRect(\n            adjustLeft(self.bottom_axis.geometry()).adjusted(0, -margin, 0, 0)\n        )\n\n    def _dendrogram_slider_changed(self, value):\n        p = QPointF(value, 0)\n        cl_height = self.dendrogram.height_at(p)\n\n        self.set_cutoff_height(cl_height)\n\n    def _set_slider_value(self, value, span):\n        with blocked(self.cut_line):\n            self.cut_line.setRange(0, span)\n            self.cut_line.setValue(value)\n\n    def set_cutoff_height(self, height):\n        self.cutoff_height = height\n        if self.root:\n            self.cut_ratio = 100 * height / self.root.value.height\n        self.select_max_height(height)\n\n    def _set_cut_line_visible(self, visible):\n        self.cut_line.setVisible(visible)\n\n    def select_top_n(self, n):\n        root = self._displayed_root\n        if root:\n            clusters = top_clusters(root, n)\n            self.dendrogram.set_selected_clusters(clusters)\n\n    def select_max_height(self, height):\n        root = self._displayed_root\n        if root:\n            clusters = clusters_at_height(root, height)\n            self.dendrogram.set_selected_clusters(clusters)\n\n    def _cut_ratio_changed(self):\n        self.selection_method = 1\n        self._selection_method_changed()\n\n    def _cut_ratio_return(self):\n        if self.selection_method != 1:\n            self.selection_method = 1\n            self._selection_method_changed()\n\n    def _top_n_changed(self):\n        self.selection_method = 2\n        self._selection_method_changed()\n\n    def _top_n_return(self):\n        if self.selection_method != 2:\n            self.selection_method = 2\n            self._selection_method_changed()\n\n    def _selection_method_changed(self):\n        self._set_cut_line_visible(self.selection_method == 1)\n        if self.root:\n            self._apply_selection()\n\n    def _apply_selection(self):\n        if not self.root:\n            return\n\n        if self.selection_method == 0:\n            pass\n        elif self.selection_method == 1:\n            height = self.cut_ratio * self.root.value.height / 100\n            self.set_cutoff_height(height)\n            pos = self.dendrogram.pos_at_height(height)\n            self._set_slider_value(pos.x(), self.dendrogram.size().width())\n        elif self.selection_method == 2:\n            self.select_top_n(self.top_n)\n\n    def _selection_edited(self):\n        # Selection was edited by clicking on a cluster in the\n        # dendrogram view.\n        self.selection_method = 0\n        self._selection_method_changed()\n        self._invalidate_output()\n\n    def _save_selection(self):\n        # Save the current manual node selection state\n        selection_state = None\n        if self.selection_method == 0 and self.root:\n            assert self.linkmatrix is not None\n            linkmat = [(int(_0), int(_1), _2)\n                       for _0, _1, _2 in self.linkmatrix[:, :3].tolist()]\n            nodes_ = self.dendrogram.selected_nodes()\n            # match the display (pruned) nodes back (by ranges)\n            mapping = {node.value.range: node for node in postorder(self.root)}\n            nodes = [mapping[node.value.range] for node in nodes_]\n            indices = [tuple(node.value.index for node in leaves(node))\n                       for node in nodes]\n            if nodes:\n                selection_state = (indices, linkmat)\n        return selection_state\n\n    def save_state(self):\n        # type: () -> Dict[str, Any]\n        \"\"\"\n        Save state for `set_restore_state`\n        \"\"\"\n        selection = self._save_selection()\n        res = {\"version\": (0, 0, 0)}\n        if selection is not None:\n            res[\"selection_state\"] = selection\n        return res\n\n    def set_restore_state(self, state):\n        # type: (Dict[str, Any]) -> bool\n        \"\"\"\n        Restore session data from a saved state.\n\n        Parameters\n        ----------\n        state : Dict[str, Any]\n\n        NOTE\n        ----\n        This is method called while the instance (self) is being constructed,\n        even before its `__init__` is called. Consider `self` to be only a\n        `QObject` at this stage.\n        \"\"\"\n        if \"selection_state\" in state:\n            selection = state[\"selection_state\"]\n            self.__pending_selection_restore = selection\n        return True\n\n    def __zoom_in(self):\n        def clip(minval, maxval, val):\n            return min(max(val, minval), maxval)\n        self.zoom_factor = clip(self.zoom_slider.minimum(),\n                                self.zoom_slider.maximum(),\n                                self.zoom_factor + 1)\n        self.__update_font_scale()\n\n    def __zoom_out(self):\n        def clip(minval, maxval, val):\n            return min(max(val, minval), maxval)\n        self.zoom_factor = clip(self.zoom_slider.minimum(),\n                                self.zoom_slider.maximum(),\n                                self.zoom_factor - 1)\n        self.__update_font_scale()\n\n    def __zoom_reset(self):\n        self.zoom_factor = 0\n        self.__update_font_scale()\n\n    def __update_font_scale(self):\n        font = self.scene.font()\n        factor = (1.25 ** self.zoom_factor)\n        font = qfont_scaled(font, factor)\n        self._main_graphics.setFont(font)\n        self.label_model.setFont(font)\n\n    def send_report(self):\n        annot = self.label_cb.currentText()\n        if isinstance(self.annotation, str):\n            annot = annot.lower()\n        if self.selection_method == 0:\n            sel = \"manual\"\n        elif self.selection_method == 1:\n            sel = \"at {:.1f} of height\".format(self.cut_ratio)\n        else:\n            sel = f\"top {self.top_n} {pl(self.top_n, 'cluster')}\"\n        self.report_items((\n            (\"Linkage\", LINKAGE[self.linkage]),\n            (\"Annotation\", annot),\n            (\"Pruning\",\n             self.pruning != 0 and \"{} levels\".format(self.max_depth)),\n            (\"Selection\", sel),\n        ))\n        self.report_plot()\n\n    @classmethod\n    def migrate_context(cls, context, version):\n        if version < 2:\n            if context.values[\"annotation\"] == \"None\":\n                context.values[\"annotation\"] = None",
                            "ast_type_str": "ClassDef",
                            "start_lineno": 177,
                            "end_lineno": 1068,
                            "parent": null
                        }
                    ],
                    "added_entities": [],
                    "deleted_entities": []
                },
                {
                    "descriptor": {
                        "old_range": {
                            "start": 766,
                            "length": 6
                        },
                        "new_range": {
                            "start": 769,
                            "length": 7
                        },
                        "section": "class OWHierarchicalClustering(widget.OWWidget):"
                    },
                    "line_group": {
                        "all_lines": [
                            {
                                "content": "    @gui.deferred",
                                "type": "context"
                            },
                            {
                                "content": "    def commit(self):",
                                "type": "context"
                            },
                            {
                                "content": "        items = getattr(self.matrix, \"items\", self.items)",
                                "type": "context"
                            },
                            {
                                "content": "        self.Warning.many_clusters.clear()",
                                "type": "added"
                            },
                            {
                                "content": "        if not items:",
                                "type": "context"
                            },
                            {
                                "content": "            self.Outputs.selected_data.send(None)",
                                "type": "context"
                            },
                            {
                                "content": "            self.Outputs.annotated_data.send(None)",
                                "type": "context"
                            }
                        ]
                    },
                    "modified_entities": [
                        {
                            "file_name": "Orange/widgets/unsupervised/owhierarchicalclustering.py",
                            "type": "class",
                            "name": "OWHierarchicalClustering",
                            "content": "class OWHierarchicalClustering(widget.OWWidget):\n    name = \"Hierarchical Clustering\"\n    description = \"Display a dendrogram of a hierarchical clustering \" \\\n                  \"constructed from the input distance matrix.\"\n    icon = \"icons/HierarchicalClustering.svg\"\n    priority = 2100\n    keywords = \"hierarchical clustering\"\n\n    class Inputs:\n        distances = Input(\"Distances\", Orange.misc.DistMatrix)\n        subset = Input(\"Data Subset\", Orange.data.Table, explicit=True)\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    settings_version = 2\n    settingsHandler = _DomainContextHandler()\n\n    #: Selected linkage\n    linkage = settings.Setting(LINKAGE.index(DEFAULT_LINKAGE))\n    #: Index of the selected annotation item (variable, ...)\n    annotation = settings.ContextSetting(\"Enumeration\")\n    #: Out-of-context setting for the case when the \"Name\" option is available\n    annotation_if_names = settings.Setting(\"Name\")\n    #: Out-of-context setting for the case with just \"Enumeration\" and \"None\"\n    annotation_if_enumerate = settings.Setting(\"Enumeration\")\n    #: Selected tree pruning (none/max depth)\n    pruning = settings.Setting(0)\n    #: Maximum depth when max depth pruning is selected\n    max_depth = settings.Setting(10)\n\n    #: Selected cluster selection method (none, cut distance, top n)\n    selection_method = settings.Setting(0)\n    #: Cut height ratio wrt root height\n    cut_ratio = settings.Setting(75.0)\n    #: Number of top clusters to select\n    top_n = settings.Setting(3)\n    #: Dendrogram zoom factor\n    zoom_factor = settings.Setting(0)\n    #: Show labels only for subset (if present)\n    label_only_subset = settings.Setting(False)\n    #: Color for label decoration\n    color_by: Union[DiscreteVariable, ContinuousVariable, None] = \\\n        settings.ContextSetting(None)\n\n    autocommit = settings.Setting(True)\n\n    graph_name = \"scene\"  # QGraphicsScene\n\n    basic_annotations = [None, \"Enumeration\"]\n\n    class Error(widget.OWWidget.Error):\n        empty_matrix = Msg(\"Distance matrix is empty.\")\n        not_finite_distances = Msg(\"Some distances are infinite\")\n        not_symmetric = widget.Msg(\"Distance matrix is not symmetric.\")\n\n    class Warning(widget.OWWidget.Warning):\n        subset_on_no_table = \\\n            Msg(\"Unused data subset: distances do not refer to data instances\")\n        subset_not_subset = \\\n            Msg(\"Some data from the subset does not appear in distance matrix\")\n        subset_wrong = \\\n            Msg(\"Subset data refers to a different table\")\n        pruning_disables_colors = \\\n            Msg(\"Pruned cluster doesn't show colors and indicate subset\")\n        many_clusters = \\\n            Msg(\"Variables with too many values may \"\n                \"degrade the performance of downstream widgets.\")\n\n    #: Stored (manual) selection state (from a saved workflow) to restore.\n    __pending_selection_restore = None  # type: Optional[SelectionState]\n\n    def __init__(self):\n        super().__init__()\n\n        self.matrix = None\n        self.items = None\n        self.subset = None\n        self.subset_rows = set()\n        self.linkmatrix = None\n        self.root = None\n        self._displayed_root = None\n        self.cutoff_height = 0.0\n\n        spin_width = QFontMetrics(self.font()).horizontalAdvance(\"M\" * 7)\n        gui.comboBox(\n            self.controlArea, self, \"linkage\", items=LINKAGE, box=\"Linkage\",\n            callback=self._invalidate_clustering)\n\n        model = itemmodels.VariableListModel(placeholder=\"None\")\n        model[:] = self.basic_annotations\n\n        grid = QGridLayout()\n        gui.widgetBox(self.controlArea, \"Annotations\", orientation=grid)\n        self.label_cb = cb = combobox.ComboBoxSearch(\n            minimumContentsLength=14,\n            sizeAdjustPolicy=QComboBox.AdjustToMinimumContentsLengthWithIcon\n        )\n        cb.setModel(model)\n        cb.setCurrentIndex(cb.findData(self.annotation, Qt.EditRole))\n\n        def on_annotation_activated():\n            self.annotation = self.label_cb.currentData(Qt.EditRole)\n            self._update_labels()\n        cb.activated.connect(on_annotation_activated)\n\n        def on_annotation_changed(value):\n            self.label_cb.setCurrentIndex(\n                self.label_cb.findData(value, Qt.EditRole))\n        self.connect_control(\"annotation\", on_annotation_changed)\n\n        grid.addWidget(self.label_cb, 0, 0, 1, 2)\n\n        cb = gui.checkBox(\n            None, self, \"label_only_subset\", \"Show labels only for subset\",\n            disabled=True,\n            callback=self._update_labels, stateWhenDisabled=False)\n        grid.addWidget(cb, 1, 0, 1, 2)\n\n        model = itemmodels.DomainModel(\n            valid_types=(DiscreteVariable, ContinuousVariable),\n            placeholder=\"None\")\n        cb = gui.comboBox(\n            None, self, \"color_by\", orientation=Qt.Horizontal,\n            model=model, callback=self._update_labels,\n            sizePolicy=QSizePolicy(QSizePolicy.MinimumExpanding,\n                                   QSizePolicy.Fixed))\n        self.color_by_label = QLabel(\"Color by:\")\n        grid.addWidget(self.color_by_label, 2, 0)\n        grid.addWidget(cb, 2, 1)\n\n        box = gui.radioButtons(\n            self.controlArea, self, \"pruning\", box=\"Pruning\",\n            callback=self._invalidate_pruning)\n        grid = QGridLayout()\n        box.layout().addLayout(grid)\n        grid.addWidget(\n            gui.appendRadioButton(box, \"None\", addToLayout=False),\n            0, 0\n        )\n        self.max_depth_spin = gui.spin(\n            box, self, \"max_depth\", minv=1, maxv=100,\n            controlWidth=spin_width, alignment=Qt.AlignRight,\n            callback=self._max_depth_changed,\n            keyboardTracking=False, addToLayout=False\n        )\n        self.max_depth_spin.lineEdit().returnPressed.connect(\n            self._max_depth_return)\n\n        grid.addWidget(\n            gui.appendRadioButton(box, \"Max depth:\", addToLayout=False),\n            1, 0)\n        grid.addWidget(self.max_depth_spin, 1, 1)\n\n        self.selection_box = gui.radioButtons(\n            self.controlArea, self, \"selection_method\",\n            box=\"Selection\",\n            callback=self._selection_method_changed)\n\n        grid = QGridLayout()\n        self.selection_box.layout().addLayout(grid)\n        grid.addWidget(\n            gui.appendRadioButton(\n                self.selection_box, \"Manual\", addToLayout=False),\n            0, 0\n        )\n        grid.addWidget(\n            gui.appendRadioButton(\n                self.selection_box, \"Height ratio:\", addToLayout=False),\n            1, 0\n        )\n        self.cut_ratio_spin = gui.spin(\n            self.selection_box, self, \"cut_ratio\", 0, 100, step=1e-1,\n            controlWidth=spin_width, alignment = Qt.AlignRight,\n            spinType=float, callback=self._cut_ratio_changed,\n            addToLayout=False\n        )\n        self.cut_ratio_spin.setSuffix(\" %\")\n        self.cut_ratio_spin.lineEdit().returnPressed.connect(\n            self._cut_ratio_return)\n\n        grid.addWidget(self.cut_ratio_spin, 1, 1)\n\n        grid.addWidget(\n            gui.appendRadioButton(\n                self.selection_box, \"Top N:\", addToLayout=False),\n            2, 0\n        )\n        self.top_n_spin = gui.spin(\n            self.selection_box, self, \"top_n\", 1, 1000,\n            controlWidth=spin_width, alignment=Qt.AlignRight,\n            callback=self._top_n_changed, addToLayout=False)\n        self.top_n_spin.lineEdit().returnPressed.connect(self._top_n_return)\n        grid.addWidget(self.top_n_spin, 2, 1)\n\n        self.zoom_slider = gui.hSlider(\n            self.controlArea, self, \"zoom_factor\", box=\"Zoom\",\n            minValue=-6, maxValue=3, step=1, ticks=True, createLabel=False,\n            callback=self.__update_font_scale)\n\n        zoom_in = QAction(\n            \"Zoom in\", self, shortcut=QKeySequence.ZoomIn,\n            triggered=self.__zoom_in\n        )\n        zoom_out = QAction(\n            \"Zoom out\", self, shortcut=QKeySequence.ZoomOut,\n            triggered=self.__zoom_out\n        )\n        zoom_reset = QAction(\n            \"Reset zoom\", self,\n            shortcut=QKeySequence(Qt.ControlModifier | Qt.Key_0),\n            triggered=self.__zoom_reset\n        )\n        self.addActions([zoom_in, zoom_out, zoom_reset])\n\n        self.controlArea.layout().addStretch()\n\n        gui.auto_send(self.buttonsArea, self, \"autocommit\")\n\n        self.scene = QGraphicsScene(self)\n        self.view = GraphicsView(\n            self.scene,\n            horizontalScrollBarPolicy=Qt.ScrollBarAlwaysOff,\n            verticalScrollBarPolicy=Qt.ScrollBarAlwaysOn,\n            alignment=Qt.AlignLeft | Qt.AlignVCenter,\n            widgetResizable=True,\n        )\n        # Disable conflicting action shortcuts. We define our own.\n        for a in self.view.viewActions():\n            a.setEnabled(False)\n        self.mainArea.layout().setSpacing(1)\n        self.mainArea.layout().addWidget(self.view)\n\n        def axis_view(orientation):\n            ax = AxisItem(orientation=orientation, maxTickLength=7)\n            ax.mousePressed.connect(self._activate_cut_line)\n            ax.mouseMoved.connect(self._activate_cut_line)\n            ax.mouseReleased.connect(self._activate_cut_line)\n            ax.setRange(1.0, 0.0)\n            return ax\n\n        self.top_axis = axis_view(\"top\")\n        self.bottom_axis = axis_view(\"bottom\")\n\n        self._main_graphics = QGraphicsWidget(\n            sizePolicy=QSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.Preferred)\n        )\n        scenelayout = QGraphicsGridLayout()\n        scenelayout.setHorizontalSpacing(10)\n        scenelayout.setVerticalSpacing(10)\n\n        self._main_graphics.setLayout(scenelayout)\n        self.scene.addItem(self._main_graphics)\n        self.view.setCentralWidget(self._main_graphics)\n        self.scene.addItem(self._main_graphics)\n\n        self.dendrogram = DendrogramWidget(pen_width=2)\n        self.dendrogram.setSizePolicy(QSizePolicy.MinimumExpanding,\n                                      QSizePolicy.MinimumExpanding)\n        self.dendrogram.selectionChanged.connect(self._invalidate_output)\n        self.dendrogram.selectionEdited.connect(self._selection_edited)\n\n        self.labels = TextListView()\n        self.label_model = SelectedLabelsModel()\n        self.labels.setModel(self.label_model)\n        self.labels.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Preferred)\n        self.labels.setAlignment(Qt.AlignLeft)\n        self.labels.setMaximumWidth(200)\n\n        scenelayout.addItem(self.top_axis, 0, 0,\n                            alignment=Qt.AlignLeft | Qt.AlignVCenter)\n        scenelayout.addItem(self.dendrogram, 1, 0,\n                            alignment=Qt.AlignLeft | Qt.AlignVCenter)\n        scenelayout.addItem(self.labels, 1, 1,\n                            alignment=Qt.AlignLeft | Qt.AlignVCenter)\n        scenelayout.addItem(self.bottom_axis, 2, 0,\n                            alignment=Qt.AlignLeft | Qt.AlignVCenter)\n        self.top_axis.setZValue(self.dendrogram.zValue() + 10)\n        self.bottom_axis.setZValue(self.dendrogram.zValue() + 10)\n        self.cut_line = SliderLine(self.top_axis,\n                                   orientation=Qt.Horizontal)\n        self.cut_line.valueChanged.connect(self._dendrogram_slider_changed)\n        self.dendrogram.geometryChanged.connect(self._dendrogram_geom_changed)\n        self._set_cut_line_visible(self.selection_method == 1)\n        self.__update_font_scale()\n\n    @Inputs.distances\n    def set_distances(self, matrix):\n        self.error()\n        self.Error.clear()\n\n        self.matrix = None\n        self.Error.clear()\n        if matrix is not None:\n            if len(matrix) < 2:\n                self.Error.empty_matrix()\n            elif not matrix.is_symmetric():\n                self.Error.not_symmetric()\n            elif not np.all(np.isfinite(matrix)):\n                self.Error.not_finite_distances()\n            else:\n                self.matrix = matrix\n\n    @Inputs.subset\n    def set_subset(self, subset):\n        self.subset = subset\n        self.controls.label_only_subset.setDisabled(subset is None)\n\n    def handleNewSignals(self):\n        if self.__pending_selection_restore is not None:\n            selection_state = self.__pending_selection_restore\n        else:\n            # save the current selection to (possibly) restore later\n            selection_state = self._save_selection()\n\n        matrix = self.matrix\n        if matrix is not None:\n            self._set_items(matrix.row_items, matrix.axis)\n        else:\n            self._set_items(None)\n        self._update()\n\n        # Can now attempt to restore session state from a saved workflow.\n        if self.root and selection_state is not None:\n            self._restore_selection(selection_state)\n            self.__pending_selection_restore = None\n\n        self.Warning.clear()\n        rows = set()\n        if self.subset:\n            subsetids = set(self.subset.ids)\n            if not isinstance(self.items, Orange.data.Table) \\\n                    or not self.matrix.axis:\n                self.Warning.subset_on_no_table()\n            elif (dataids := set(self.items.ids)) and not subsetids & dataids:\n                self.Warning.subset_wrong()\n            elif not subsetids <= dataids:\n                self.Warning.subset_not_subset()\n            else:\n                indices = [leaf.value.index for leaf in leaves(self.root)]\n                rows = {\n                    row for row, rowid in enumerate(self.items.ids[indices])\n                    if rowid in subsetids\n                }\n\n        self.subset_rows = rows\n        self._update_labels()\n        self.commit.now()\n\n    def _set_items(self, items, axis=1):\n        self.closeContext()\n        self.items = items\n        model = self.label_cb.model()\n        color_model = self.controls.color_by.model()\n        color_model.set_domain(None)\n        self.color_by = None\n        if len(model) == 2 and model[0] is None:\n            if model[1] == \"Name\":\n                self.annotation_if_names = self.annotation\n            if model[1] == self.basic_annotations[1]:\n                self.annotation_if_enumerate = self.annotation\n        if isinstance(items, Orange.data.Table) and axis:\n            metas_class = tuple(\n                filter_visible(chain(items.domain.metas,\n                                     items.domain.class_vars)))\n            visible_attrs = tuple(filter_visible(items.domain.attributes))\n            if not (metas_class or visible_attrs):\n                model[:] = self.basic_annotations\n            else:\n                model[:] = (\n                    (None, )\n                    + metas_class\n                    + (model.Separator, ) * bool(metas_class and visible_attrs)\n                    + visible_attrs)\n            for meta in items.domain.metas:\n                if isinstance(meta, StringVariable):\n                    self.annotation = meta\n                    break\n            else:\n                if items.domain.class_vars:\n                    # No string metas: show class\n                    self.annotation = items.domain.class_vars[0]\n                else:\n                    # No string metas and no class: show the first option\n                    # which is not None (in the worst case, Enumeration)\n                    self.annotation = model[1]\n\n            color_model.set_domain(items.domain)\n            if items.domain.class_vars:\n                self.color_by = items.domain.class_vars[0]\n            self.openContext(items.domain)\n        elif isinstance(items, Orange.data.Table) and not axis \\\n                or (isinstance(items, list) and \\\n                    all(isinstance(var, Orange.data.Variable)\n                        for var in items)):\n            model[:] = (None, \"Name\")\n            self.annotation = self.annotation_if_names\n        else:\n            model[:] = self.basic_annotations\n            self.annotation = self.annotation_if_enumerate\n\n        no_colors = len(color_model) == 1\n        self.controls.color_by.setDisabled(no_colors)\n        self.color_by_label.setDisabled(no_colors)\n\n    def _clear_plot(self):\n        self.dendrogram.set_root(None)\n        self.label_model.clear()\n\n    def _set_displayed_root(self, root):\n        self._clear_plot()\n        self._displayed_root = root\n        self.dendrogram.set_root(root)\n        self._update_labels()\n\n    def _update(self):\n        self._clear_plot()\n\n        distances = self.matrix\n\n        if distances is not None:\n            method = LINKAGE_ARGS[self.linkage]\n            Z = dist_matrix_linkage(distances, linkage=method)\n\n            tree = tree_from_linkage(Z)\n            self.linkmatrix = Z\n            self.root = tree\n\n            self.top_axis.setRange(tree.value.height, 0.0)\n            self.bottom_axis.setRange(tree.value.height, 0.0)\n\n            if self.pruning:\n                self._set_displayed_root(prune(tree, level=self.max_depth))\n            else:\n                self._set_displayed_root(tree)\n        else:\n            self.linkmatrix = None\n            self.root = None\n            self._set_displayed_root(None)\n\n        self._apply_selection()\n\n    def _update_labels(self):\n        if not hasattr(self, \"label_model\"):\n            # This method can be called during widget initialization when\n            # creating check box for label_only_subset, if it's value is\n            # initially True.\n            # See https://github.com/biolab/orange-widget-base/pull/213;\n            # if it's merged, this check can be removed.\n            return\n\n        self.Warning.pruning_disables_colors(\n            shown=self.pruning\n                  and (self.subset_rows or self.color_by is not None))\n        labels = []\n        if self.root and self._displayed_root:\n            indices = [leaf.value.index for leaf in leaves(self.root)]\n\n            if self.annotation is None:\n                if not self.pruning \\\n                        and self.subset_rows and self.color_by is None:\n                    # Model fails if number of labels and of colors mismatch\n                    labels = [\"\"] * len(indices)\n                else:\n                    labels = []\n            elif self.annotation == \"Enumeration\":\n                labels = [str(i+1) for i in indices]\n            elif self.annotation == \"Name\":\n                attr = self.matrix.row_items.domain.attributes\n                labels = [str(attr[i]) for i in indices]\n            elif isinstance(self.annotation, Orange.data.Variable):\n                col_data = self.items.get_column(self.annotation)\n                labels = [self.annotation.str_val(val) for val in col_data]\n                labels = [labels[idx] for idx in indices]\n            else:\n                labels = []\n            if not self.pruning and \\\n                    labels and self.label_only_subset and self.subset_rows:\n                labels = [label if row in self.subset_rows else \"\"\n                          for row, label in enumerate(labels)]\n\n            if labels and self._displayed_root is not self.root:\n                joined = leaves(self._displayed_root)\n                labels = [\", \".join(labels[leaf.value.first: leaf.value.last])\n                          for leaf in joined]\n\n        self.label_model[:] = labels\n        self.label_model.set_subset(set() if self.pruning else self.subset_rows)\n        self.labels.setMinimumWidth(1 if labels else -1)\n\n        if not self.pruning and self.color_by is not None:\n            col = self.items.get_column(self.color_by)\n            self.label_model.set_colors(\n                self.color_by.palette.values_to_qcolors(col[indices]))\n        else:\n            self.label_model.set_colors(None)\n\n    def _restore_selection(self, state):\n        # type: (SelectionState) -> bool\n        \"\"\"\n        Restore the (manual) node selection state.\n\n        Return True if successful; False otherwise.\n        \"\"\"\n        linkmatrix = self.linkmatrix\n        if self.selection_method == 0 and self.root:\n            selected, linksaved = state\n            linkstruct = np.array(linksaved, dtype=float)\n            selected = set(selected)  # type: Set[Tuple[int]]\n            if not selected:\n                return False\n            if linkmatrix.shape[0] != linkstruct.shape[0]:\n                return False\n            # check that the linkage matrix structure matches. Use isclose for\n            # the height column to account for inexact floating point math\n            # (e.g. summation order in different ?gemm implementations for\n            # euclidean distances, ...)\n            if np.any(linkstruct[:, :2] != linkmatrix[:, :2]) or \\\n                    not np.all(np.isclose(linkstruct[:, 2], linkstruct[:, 2])):\n                return False\n            selection = []\n            indices = np.array([n.value.index for n in leaves(self.root)],\n                               dtype=int)\n            # mapping from ranges to display (pruned) nodes\n            mapping = {node.value.range: node\n                       for node in postorder(self._displayed_root)}\n            for node in postorder(self.root):  # type: Tree\n                r = tuple(indices[node.value.first: node.value.last])\n                if r in selected:\n                    if node.value.range not in mapping:\n                        # the node was pruned from display and cannot be\n                        # selected\n                        break\n                    selection.append(mapping[node.value.range])\n                    selected.remove(r)\n                if not selected:\n                    break  # found all, nothing more to do\n            if selection and selected:\n                # Could not restore all selected nodes (only partial match)\n                return False\n\n            self._set_selected_nodes(selection)\n            return True\n        return False\n\n    def _set_selected_nodes(self, selection):\n        # type: (List[Tree]) -> None\n        \"\"\"\n        Set the nodes in `selection` to be the current selected nodes.\n\n        The selection nodes must be subtrees of the current `_displayed_root`.\n        \"\"\"\n        self.dendrogram.selectionChanged.disconnect(self._invalidate_output)\n        try:\n            self.dendrogram.set_selected_clusters(selection)\n        finally:\n            self.dendrogram.selectionChanged.connect(self._invalidate_output)\n\n    def _max_depth_return(self):\n        if self.pruning != 1:\n            self.pruning = 1\n            self._invalidate_pruning()\n\n    def _max_depth_changed(self):\n        self.pruning = 1\n        self._invalidate_pruning()\n\n    def _invalidate_clustering(self):\n        self._update()\n        self._update_labels()\n        self._invalidate_output()\n\n    def _invalidate_output(self):\n        self.commit.deferred()\n\n    def _invalidate_pruning(self):\n        if self.root:\n            selection = self.dendrogram.selected_nodes()\n            ranges = [node.value.range for node in selection]\n            if self.pruning:\n                self._set_displayed_root(\n                    prune(self.root, level=self.max_depth))\n            else:\n                self._set_displayed_root(self.root)\n            selected = [node for node in preorder(self._displayed_root)\n                        if node.value.range in ranges]\n\n            self.dendrogram.set_selected_clusters(selected)\n\n        self._apply_selection()\n\n    @gui.deferred\n    def commit(self):\n        items = getattr(self.matrix, \"items\", self.items)\n        self.Warning.many_clusters.clear()\n        if not items:\n            self.Outputs.selected_data.send(None)\n            self.Outputs.annotated_data.send(None)\n            return\n\n        selection = self.dendrogram.selected_nodes()\n        selection = sorted(selection, key=lambda c: c.value.first)\n\n        indices = [leaf.value.index for leaf in leaves(self.root)]\n\n        maps = [indices[node.value.first:node.value.last]\n                for node in selection]\n        if len(maps) > 20:\n            self.Warning.many_clusters()\n\n        selected_indices = list(chain(*maps))\n\n        if not selected_indices:\n            self.Outputs.selected_data.send(None)\n            annotated_data = lazy_annotated_table(items, []) \\\n                if self.selection_method == 0 and self.matrix.axis else None\n            self.Outputs.annotated_data.send(annotated_data)\n            return\n\n        selected_data = annotated_data = None\n\n        if isinstance(items, Orange.data.Table) and self.matrix.axis == 1:\n            # Select rows\n            data, domain = items, items.domain\n\n            c = np.full(self.matrix.shape[0], len(maps))\n            for i, indices in enumerate(maps):\n                c[indices] = i\n\n            clust_name = get_unique_names(domain, \"Cluster\")\n            values = [f\"C{i + 1}\" for i in range(len(maps))]\n\n            sel_clust_var = Orange.data.DiscreteVariable(\n                name=clust_name, values=values)\n            sel_domain = add_columns(domain, metas=(sel_clust_var,))\n            selected_data = LazyValue[Table](\n                lambda: items.add_column(\n                    sel_clust_var, c, to_metas=True)[c != len(maps)],\n                domain=sel_domain, length=len(selected_indices))\n\n            ann_clust_var = Orange.data.DiscreteVariable(\n                name=clust_name, values=values + [\"Other\"]\n            )\n            ann_domain = add_columns(\n                domain_with_annotation_column(data)[0], metas=(ann_clust_var, ))\n            annotated_data = LazyValue[Table](\n                lambda: create_annotated_table(\n                    data=items.add_column(ann_clust_var, c, to_metas=True),\n                    selected_indices=selected_indices),\n                domain=ann_domain, length=len(items)\n            )\n\n        elif isinstance(items, Orange.data.Table) and self.matrix.axis == 0:\n            # Select columns\n            attrs = []\n            unselected_indices = sorted(set(range(self.root.value.last)) -\n                                        set(selected_indices))\n            for clust, indices in chain(enumerate(maps, start=1),\n                                        [(0, unselected_indices)]):\n                for i in indices:\n                    attr = items.domain[i].copy()\n                    attr.attributes[\"cluster\"] = clust\n                    attrs.append(attr)\n            all_domain = Orange.data.Domain(\n                # len(unselected_indices) can be 0\n                attrs[:len(attrs) - len(unselected_indices)],\n                items.domain.class_vars, items.domain.metas)\n\n            selected_data = LazyValue[Table](\n                lambda: items.from_table(all_domain, items),\n                domain=all_domain, length=len(items))\n\n            sel_domain = Orange.data.Domain(\n                attrs,\n                items.domain.class_vars, items.domain.metas)\n            annotated_data = LazyValue[Table](\n                lambda: items.from_table(sel_domain, items),\n                domain=sel_domain, length=len(items))\n\n        self.Outputs.selected_data.send(selected_data)\n        self.Outputs.annotated_data.send(annotated_data)\n\n    @Slot(QPointF)\n    def _activate_cut_line(self, pos: QPointF):\n        \"\"\"Activate cut line selection an set cut value to `pos.x()`.\"\"\"\n        self.selection_method = 1\n        self.cut_line.setValue(pos.x())\n        self._selection_method_changed()\n\n    def onDeleteWidget(self):\n        super().onDeleteWidget()\n        self._clear_plot()\n        self.dendrogram.clear()\n        self.dendrogram.deleteLater()\n\n    def _dendrogram_geom_changed(self):\n        pos = self.dendrogram.pos_at_height(self.cutoff_height)\n        geom = self.dendrogram.geometry()\n        self._set_slider_value(pos.x(), geom.width())\n\n        self.cut_line.setLength(\n            self.bottom_axis.geometry().bottom()\n            - self.top_axis.geometry().top()\n        )\n\n        geom = self._main_graphics.geometry()\n        assert geom.topLeft() == QPointF(0, 0)\n\n        def adjustLeft(rect):\n            rect = QRectF(rect)\n            rect.setLeft(geom.left())\n            return rect\n        margin = 3\n        self.scene.setSceneRect(geom)\n        self.view.setSceneRect(geom)\n        self.view.setHeaderSceneRect(\n            adjustLeft(self.top_axis.geometry()).adjusted(0, 0, 0, margin)\n        )\n        self.view.setFooterSceneRect(\n            adjustLeft(self.bottom_axis.geometry()).adjusted(0, -margin, 0, 0)\n        )\n\n    def _dendrogram_slider_changed(self, value):\n        p = QPointF(value, 0)\n        cl_height = self.dendrogram.height_at(p)\n\n        self.set_cutoff_height(cl_height)\n\n    def _set_slider_value(self, value, span):\n        with blocked(self.cut_line):\n            self.cut_line.setRange(0, span)\n            self.cut_line.setValue(value)\n\n    def set_cutoff_height(self, height):\n        self.cutoff_height = height\n        if self.root:\n            self.cut_ratio = 100 * height / self.root.value.height\n        self.select_max_height(height)\n\n    def _set_cut_line_visible(self, visible):\n        self.cut_line.setVisible(visible)\n\n    def select_top_n(self, n):\n        root = self._displayed_root\n        if root:\n            clusters = top_clusters(root, n)\n            self.dendrogram.set_selected_clusters(clusters)\n\n    def select_max_height(self, height):\n        root = self._displayed_root\n        if root:\n            clusters = clusters_at_height(root, height)\n            self.dendrogram.set_selected_clusters(clusters)\n\n    def _cut_ratio_changed(self):\n        self.selection_method = 1\n        self._selection_method_changed()\n\n    def _cut_ratio_return(self):\n        if self.selection_method != 1:\n            self.selection_method = 1\n            self._selection_method_changed()\n\n    def _top_n_changed(self):\n        self.selection_method = 2\n        self._selection_method_changed()\n\n    def _top_n_return(self):\n        if self.selection_method != 2:\n            self.selection_method = 2\n            self._selection_method_changed()\n\n    def _selection_method_changed(self):\n        self._set_cut_line_visible(self.selection_method == 1)\n        if self.root:\n            self._apply_selection()\n\n    def _apply_selection(self):\n        if not self.root:\n            return\n\n        if self.selection_method == 0:\n            pass\n        elif self.selection_method == 1:\n            height = self.cut_ratio * self.root.value.height / 100\n            self.set_cutoff_height(height)\n            pos = self.dendrogram.pos_at_height(height)\n            self._set_slider_value(pos.x(), self.dendrogram.size().width())\n        elif self.selection_method == 2:\n            self.select_top_n(self.top_n)\n\n    def _selection_edited(self):\n        # Selection was edited by clicking on a cluster in the\n        # dendrogram view.\n        self.selection_method = 0\n        self._selection_method_changed()\n        self._invalidate_output()\n\n    def _save_selection(self):\n        # Save the current manual node selection state\n        selection_state = None\n        if self.selection_method == 0 and self.root:\n            assert self.linkmatrix is not None\n            linkmat = [(int(_0), int(_1), _2)\n                       for _0, _1, _2 in self.linkmatrix[:, :3].tolist()]\n            nodes_ = self.dendrogram.selected_nodes()\n            # match the display (pruned) nodes back (by ranges)\n            mapping = {node.value.range: node for node in postorder(self.root)}\n            nodes = [mapping[node.value.range] for node in nodes_]\n            indices = [tuple(node.value.index for node in leaves(node))\n                       for node in nodes]\n            if nodes:\n                selection_state = (indices, linkmat)\n        return selection_state\n\n    def save_state(self):\n        # type: () -> Dict[str, Any]\n        \"\"\"\n        Save state for `set_restore_state`\n        \"\"\"\n        selection = self._save_selection()\n        res = {\"version\": (0, 0, 0)}\n        if selection is not None:\n            res[\"selection_state\"] = selection\n        return res\n\n    def set_restore_state(self, state):\n        # type: (Dict[str, Any]) -> bool\n        \"\"\"\n        Restore session data from a saved state.\n\n        Parameters\n        ----------\n        state : Dict[str, Any]\n\n        NOTE\n        ----\n        This is method called while the instance (self) is being constructed,\n        even before its `__init__` is called. Consider `self` to be only a\n        `QObject` at this stage.\n        \"\"\"\n        if \"selection_state\" in state:\n            selection = state[\"selection_state\"]\n            self.__pending_selection_restore = selection\n        return True\n\n    def __zoom_in(self):\n        def clip(minval, maxval, val):\n            return min(max(val, minval), maxval)\n        self.zoom_factor = clip(self.zoom_slider.minimum(),\n                                self.zoom_slider.maximum(),\n                                self.zoom_factor + 1)\n        self.__update_font_scale()\n\n    def __zoom_out(self):\n        def clip(minval, maxval, val):\n            return min(max(val, minval), maxval)\n        self.zoom_factor = clip(self.zoom_slider.minimum(),\n                                self.zoom_slider.maximum(),\n                                self.zoom_factor - 1)\n        self.__update_font_scale()\n\n    def __zoom_reset(self):\n        self.zoom_factor = 0\n        self.__update_font_scale()\n\n    def __update_font_scale(self):\n        font = self.scene.font()\n        factor = (1.25 ** self.zoom_factor)\n        font = qfont_scaled(font, factor)\n        self._main_graphics.setFont(font)\n        self.label_model.setFont(font)\n\n    def send_report(self):\n        annot = self.label_cb.currentText()\n        if isinstance(self.annotation, str):\n            annot = annot.lower()\n        if self.selection_method == 0:\n            sel = \"manual\"\n        elif self.selection_method == 1:\n            sel = \"at {:.1f} of height\".format(self.cut_ratio)\n        else:\n            sel = f\"top {self.top_n} {pl(self.top_n, 'cluster')}\"\n        self.report_items((\n            (\"Linkage\", LINKAGE[self.linkage]),\n            (\"Annotation\", annot),\n            (\"Pruning\",\n             self.pruning != 0 and \"{} levels\".format(self.max_depth)),\n            (\"Selection\", sel),\n        ))\n        self.report_plot()\n\n    @classmethod\n    def migrate_context(cls, context, version):\n        if version < 2:\n            if context.values[\"annotation\"] == \"None\":\n                context.values[\"annotation\"] = None",
                            "ast_type_str": "ClassDef",
                            "start_lineno": 177,
                            "end_lineno": 1074,
                            "parent": null
                        },
                        {
                            "file_name": "Orange/widgets/unsupervised/owhierarchicalclustering.py",
                            "type": "method",
                            "name": "OWHierarchicalClustering.commit",
                            "content": "class OWHierarchicalClustering(widget.OWWidget):\n    name = \"Hierarchical Clustering\"\n    description = \"Display a dendrogram of a hierarchical clustering \" \\\n                  \"constructed from the input distance matrix.\"\n    icon = \"icons/HierarchicalClustering.svg\"\n    priority = 2100\n    keywords = \"hierarchical clustering\"\n\n    class Inputs:\n        distances = Input(\"Distances\", Orange.misc.DistMatrix)\n        subset = Input(\"Data Subset\", Orange.data.Table, explicit=True)\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    settings_version = 2\n    settingsHandler = _DomainContextHandler()\n\n    #: Selected linkage\n    linkage = settings.Setting(LINKAGE.index(DEFAULT_LINKAGE))\n    #: Index of the selected annotation item (variable, ...)\n    annotation = settings.ContextSetting(\"Enumeration\")\n    #: Out-of-context setting for the case when the \"Name\" option is available\n    annotation_if_names = settings.Setting(\"Name\")\n    #: Out-of-context setting for the case with just \"Enumeration\" and \"None\"\n    annotation_if_enumerate = settings.Setting(\"Enumeration\")\n    #: Selected tree pruning (none/max depth)\n    pruning = settings.Setting(0)\n    #: Maximum depth when max depth pruning is selected\n    max_depth = settings.Setting(10)\n\n    #: Selected cluster selection method (none, cut distance, top n)\n    selection_method = settings.Setting(0)\n    #: Cut height ratio wrt root height\n    cut_ratio = settings.Setting(75.0)\n    #: Number of top clusters to select\n    top_n = settings.Setting(3)\n    #: Dendrogram zoom factor\n    zoom_factor = settings.Setting(0)\n    #: Show labels only for subset (if present)\n    label_only_subset = settings.Setting(False)\n    #: Color for label decoration\n    color_by: Union[DiscreteVariable, ContinuousVariable, None] = \\\n        settings.ContextSetting(None)\n\n    autocommit = settings.Setting(True)\n\n    graph_name = \"scene\"  # QGraphicsScene\n\n    basic_annotations = [None, \"Enumeration\"]\n\n    class Error(widget.OWWidget.Error):\n        empty_matrix = Msg(\"Distance matrix is empty.\")\n        not_finite_distances = Msg(\"Some distances are infinite\")\n        not_symmetric = widget.Msg(\"Distance matrix is not symmetric.\")\n\n    class Warning(widget.OWWidget.Warning):\n        subset_on_no_table = \\\n            Msg(\"Unused data subset: distances do not refer to data instances\")\n        subset_not_subset = \\\n            Msg(\"Some data from the subset does not appear in distance matrix\")\n        subset_wrong = \\\n            Msg(\"Subset data refers to a different table\")\n        pruning_disables_colors = \\\n            Msg(\"Pruned cluster doesn't show colors and indicate subset\")\n        many_clusters = \\\n            Msg(\"Variables with too many values may \"\n                \"degrade the performance of downstream widgets.\")\n\n    #: Stored (manual) selection state (from a saved workflow) to restore.\n    __pending_selection_restore = None  # type: Optional[SelectionState]\n\n    def __init__(self):\n        super().__init__()\n\n        self.matrix = None\n        self.items = None\n        self.subset = None\n        self.subset_rows = set()\n        self.linkmatrix = None\n        self.root = None\n        self._displayed_root = None\n        self.cutoff_height = 0.0\n\n        spin_width = QFontMetrics(self.font()).horizontalAdvance(\"M\" * 7)\n        gui.comboBox(\n            self.controlArea, self, \"linkage\", items=LINKAGE, box=\"Linkage\",\n            callback=self._invalidate_clustering)\n\n        model = itemmodels.VariableListModel(placeholder=\"None\")\n        model[:] = self.basic_annotations\n\n        grid = QGridLayout()\n        gui.widgetBox(self.controlArea, \"Annotations\", orientation=grid)\n        self.label_cb = cb = combobox.ComboBoxSearch(\n            minimumContentsLength=14,\n            sizeAdjustPolicy=QComboBox.AdjustToMinimumContentsLengthWithIcon\n        )\n        cb.setModel(model)\n        cb.setCurrentIndex(cb.findData(self.annotation, Qt.EditRole))\n\n        def on_annotation_activated():\n            self.annotation = self.label_cb.currentData(Qt.EditRole)\n            self._update_labels()\n        cb.activated.connect(on_annotation_activated)\n\n        def on_annotation_changed(value):\n            self.label_cb.setCurrentIndex(\n                self.label_cb.findData(value, Qt.EditRole))\n        self.connect_control(\"annotation\", on_annotation_changed)\n\n        grid.addWidget(self.label_cb, 0, 0, 1, 2)\n\n        cb = gui.checkBox(\n            None, self, \"label_only_subset\", \"Show labels only for subset\",\n            disabled=True,\n            callback=self._update_labels, stateWhenDisabled=False)\n        grid.addWidget(cb, 1, 0, 1, 2)\n\n        model = itemmodels.DomainModel(\n            valid_types=(DiscreteVariable, ContinuousVariable),\n            placeholder=\"None\")\n        cb = gui.comboBox(\n            None, self, \"color_by\", orientation=Qt.Horizontal,\n            model=model, callback=self._update_labels,\n            sizePolicy=QSizePolicy(QSizePolicy.MinimumExpanding,\n                                   QSizePolicy.Fixed))\n        self.color_by_label = QLabel(\"Color by:\")\n        grid.addWidget(self.color_by_label, 2, 0)\n        grid.addWidget(cb, 2, 1)\n\n        box = gui.radioButtons(\n            self.controlArea, self, \"pruning\", box=\"Pruning\",\n            callback=self._invalidate_pruning)\n        grid = QGridLayout()\n        box.layout().addLayout(grid)\n        grid.addWidget(\n            gui.appendRadioButton(box, \"None\", addToLayout=False),\n            0, 0\n        )\n        self.max_depth_spin = gui.spin(\n            box, self, \"max_depth\", minv=1, maxv=100,\n            controlWidth=spin_width, alignment=Qt.AlignRight,\n            callback=self._max_depth_changed,\n            keyboardTracking=False, addToLayout=False\n        )\n        self.max_depth_spin.lineEdit().returnPressed.connect(\n            self._max_depth_return)\n\n        grid.addWidget(\n            gui.appendRadioButton(box, \"Max depth:\", addToLayout=False),\n            1, 0)\n        grid.addWidget(self.max_depth_spin, 1, 1)\n\n        self.selection_box = gui.radioButtons(\n            self.controlArea, self, \"selection_method\",\n            box=\"Selection\",\n            callback=self._selection_method_changed)\n\n        grid = QGridLayout()\n        self.selection_box.layout().addLayout(grid)\n        grid.addWidget(\n            gui.appendRadioButton(\n                self.selection_box, \"Manual\", addToLayout=False),\n            0, 0\n        )\n        grid.addWidget(\n            gui.appendRadioButton(\n                self.selection_box, \"Height ratio:\", addToLayout=False),\n            1, 0\n        )\n        self.cut_ratio_spin = gui.spin(\n            self.selection_box, self, \"cut_ratio\", 0, 100, step=1e-1,\n            controlWidth=spin_width, alignment = Qt.AlignRight,\n            spinType=float, callback=self._cut_ratio_changed,\n            addToLayout=False\n        )\n        self.cut_ratio_spin.setSuffix(\" %\")\n        self.cut_ratio_spin.lineEdit().returnPressed.connect(\n            self._cut_ratio_return)\n\n        grid.addWidget(self.cut_ratio_spin, 1, 1)\n\n        grid.addWidget(\n            gui.appendRadioButton(\n                self.selection_box, \"Top N:\", addToLayout=False),\n            2, 0\n        )\n        self.top_n_spin = gui.spin(\n            self.selection_box, self, \"top_n\", 1, 1000,\n            controlWidth=spin_width, alignment=Qt.AlignRight,\n            callback=self._top_n_changed, addToLayout=False)\n        self.top_n_spin.lineEdit().returnPressed.connect(self._top_n_return)\n        grid.addWidget(self.top_n_spin, 2, 1)\n\n        self.zoom_slider = gui.hSlider(\n            self.controlArea, self, \"zoom_factor\", box=\"Zoom\",\n            minValue=-6, maxValue=3, step=1, ticks=True, createLabel=False,\n            callback=self.__update_font_scale)\n\n        zoom_in = QAction(\n            \"Zoom in\", self, shortcut=QKeySequence.ZoomIn,\n            triggered=self.__zoom_in\n        )\n        zoom_out = QAction(\n            \"Zoom out\", self, shortcut=QKeySequence.ZoomOut,\n            triggered=self.__zoom_out\n        )\n        zoom_reset = QAction(\n            \"Reset zoom\", self,\n            shortcut=QKeySequence(Qt.ControlModifier | Qt.Key_0),\n            triggered=self.__zoom_reset\n        )\n        self.addActions([zoom_in, zoom_out, zoom_reset])\n\n        self.controlArea.layout().addStretch()\n\n        gui.auto_send(self.buttonsArea, self, \"autocommit\")\n\n        self.scene = QGraphicsScene(self)\n        self.view = GraphicsView(\n            self.scene,\n            horizontalScrollBarPolicy=Qt.ScrollBarAlwaysOff,\n            verticalScrollBarPolicy=Qt.ScrollBarAlwaysOn,\n            alignment=Qt.AlignLeft | Qt.AlignVCenter,\n            widgetResizable=True,\n        )\n        # Disable conflicting action shortcuts. We define our own.\n        for a in self.view.viewActions():\n            a.setEnabled(False)\n        self.mainArea.layout().setSpacing(1)\n        self.mainArea.layout().addWidget(self.view)\n\n        def axis_view(orientation):\n            ax = AxisItem(orientation=orientation, maxTickLength=7)\n            ax.mousePressed.connect(self._activate_cut_line)\n            ax.mouseMoved.connect(self._activate_cut_line)\n            ax.mouseReleased.connect(self._activate_cut_line)\n            ax.setRange(1.0, 0.0)\n            return ax\n\n        self.top_axis = axis_view(\"top\")\n        self.bottom_axis = axis_view(\"bottom\")\n\n        self._main_graphics = QGraphicsWidget(\n            sizePolicy=QSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.Preferred)\n        )\n        scenelayout = QGraphicsGridLayout()\n        scenelayout.setHorizontalSpacing(10)\n        scenelayout.setVerticalSpacing(10)\n\n        self._main_graphics.setLayout(scenelayout)\n        self.scene.addItem(self._main_graphics)\n        self.view.setCentralWidget(self._main_graphics)\n        self.scene.addItem(self._main_graphics)\n\n        self.dendrogram = DendrogramWidget(pen_width=2)\n        self.dendrogram.setSizePolicy(QSizePolicy.MinimumExpanding,\n                                      QSizePolicy.MinimumExpanding)\n        self.dendrogram.selectionChanged.connect(self._invalidate_output)\n        self.dendrogram.selectionEdited.connect(self._selection_edited)\n\n        self.labels = TextListView()\n        self.label_model = SelectedLabelsModel()\n        self.labels.setModel(self.label_model)\n        self.labels.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Preferred)\n        self.labels.setAlignment(Qt.AlignLeft)\n        self.labels.setMaximumWidth(200)\n\n        scenelayout.addItem(self.top_axis, 0, 0,\n                            alignment=Qt.AlignLeft | Qt.AlignVCenter)\n        scenelayout.addItem(self.dendrogram, 1, 0,\n                            alignment=Qt.AlignLeft | Qt.AlignVCenter)\n        scenelayout.addItem(self.labels, 1, 1,\n                            alignment=Qt.AlignLeft | Qt.AlignVCenter)\n        scenelayout.addItem(self.bottom_axis, 2, 0,\n                            alignment=Qt.AlignLeft | Qt.AlignVCenter)\n        self.top_axis.setZValue(self.dendrogram.zValue() + 10)\n        self.bottom_axis.setZValue(self.dendrogram.zValue() + 10)\n        self.cut_line = SliderLine(self.top_axis,\n                                   orientation=Qt.Horizontal)\n        self.cut_line.valueChanged.connect(self._dendrogram_slider_changed)\n        self.dendrogram.geometryChanged.connect(self._dendrogram_geom_changed)\n        self._set_cut_line_visible(self.selection_method == 1)\n        self.__update_font_scale()\n\n    @Inputs.distances\n    def set_distances(self, matrix):\n        self.error()\n        self.Error.clear()\n\n        self.matrix = None\n        self.Error.clear()\n        if matrix is not None:\n            if len(matrix) < 2:\n                self.Error.empty_matrix()\n            elif not matrix.is_symmetric():\n                self.Error.not_symmetric()\n            elif not np.all(np.isfinite(matrix)):\n                self.Error.not_finite_distances()\n            else:\n                self.matrix = matrix\n\n    @Inputs.subset\n    def set_subset(self, subset):\n        self.subset = subset\n        self.controls.label_only_subset.setDisabled(subset is None)\n\n    def handleNewSignals(self):\n        if self.__pending_selection_restore is not None:\n            selection_state = self.__pending_selection_restore\n        else:\n            # save the current selection to (possibly) restore later\n            selection_state = self._save_selection()\n\n        matrix = self.matrix\n        if matrix is not None:\n            self._set_items(matrix.row_items, matrix.axis)\n        else:\n            self._set_items(None)\n        self._update()\n\n        # Can now attempt to restore session state from a saved workflow.\n        if self.root and selection_state is not None:\n            self._restore_selection(selection_state)\n            self.__pending_selection_restore = None\n\n        self.Warning.clear()\n        rows = set()\n        if self.subset:\n            subsetids = set(self.subset.ids)\n            if not isinstance(self.items, Orange.data.Table) \\\n                    or not self.matrix.axis:\n                self.Warning.subset_on_no_table()\n            elif (dataids := set(self.items.ids)) and not subsetids & dataids:\n                self.Warning.subset_wrong()\n            elif not subsetids <= dataids:\n                self.Warning.subset_not_subset()\n            else:\n                indices = [leaf.value.index for leaf in leaves(self.root)]\n                rows = {\n                    row for row, rowid in enumerate(self.items.ids[indices])\n                    if rowid in subsetids\n                }\n\n        self.subset_rows = rows\n        self._update_labels()\n        self.commit.now()\n\n    def _set_items(self, items, axis=1):\n        self.closeContext()\n        self.items = items\n        model = self.label_cb.model()\n        color_model = self.controls.color_by.model()\n        color_model.set_domain(None)\n        self.color_by = None\n        if len(model) == 2 and model[0] is None:\n            if model[1] == \"Name\":\n                self.annotation_if_names = self.annotation\n            if model[1] == self.basic_annotations[1]:\n                self.annotation_if_enumerate = self.annotation\n        if isinstance(items, Orange.data.Table) and axis:\n            metas_class = tuple(\n                filter_visible(chain(items.domain.metas,\n                                     items.domain.class_vars)))\n            visible_attrs = tuple(filter_visible(items.domain.attributes))\n            if not (metas_class or visible_attrs):\n                model[:] = self.basic_annotations\n            else:\n                model[:] = (\n                    (None, )\n                    + metas_class\n                    + (model.Separator, ) * bool(metas_class and visible_attrs)\n                    + visible_attrs)\n            for meta in items.domain.metas:\n                if isinstance(meta, StringVariable):\n                    self.annotation = meta\n                    break\n            else:\n                if items.domain.class_vars:\n                    # No string metas: show class\n                    self.annotation = items.domain.class_vars[0]\n                else:\n                    # No string metas and no class: show the first option\n                    # which is not None (in the worst case, Enumeration)\n                    self.annotation = model[1]\n\n            color_model.set_domain(items.domain)\n            if items.domain.class_vars:\n                self.color_by = items.domain.class_vars[0]\n            self.openContext(items.domain)\n        elif isinstance(items, Orange.data.Table) and not axis \\\n                or (isinstance(items, list) and \\\n                    all(isinstance(var, Orange.data.Variable)\n                        for var in items)):\n            model[:] = (None, \"Name\")\n            self.annotation = self.annotation_if_names\n        else:\n            model[:] = self.basic_annotations\n            self.annotation = self.annotation_if_enumerate\n\n        no_colors = len(color_model) == 1\n        self.controls.color_by.setDisabled(no_colors)\n        self.color_by_label.setDisabled(no_colors)\n\n    def _clear_plot(self):\n        self.dendrogram.set_root(None)\n        self.label_model.clear()\n\n    def _set_displayed_root(self, root):\n        self._clear_plot()\n        self._displayed_root = root\n        self.dendrogram.set_root(root)\n        self._update_labels()\n\n    def _update(self):\n        self._clear_plot()\n\n        distances = self.matrix\n\n        if distances is not None:\n            method = LINKAGE_ARGS[self.linkage]\n            Z = dist_matrix_linkage(distances, linkage=method)\n\n            tree = tree_from_linkage(Z)\n            self.linkmatrix = Z\n            self.root = tree\n\n            self.top_axis.setRange(tree.value.height, 0.0)\n            self.bottom_axis.setRange(tree.value.height, 0.0)\n\n            if self.pruning:\n                self._set_displayed_root(prune(tree, level=self.max_depth))\n            else:\n                self._set_displayed_root(tree)\n        else:\n            self.linkmatrix = None\n            self.root = None\n            self._set_displayed_root(None)\n\n        self._apply_selection()\n\n    def _update_labels(self):\n        if not hasattr(self, \"label_model\"):\n            # This method can be called during widget initialization when\n            # creating check box for label_only_subset, if it's value is\n            # initially True.\n            # See https://github.com/biolab/orange-widget-base/pull/213;\n            # if it's merged, this check can be removed.\n            return\n\n        self.Warning.pruning_disables_colors(\n            shown=self.pruning\n                  and (self.subset_rows or self.color_by is not None))\n        labels = []\n        if self.root and self._displayed_root:\n            indices = [leaf.value.index for leaf in leaves(self.root)]\n\n            if self.annotation is None:\n                if not self.pruning \\\n                        and self.subset_rows and self.color_by is None:\n                    # Model fails if number of labels and of colors mismatch\n                    labels = [\"\"] * len(indices)\n                else:\n                    labels = []\n            elif self.annotation == \"Enumeration\":\n                labels = [str(i+1) for i in indices]\n            elif self.annotation == \"Name\":\n                attr = self.matrix.row_items.domain.attributes\n                labels = [str(attr[i]) for i in indices]\n            elif isinstance(self.annotation, Orange.data.Variable):\n                col_data = self.items.get_column(self.annotation)\n                labels = [self.annotation.str_val(val) for val in col_data]\n                labels = [labels[idx] for idx in indices]\n            else:\n                labels = []\n            if not self.pruning and \\\n                    labels and self.label_only_subset and self.subset_rows:\n                labels = [label if row in self.subset_rows else \"\"\n                          for row, label in enumerate(labels)]\n\n            if labels and self._displayed_root is not self.root:\n                joined = leaves(self._displayed_root)\n                labels = [\", \".join(labels[leaf.value.first: leaf.value.last])\n                          for leaf in joined]\n\n        self.label_model[:] = labels\n        self.label_model.set_subset(set() if self.pruning else self.subset_rows)\n        self.labels.setMinimumWidth(1 if labels else -1)\n\n        if not self.pruning and self.color_by is not None:\n            col = self.items.get_column(self.color_by)\n            self.label_model.set_colors(\n                self.color_by.palette.values_to_qcolors(col[indices]))\n        else:\n            self.label_model.set_colors(None)\n\n    def _restore_selection(self, state):\n        # type: (SelectionState) -> bool\n        \"\"\"\n        Restore the (manual) node selection state.\n\n        Return True if successful; False otherwise.\n        \"\"\"\n        linkmatrix = self.linkmatrix\n        if self.selection_method == 0 and self.root:\n            selected, linksaved = state\n            linkstruct = np.array(linksaved, dtype=float)\n            selected = set(selected)  # type: Set[Tuple[int]]\n            if not selected:\n                return False\n            if linkmatrix.shape[0] != linkstruct.shape[0]:\n                return False\n            # check that the linkage matrix structure matches. Use isclose for\n            # the height column to account for inexact floating point math\n            # (e.g. summation order in different ?gemm implementations for\n            # euclidean distances, ...)\n            if np.any(linkstruct[:, :2] != linkmatrix[:, :2]) or \\\n                    not np.all(np.isclose(linkstruct[:, 2], linkstruct[:, 2])):\n                return False\n            selection = []\n            indices = np.array([n.value.index for n in leaves(self.root)],\n                               dtype=int)\n            # mapping from ranges to display (pruned) nodes\n            mapping = {node.value.range: node\n                       for node in postorder(self._displayed_root)}\n            for node in postorder(self.root):  # type: Tree\n                r = tuple(indices[node.value.first: node.value.last])\n                if r in selected:\n                    if node.value.range not in mapping:\n                        # the node was pruned from display and cannot be\n                        # selected\n                        break\n                    selection.append(mapping[node.value.range])\n                    selected.remove(r)\n                if not selected:\n                    break  # found all, nothing more to do\n            if selection and selected:\n                # Could not restore all selected nodes (only partial match)\n                return False\n\n            self._set_selected_nodes(selection)\n            return True\n        return False\n\n    def _set_selected_nodes(self, selection):\n        # type: (List[Tree]) -> None\n        \"\"\"\n        Set the nodes in `selection` to be the current selected nodes.\n\n        The selection nodes must be subtrees of the current `_displayed_root`.\n        \"\"\"\n        self.dendrogram.selectionChanged.disconnect(self._invalidate_output)\n        try:\n            self.dendrogram.set_selected_clusters(selection)\n        finally:\n            self.dendrogram.selectionChanged.connect(self._invalidate_output)\n\n    def _max_depth_return(self):\n        if self.pruning != 1:\n            self.pruning = 1\n            self._invalidate_pruning()\n\n    def _max_depth_changed(self):\n        self.pruning = 1\n        self._invalidate_pruning()\n\n    def _invalidate_clustering(self):\n        self._update()\n        self._update_labels()\n        self._invalidate_output()\n\n    def _invalidate_output(self):\n        self.commit.deferred()\n\n    def _invalidate_pruning(self):\n        if self.root:\n            selection = self.dendrogram.selected_nodes()\n            ranges = [node.value.range for node in selection]\n            if self.pruning:\n                self._set_displayed_root(\n                    prune(self.root, level=self.max_depth))\n            else:\n                self._set_displayed_root(self.root)\n            selected = [node for node in preorder(self._displayed_root)\n                        if node.value.range in ranges]\n\n            self.dendrogram.set_selected_clusters(selected)\n\n        self._apply_selection()\n\n    @gui.deferred\n    def commit(self):\n        items = getattr(self.matrix, \"items\", self.items)\n        self.Warning.many_clusters.clear()\n        if not items:\n            self.Outputs.selected_data.send(None)\n            self.Outputs.annotated_data.send(None)\n            return\n\n        selection = self.dendrogram.selected_nodes()\n        selection = sorted(selection, key=lambda c: c.value.first)\n\n        indices = [leaf.value.index for leaf in leaves(self.root)]\n\n        maps = [indices[node.value.first:node.value.last]\n                for node in selection]\n        if len(maps) > 20:\n            self.Warning.many_clusters()\n\n        selected_indices = list(chain(*maps))\n\n        if not selected_indices:\n            self.Outputs.selected_data.send(None)\n            annotated_data = lazy_annotated_table(items, []) \\\n                if self.selection_method == 0 and self.matrix.axis else None\n            self.Outputs.annotated_data.send(annotated_data)\n            return\n\n        selected_data = annotated_data = None\n\n        if isinstance(items, Orange.data.Table) and self.matrix.axis == 1:\n            # Select rows\n            data, domain = items, items.domain\n\n            c = np.full(self.matrix.shape[0], len(maps))\n            for i, indices in enumerate(maps):\n                c[indices] = i\n\n            clust_name = get_unique_names(domain, \"Cluster\")\n            values = [f\"C{i + 1}\" for i in range(len(maps))]\n\n            sel_clust_var = Orange.data.DiscreteVariable(\n                name=clust_name, values=values)\n            sel_domain = add_columns(domain, metas=(sel_clust_var,))\n            selected_data = LazyValue[Table](\n                lambda: items.add_column(\n                    sel_clust_var, c, to_metas=True)[c != len(maps)],\n                domain=sel_domain, length=len(selected_indices))\n\n            ann_clust_var = Orange.data.DiscreteVariable(\n                name=clust_name, values=values + [\"Other\"]\n            )\n            ann_domain = add_columns(\n                domain_with_annotation_column(data)[0], metas=(ann_clust_var, ))\n            annotated_data = LazyValue[Table](\n                lambda: create_annotated_table(\n                    data=items.add_column(ann_clust_var, c, to_metas=True),\n                    selected_indices=selected_indices),\n                domain=ann_domain, length=len(items)\n            )\n\n        elif isinstance(items, Orange.data.Table) and self.matrix.axis == 0:\n            # Select columns\n            attrs = []\n            unselected_indices = sorted(set(range(self.root.value.last)) -\n                                        set(selected_indices))\n            for clust, indices in chain(enumerate(maps, start=1),\n                                        [(0, unselected_indices)]):\n                for i in indices:\n                    attr = items.domain[i].copy()\n                    attr.attributes[\"cluster\"] = clust\n                    attrs.append(attr)\n            all_domain = Orange.data.Domain(\n                # len(unselected_indices) can be 0\n                attrs[:len(attrs) - len(unselected_indices)],\n                items.domain.class_vars, items.domain.metas)\n\n            selected_data = LazyValue[Table](\n                lambda: items.from_table(all_domain, items),\n                domain=all_domain, length=len(items))\n\n            sel_domain = Orange.data.Domain(\n                attrs,\n                items.domain.class_vars, items.domain.metas)\n            annotated_data = LazyValue[Table](\n                lambda: items.from_table(sel_domain, items),\n                domain=sel_domain, length=len(items))\n\n        self.Outputs.selected_data.send(selected_data)\n        self.Outputs.annotated_data.send(annotated_data)\n\n    @Slot(QPointF)\n    def _activate_cut_line(self, pos: QPointF):\n        \"\"\"Activate cut line selection an set cut value to `pos.x()`.\"\"\"\n        self.selection_method = 1\n        self.cut_line.setValue(pos.x())\n        self._selection_method_changed()\n\n    def onDeleteWidget(self):\n        super().onDeleteWidget()\n        self._clear_plot()\n        self.dendrogram.clear()\n        self.dendrogram.deleteLater()\n\n    def _dendrogram_geom_changed(self):\n        pos = self.dendrogram.pos_at_height(self.cutoff_height)\n        geom = self.dendrogram.geometry()\n        self._set_slider_value(pos.x(), geom.width())\n\n        self.cut_line.setLength(\n            self.bottom_axis.geometry().bottom()\n            - self.top_axis.geometry().top()\n        )\n\n        geom = self._main_graphics.geometry()\n        assert geom.topLeft() == QPointF(0, 0)\n\n        def adjustLeft(rect):\n            rect = QRectF(rect)\n            rect.setLeft(geom.left())\n            return rect\n        margin = 3\n        self.scene.setSceneRect(geom)\n        self.view.setSceneRect(geom)\n        self.view.setHeaderSceneRect(\n            adjustLeft(self.top_axis.geometry()).adjusted(0, 0, 0, margin)\n        )\n        self.view.setFooterSceneRect(\n            adjustLeft(self.bottom_axis.geometry()).adjusted(0, -margin, 0, 0)\n        )\n\n    def _dendrogram_slider_changed(self, value):\n        p = QPointF(value, 0)\n        cl_height = self.dendrogram.height_at(p)\n\n        self.set_cutoff_height(cl_height)\n\n    def _set_slider_value(self, value, span):\n        with blocked(self.cut_line):\n            self.cut_line.setRange(0, span)\n            self.cut_line.setValue(value)\n\n    def set_cutoff_height(self, height):\n        self.cutoff_height = height\n        if self.root:\n            self.cut_ratio = 100 * height / self.root.value.height\n        self.select_max_height(height)\n\n    def _set_cut_line_visible(self, visible):\n        self.cut_line.setVisible(visible)\n\n    def select_top_n(self, n):\n        root = self._displayed_root\n        if root:\n            clusters = top_clusters(root, n)\n            self.dendrogram.set_selected_clusters(clusters)\n\n    def select_max_height(self, height):\n        root = self._displayed_root\n        if root:\n            clusters = clusters_at_height(root, height)\n            self.dendrogram.set_selected_clusters(clusters)\n\n    def _cut_ratio_changed(self):\n        self.selection_method = 1\n        self._selection_method_changed()\n\n    def _cut_ratio_return(self):\n        if self.selection_method != 1:\n            self.selection_method = 1\n            self._selection_method_changed()\n\n    def _top_n_changed(self):\n        self.selection_method = 2\n        self._selection_method_changed()\n\n    def _top_n_return(self):\n        if self.selection_method != 2:\n            self.selection_method = 2\n            self._selection_method_changed()\n\n    def _selection_method_changed(self):\n        self._set_cut_line_visible(self.selection_method == 1)\n        if self.root:\n            self._apply_selection()\n\n    def _apply_selection(self):\n        if not self.root:\n            return\n\n        if self.selection_method == 0:\n            pass\n        elif self.selection_method == 1:\n            height = self.cut_ratio * self.root.value.height / 100\n            self.set_cutoff_height(height)\n            pos = self.dendrogram.pos_at_height(height)\n            self._set_slider_value(pos.x(), self.dendrogram.size().width())\n        elif self.selection_method == 2:\n            self.select_top_n(self.top_n)\n\n    def _selection_edited(self):\n        # Selection was edited by clicking on a cluster in the\n        # dendrogram view.\n        self.selection_method = 0\n        self._selection_method_changed()\n        self._invalidate_output()\n\n    def _save_selection(self):\n        # Save the current manual node selection state\n        selection_state = None\n        if self.selection_method == 0 and self.root:\n            assert self.linkmatrix is not None\n            linkmat = [(int(_0), int(_1), _2)\n                       for _0, _1, _2 in self.linkmatrix[:, :3].tolist()]\n            nodes_ = self.dendrogram.selected_nodes()\n            # match the display (pruned) nodes back (by ranges)\n            mapping = {node.value.range: node for node in postorder(self.root)}\n            nodes = [mapping[node.value.range] for node in nodes_]\n            indices = [tuple(node.value.index for node in leaves(node))\n                       for node in nodes]\n            if nodes:\n                selection_state = (indices, linkmat)\n        return selection_state\n\n    def save_state(self):\n        # type: () -> Dict[str, Any]\n        \"\"\"\n        Save state for `set_restore_state`\n        \"\"\"\n        selection = self._save_selection()\n        res = {\"version\": (0, 0, 0)}\n        if selection is not None:\n            res[\"selection_state\"] = selection\n        return res\n\n    def set_restore_state(self, state):\n        # type: (Dict[str, Any]) -> bool\n        \"\"\"\n        Restore session data from a saved state.\n\n        Parameters\n        ----------\n        state : Dict[str, Any]\n\n        NOTE\n        ----\n        This is method called while the instance (self) is being constructed,\n        even before its `__init__` is called. Consider `self` to be only a\n        `QObject` at this stage.\n        \"\"\"\n        if \"selection_state\" in state:\n            selection = state[\"selection_state\"]\n            self.__pending_selection_restore = selection\n        return True\n\n    def __zoom_in(self):\n        def clip(minval, maxval, val):\n            return min(max(val, minval), maxval)\n        self.zoom_factor = clip(self.zoom_slider.minimum(),\n                                self.zoom_slider.maximum(),\n                                self.zoom_factor + 1)\n        self.__update_font_scale()\n\n    def __zoom_out(self):\n        def clip(minval, maxval, val):\n            return min(max(val, minval), maxval)\n        self.zoom_factor = clip(self.zoom_slider.minimum(),\n                                self.zoom_slider.maximum(),\n                                self.zoom_factor - 1)\n        self.__update_font_scale()\n\n    def __zoom_reset(self):\n        self.zoom_factor = 0\n        self.__update_font_scale()\n\n    def __update_font_scale(self):\n        font = self.scene.font()\n        factor = (1.25 ** self.zoom_factor)\n        font = qfont_scaled(font, factor)\n        self._main_graphics.setFont(font)\n        self.label_model.setFont(font)\n\n    def send_report(self):\n        annot = self.label_cb.currentText()\n        if isinstance(self.annotation, str):\n            annot = annot.lower()\n        if self.selection_method == 0:\n            sel = \"manual\"\n        elif self.selection_method == 1:\n            sel = \"at {:.1f} of height\".format(self.cut_ratio)\n        else:\n            sel = f\"top {self.top_n} {pl(self.top_n, 'cluster')}\"\n        self.report_items((\n            (\"Linkage\", LINKAGE[self.linkage]),\n            (\"Annotation\", annot),\n            (\"Pruning\",\n             self.pruning != 0 and \"{} levels\".format(self.max_depth)),\n            (\"Selection\", sel),\n        ))\n        self.report_plot()\n\n    @classmethod\n    def migrate_context(cls, context, version):\n        if version < 2:\n            if context.values[\"annotation\"] == \"None\":\n                context.values[\"annotation\"] = None",
                            "ast_type_str": "ClassDef",
                            "start_lineno": 770,
                            "end_lineno": 858,
                            "parent": {
                                "file_name": "Orange/widgets/unsupervised/owhierarchicalclustering.py",
                                "type": "class",
                                "name": "OWHierarchicalClustering",
                                "content": "class OWHierarchicalClustering(widget.OWWidget):\n    name = \"Hierarchical Clustering\"\n    description = \"Display a dendrogram of a hierarchical clustering \" \\\n                  \"constructed from the input distance matrix.\"\n    icon = \"icons/HierarchicalClustering.svg\"\n    priority = 2100\n    keywords = \"hierarchical clustering\"\n\n    class Inputs:\n        distances = Input(\"Distances\", Orange.misc.DistMatrix)\n        subset = Input(\"Data Subset\", Orange.data.Table, explicit=True)\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    settings_version = 2\n    settingsHandler = _DomainContextHandler()\n\n    #: Selected linkage\n    linkage = settings.Setting(LINKAGE.index(DEFAULT_LINKAGE))\n    #: Index of the selected annotation item (variable, ...)\n    annotation = settings.ContextSetting(\"Enumeration\")\n    #: Out-of-context setting for the case when the \"Name\" option is available\n    annotation_if_names = settings.Setting(\"Name\")\n    #: Out-of-context setting for the case with just \"Enumeration\" and \"None\"\n    annotation_if_enumerate = settings.Setting(\"Enumeration\")\n    #: Selected tree pruning (none/max depth)\n    pruning = settings.Setting(0)\n    #: Maximum depth when max depth pruning is selected\n    max_depth = settings.Setting(10)\n\n    #: Selected cluster selection method (none, cut distance, top n)\n    selection_method = settings.Setting(0)\n    #: Cut height ratio wrt root height\n    cut_ratio = settings.Setting(75.0)\n    #: Number of top clusters to select\n    top_n = settings.Setting(3)\n    #: Dendrogram zoom factor\n    zoom_factor = settings.Setting(0)\n    #: Show labels only for subset (if present)\n    label_only_subset = settings.Setting(False)\n    #: Color for label decoration\n    color_by: Union[DiscreteVariable, ContinuousVariable, None] = \\\n        settings.ContextSetting(None)\n\n    autocommit = settings.Setting(True)\n\n    graph_name = \"scene\"  # QGraphicsScene\n\n    basic_annotations = [None, \"Enumeration\"]\n\n    class Error(widget.OWWidget.Error):\n        empty_matrix = Msg(\"Distance matrix is empty.\")\n        not_finite_distances = Msg(\"Some distances are infinite\")\n        not_symmetric = widget.Msg(\"Distance matrix is not symmetric.\")\n\n    class Warning(widget.OWWidget.Warning):\n        subset_on_no_table = \\\n            Msg(\"Unused data subset: distances do not refer to data instances\")\n        subset_not_subset = \\\n            Msg(\"Some data from the subset does not appear in distance matrix\")\n        subset_wrong = \\\n            Msg(\"Subset data refers to a different table\")\n        pruning_disables_colors = \\\n            Msg(\"Pruned cluster doesn't show colors and indicate subset\")\n        many_clusters = \\\n            Msg(\"Variables with too many values may \"\n                \"degrade the performance of downstream widgets.\")\n\n    #: Stored (manual) selection state (from a saved workflow) to restore.\n    __pending_selection_restore = None  # type: Optional[SelectionState]\n\n    def __init__(self):\n        super().__init__()\n\n        self.matrix = None\n        self.items = None\n        self.subset = None\n        self.subset_rows = set()\n        self.linkmatrix = None\n        self.root = None\n        self._displayed_root = None\n        self.cutoff_height = 0.0\n\n        spin_width = QFontMetrics(self.font()).horizontalAdvance(\"M\" * 7)\n        gui.comboBox(\n            self.controlArea, self, \"linkage\", items=LINKAGE, box=\"Linkage\",\n            callback=self._invalidate_clustering)\n\n        model = itemmodels.VariableListModel(placeholder=\"None\")\n        model[:] = self.basic_annotations\n\n        grid = QGridLayout()\n        gui.widgetBox(self.controlArea, \"Annotations\", orientation=grid)\n        self.label_cb = cb = combobox.ComboBoxSearch(\n            minimumContentsLength=14,\n            sizeAdjustPolicy=QComboBox.AdjustToMinimumContentsLengthWithIcon\n        )\n        cb.setModel(model)\n        cb.setCurrentIndex(cb.findData(self.annotation, Qt.EditRole))\n\n        def on_annotation_activated():\n            self.annotation = self.label_cb.currentData(Qt.EditRole)\n            self._update_labels()\n        cb.activated.connect(on_annotation_activated)\n\n        def on_annotation_changed(value):\n            self.label_cb.setCurrentIndex(\n                self.label_cb.findData(value, Qt.EditRole))\n        self.connect_control(\"annotation\", on_annotation_changed)\n\n        grid.addWidget(self.label_cb, 0, 0, 1, 2)\n\n        cb = gui.checkBox(\n            None, self, \"label_only_subset\", \"Show labels only for subset\",\n            disabled=True,\n            callback=self._update_labels, stateWhenDisabled=False)\n        grid.addWidget(cb, 1, 0, 1, 2)\n\n        model = itemmodels.DomainModel(\n            valid_types=(DiscreteVariable, ContinuousVariable),\n            placeholder=\"None\")\n        cb = gui.comboBox(\n            None, self, \"color_by\", orientation=Qt.Horizontal,\n            model=model, callback=self._update_labels,\n            sizePolicy=QSizePolicy(QSizePolicy.MinimumExpanding,\n                                   QSizePolicy.Fixed))\n        self.color_by_label = QLabel(\"Color by:\")\n        grid.addWidget(self.color_by_label, 2, 0)\n        grid.addWidget(cb, 2, 1)\n\n        box = gui.radioButtons(\n            self.controlArea, self, \"pruning\", box=\"Pruning\",\n            callback=self._invalidate_pruning)\n        grid = QGridLayout()\n        box.layout().addLayout(grid)\n        grid.addWidget(\n            gui.appendRadioButton(box, \"None\", addToLayout=False),\n            0, 0\n        )\n        self.max_depth_spin = gui.spin(\n            box, self, \"max_depth\", minv=1, maxv=100,\n            controlWidth=spin_width, alignment=Qt.AlignRight,\n            callback=self._max_depth_changed,\n            keyboardTracking=False, addToLayout=False\n        )\n        self.max_depth_spin.lineEdit().returnPressed.connect(\n            self._max_depth_return)\n\n        grid.addWidget(\n            gui.appendRadioButton(box, \"Max depth:\", addToLayout=False),\n            1, 0)\n        grid.addWidget(self.max_depth_spin, 1, 1)\n\n        self.selection_box = gui.radioButtons(\n            self.controlArea, self, \"selection_method\",\n            box=\"Selection\",\n            callback=self._selection_method_changed)\n\n        grid = QGridLayout()\n        self.selection_box.layout().addLayout(grid)\n        grid.addWidget(\n            gui.appendRadioButton(\n                self.selection_box, \"Manual\", addToLayout=False),\n            0, 0\n        )\n        grid.addWidget(\n            gui.appendRadioButton(\n                self.selection_box, \"Height ratio:\", addToLayout=False),\n            1, 0\n        )\n        self.cut_ratio_spin = gui.spin(\n            self.selection_box, self, \"cut_ratio\", 0, 100, step=1e-1,\n            controlWidth=spin_width, alignment = Qt.AlignRight,\n            spinType=float, callback=self._cut_ratio_changed,\n            addToLayout=False\n        )\n        self.cut_ratio_spin.setSuffix(\" %\")\n        self.cut_ratio_spin.lineEdit().returnPressed.connect(\n            self._cut_ratio_return)\n\n        grid.addWidget(self.cut_ratio_spin, 1, 1)\n\n        grid.addWidget(\n            gui.appendRadioButton(\n                self.selection_box, \"Top N:\", addToLayout=False),\n            2, 0\n        )\n        self.top_n_spin = gui.spin(\n            self.selection_box, self, \"top_n\", 1, 1000,\n            controlWidth=spin_width, alignment=Qt.AlignRight,\n            callback=self._top_n_changed, addToLayout=False)\n        self.top_n_spin.lineEdit().returnPressed.connect(self._top_n_return)\n        grid.addWidget(self.top_n_spin, 2, 1)\n\n        self.zoom_slider = gui.hSlider(\n            self.controlArea, self, \"zoom_factor\", box=\"Zoom\",\n            minValue=-6, maxValue=3, step=1, ticks=True, createLabel=False,\n            callback=self.__update_font_scale)\n\n        zoom_in = QAction(\n            \"Zoom in\", self, shortcut=QKeySequence.ZoomIn,\n            triggered=self.__zoom_in\n        )\n        zoom_out = QAction(\n            \"Zoom out\", self, shortcut=QKeySequence.ZoomOut,\n            triggered=self.__zoom_out\n        )\n        zoom_reset = QAction(\n            \"Reset zoom\", self,\n            shortcut=QKeySequence(Qt.ControlModifier | Qt.Key_0),\n            triggered=self.__zoom_reset\n        )\n        self.addActions([zoom_in, zoom_out, zoom_reset])\n\n        self.controlArea.layout().addStretch()\n\n        gui.auto_send(self.buttonsArea, self, \"autocommit\")\n\n        self.scene = QGraphicsScene(self)\n        self.view = GraphicsView(\n            self.scene,\n            horizontalScrollBarPolicy=Qt.ScrollBarAlwaysOff,\n            verticalScrollBarPolicy=Qt.ScrollBarAlwaysOn,\n            alignment=Qt.AlignLeft | Qt.AlignVCenter,\n            widgetResizable=True,\n        )\n        # Disable conflicting action shortcuts. We define our own.\n        for a in self.view.viewActions():\n            a.setEnabled(False)\n        self.mainArea.layout().setSpacing(1)\n        self.mainArea.layout().addWidget(self.view)\n\n        def axis_view(orientation):\n            ax = AxisItem(orientation=orientation, maxTickLength=7)\n            ax.mousePressed.connect(self._activate_cut_line)\n            ax.mouseMoved.connect(self._activate_cut_line)\n            ax.mouseReleased.connect(self._activate_cut_line)\n            ax.setRange(1.0, 0.0)\n            return ax\n\n        self.top_axis = axis_view(\"top\")\n        self.bottom_axis = axis_view(\"bottom\")\n\n        self._main_graphics = QGraphicsWidget(\n            sizePolicy=QSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.Preferred)\n        )\n        scenelayout = QGraphicsGridLayout()\n        scenelayout.setHorizontalSpacing(10)\n        scenelayout.setVerticalSpacing(10)\n\n        self._main_graphics.setLayout(scenelayout)\n        self.scene.addItem(self._main_graphics)\n        self.view.setCentralWidget(self._main_graphics)\n        self.scene.addItem(self._main_graphics)\n\n        self.dendrogram = DendrogramWidget(pen_width=2)\n        self.dendrogram.setSizePolicy(QSizePolicy.MinimumExpanding,\n                                      QSizePolicy.MinimumExpanding)\n        self.dendrogram.selectionChanged.connect(self._invalidate_output)\n        self.dendrogram.selectionEdited.connect(self._selection_edited)\n\n        self.labels = TextListView()\n        self.label_model = SelectedLabelsModel()\n        self.labels.setModel(self.label_model)\n        self.labels.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Preferred)\n        self.labels.setAlignment(Qt.AlignLeft)\n        self.labels.setMaximumWidth(200)\n\n        scenelayout.addItem(self.top_axis, 0, 0,\n                            alignment=Qt.AlignLeft | Qt.AlignVCenter)\n        scenelayout.addItem(self.dendrogram, 1, 0,\n                            alignment=Qt.AlignLeft | Qt.AlignVCenter)\n        scenelayout.addItem(self.labels, 1, 1,\n                            alignment=Qt.AlignLeft | Qt.AlignVCenter)\n        scenelayout.addItem(self.bottom_axis, 2, 0,\n                            alignment=Qt.AlignLeft | Qt.AlignVCenter)\n        self.top_axis.setZValue(self.dendrogram.zValue() + 10)\n        self.bottom_axis.setZValue(self.dendrogram.zValue() + 10)\n        self.cut_line = SliderLine(self.top_axis,\n                                   orientation=Qt.Horizontal)\n        self.cut_line.valueChanged.connect(self._dendrogram_slider_changed)\n        self.dendrogram.geometryChanged.connect(self._dendrogram_geom_changed)\n        self._set_cut_line_visible(self.selection_method == 1)\n        self.__update_font_scale()\n\n    @Inputs.distances\n    def set_distances(self, matrix):\n        self.error()\n        self.Error.clear()\n\n        self.matrix = None\n        self.Error.clear()\n        if matrix is not None:\n            if len(matrix) < 2:\n                self.Error.empty_matrix()\n            elif not matrix.is_symmetric():\n                self.Error.not_symmetric()\n            elif not np.all(np.isfinite(matrix)):\n                self.Error.not_finite_distances()\n            else:\n                self.matrix = matrix\n\n    @Inputs.subset\n    def set_subset(self, subset):\n        self.subset = subset\n        self.controls.label_only_subset.setDisabled(subset is None)\n\n    def handleNewSignals(self):\n        if self.__pending_selection_restore is not None:\n            selection_state = self.__pending_selection_restore\n        else:\n            # save the current selection to (possibly) restore later\n            selection_state = self._save_selection()\n\n        matrix = self.matrix\n        if matrix is not None:\n            self._set_items(matrix.row_items, matrix.axis)\n        else:\n            self._set_items(None)\n        self._update()\n\n        # Can now attempt to restore session state from a saved workflow.\n        if self.root and selection_state is not None:\n            self._restore_selection(selection_state)\n            self.__pending_selection_restore = None\n\n        self.Warning.clear()\n        rows = set()\n        if self.subset:\n            subsetids = set(self.subset.ids)\n            if not isinstance(self.items, Orange.data.Table) \\\n                    or not self.matrix.axis:\n                self.Warning.subset_on_no_table()\n            elif (dataids := set(self.items.ids)) and not subsetids & dataids:\n                self.Warning.subset_wrong()\n            elif not subsetids <= dataids:\n                self.Warning.subset_not_subset()\n            else:\n                indices = [leaf.value.index for leaf in leaves(self.root)]\n                rows = {\n                    row for row, rowid in enumerate(self.items.ids[indices])\n                    if rowid in subsetids\n                }\n\n        self.subset_rows = rows\n        self._update_labels()\n        self.commit.now()\n\n    def _set_items(self, items, axis=1):\n        self.closeContext()\n        self.items = items\n        model = self.label_cb.model()\n        color_model = self.controls.color_by.model()\n        color_model.set_domain(None)\n        self.color_by = None\n        if len(model) == 2 and model[0] is None:\n            if model[1] == \"Name\":\n                self.annotation_if_names = self.annotation\n            if model[1] == self.basic_annotations[1]:\n                self.annotation_if_enumerate = self.annotation\n        if isinstance(items, Orange.data.Table) and axis:\n            metas_class = tuple(\n                filter_visible(chain(items.domain.metas,\n                                     items.domain.class_vars)))\n            visible_attrs = tuple(filter_visible(items.domain.attributes))\n            if not (metas_class or visible_attrs):\n                model[:] = self.basic_annotations\n            else:\n                model[:] = (\n                    (None, )\n                    + metas_class\n                    + (model.Separator, ) * bool(metas_class and visible_attrs)\n                    + visible_attrs)\n            for meta in items.domain.metas:\n                if isinstance(meta, StringVariable):\n                    self.annotation = meta\n                    break\n            else:\n                if items.domain.class_vars:\n                    # No string metas: show class\n                    self.annotation = items.domain.class_vars[0]\n                else:\n                    # No string metas and no class: show the first option\n                    # which is not None (in the worst case, Enumeration)\n                    self.annotation = model[1]\n\n            color_model.set_domain(items.domain)\n            if items.domain.class_vars:\n                self.color_by = items.domain.class_vars[0]\n            self.openContext(items.domain)\n        elif isinstance(items, Orange.data.Table) and not axis \\\n                or (isinstance(items, list) and \\\n                    all(isinstance(var, Orange.data.Variable)\n                        for var in items)):\n            model[:] = (None, \"Name\")\n            self.annotation = self.annotation_if_names\n        else:\n            model[:] = self.basic_annotations\n            self.annotation = self.annotation_if_enumerate\n\n        no_colors = len(color_model) == 1\n        self.controls.color_by.setDisabled(no_colors)\n        self.color_by_label.setDisabled(no_colors)\n\n    def _clear_plot(self):\n        self.dendrogram.set_root(None)\n        self.label_model.clear()\n\n    def _set_displayed_root(self, root):\n        self._clear_plot()\n        self._displayed_root = root\n        self.dendrogram.set_root(root)\n        self._update_labels()\n\n    def _update(self):\n        self._clear_plot()\n\n        distances = self.matrix\n\n        if distances is not None:\n            method = LINKAGE_ARGS[self.linkage]\n            Z = dist_matrix_linkage(distances, linkage=method)\n\n            tree = tree_from_linkage(Z)\n            self.linkmatrix = Z\n            self.root = tree\n\n            self.top_axis.setRange(tree.value.height, 0.0)\n            self.bottom_axis.setRange(tree.value.height, 0.0)\n\n            if self.pruning:\n                self._set_displayed_root(prune(tree, level=self.max_depth))\n            else:\n                self._set_displayed_root(tree)\n        else:\n            self.linkmatrix = None\n            self.root = None\n            self._set_displayed_root(None)\n\n        self._apply_selection()\n\n    def _update_labels(self):\n        if not hasattr(self, \"label_model\"):\n            # This method can be called during widget initialization when\n            # creating check box for label_only_subset, if it's value is\n            # initially True.\n            # See https://github.com/biolab/orange-widget-base/pull/213;\n            # if it's merged, this check can be removed.\n            return\n\n        self.Warning.pruning_disables_colors(\n            shown=self.pruning\n                  and (self.subset_rows or self.color_by is not None))\n        labels = []\n        if self.root and self._displayed_root:\n            indices = [leaf.value.index for leaf in leaves(self.root)]\n\n            if self.annotation is None:\n                if not self.pruning \\\n                        and self.subset_rows and self.color_by is None:\n                    # Model fails if number of labels and of colors mismatch\n                    labels = [\"\"] * len(indices)\n                else:\n                    labels = []\n            elif self.annotation == \"Enumeration\":\n                labels = [str(i+1) for i in indices]\n            elif self.annotation == \"Name\":\n                attr = self.matrix.row_items.domain.attributes\n                labels = [str(attr[i]) for i in indices]\n            elif isinstance(self.annotation, Orange.data.Variable):\n                col_data = self.items.get_column(self.annotation)\n                labels = [self.annotation.str_val(val) for val in col_data]\n                labels = [labels[idx] for idx in indices]\n            else:\n                labels = []\n            if not self.pruning and \\\n                    labels and self.label_only_subset and self.subset_rows:\n                labels = [label if row in self.subset_rows else \"\"\n                          for row, label in enumerate(labels)]\n\n            if labels and self._displayed_root is not self.root:\n                joined = leaves(self._displayed_root)\n                labels = [\", \".join(labels[leaf.value.first: leaf.value.last])\n                          for leaf in joined]\n\n        self.label_model[:] = labels\n        self.label_model.set_subset(set() if self.pruning else self.subset_rows)\n        self.labels.setMinimumWidth(1 if labels else -1)\n\n        if not self.pruning and self.color_by is not None:\n            col = self.items.get_column(self.color_by)\n            self.label_model.set_colors(\n                self.color_by.palette.values_to_qcolors(col[indices]))\n        else:\n            self.label_model.set_colors(None)\n\n    def _restore_selection(self, state):\n        # type: (SelectionState) -> bool\n        \"\"\"\n        Restore the (manual) node selection state.\n\n        Return True if successful; False otherwise.\n        \"\"\"\n        linkmatrix = self.linkmatrix\n        if self.selection_method == 0 and self.root:\n            selected, linksaved = state\n            linkstruct = np.array(linksaved, dtype=float)\n            selected = set(selected)  # type: Set[Tuple[int]]\n            if not selected:\n                return False\n            if linkmatrix.shape[0] != linkstruct.shape[0]:\n                return False\n            # check that the linkage matrix structure matches. Use isclose for\n            # the height column to account for inexact floating point math\n            # (e.g. summation order in different ?gemm implementations for\n            # euclidean distances, ...)\n            if np.any(linkstruct[:, :2] != linkmatrix[:, :2]) or \\\n                    not np.all(np.isclose(linkstruct[:, 2], linkstruct[:, 2])):\n                return False\n            selection = []\n            indices = np.array([n.value.index for n in leaves(self.root)],\n                               dtype=int)\n            # mapping from ranges to display (pruned) nodes\n            mapping = {node.value.range: node\n                       for node in postorder(self._displayed_root)}\n            for node in postorder(self.root):  # type: Tree\n                r = tuple(indices[node.value.first: node.value.last])\n                if r in selected:\n                    if node.value.range not in mapping:\n                        # the node was pruned from display and cannot be\n                        # selected\n                        break\n                    selection.append(mapping[node.value.range])\n                    selected.remove(r)\n                if not selected:\n                    break  # found all, nothing more to do\n            if selection and selected:\n                # Could not restore all selected nodes (only partial match)\n                return False\n\n            self._set_selected_nodes(selection)\n            return True\n        return False\n\n    def _set_selected_nodes(self, selection):\n        # type: (List[Tree]) -> None\n        \"\"\"\n        Set the nodes in `selection` to be the current selected nodes.\n\n        The selection nodes must be subtrees of the current `_displayed_root`.\n        \"\"\"\n        self.dendrogram.selectionChanged.disconnect(self._invalidate_output)\n        try:\n            self.dendrogram.set_selected_clusters(selection)\n        finally:\n            self.dendrogram.selectionChanged.connect(self._invalidate_output)\n\n    def _max_depth_return(self):\n        if self.pruning != 1:\n            self.pruning = 1\n            self._invalidate_pruning()\n\n    def _max_depth_changed(self):\n        self.pruning = 1\n        self._invalidate_pruning()\n\n    def _invalidate_clustering(self):\n        self._update()\n        self._update_labels()\n        self._invalidate_output()\n\n    def _invalidate_output(self):\n        self.commit.deferred()\n\n    def _invalidate_pruning(self):\n        if self.root:\n            selection = self.dendrogram.selected_nodes()\n            ranges = [node.value.range for node in selection]\n            if self.pruning:\n                self._set_displayed_root(\n                    prune(self.root, level=self.max_depth))\n            else:\n                self._set_displayed_root(self.root)\n            selected = [node for node in preorder(self._displayed_root)\n                        if node.value.range in ranges]\n\n            self.dendrogram.set_selected_clusters(selected)\n\n        self._apply_selection()\n\n    @gui.deferred\n    def commit(self):\n        items = getattr(self.matrix, \"items\", self.items)\n        self.Warning.many_clusters.clear()\n        if not items:\n            self.Outputs.selected_data.send(None)\n            self.Outputs.annotated_data.send(None)\n            return\n\n        selection = self.dendrogram.selected_nodes()\n        selection = sorted(selection, key=lambda c: c.value.first)\n\n        indices = [leaf.value.index for leaf in leaves(self.root)]\n\n        maps = [indices[node.value.first:node.value.last]\n                for node in selection]\n        if len(maps) > 20:\n            self.Warning.many_clusters()\n\n        selected_indices = list(chain(*maps))\n\n        if not selected_indices:\n            self.Outputs.selected_data.send(None)\n            annotated_data = lazy_annotated_table(items, []) \\\n                if self.selection_method == 0 and self.matrix.axis else None\n            self.Outputs.annotated_data.send(annotated_data)\n            return\n\n        selected_data = annotated_data = None\n\n        if isinstance(items, Orange.data.Table) and self.matrix.axis == 1:\n            # Select rows\n            data, domain = items, items.domain\n\n            c = np.full(self.matrix.shape[0], len(maps))\n            for i, indices in enumerate(maps):\n                c[indices] = i\n\n            clust_name = get_unique_names(domain, \"Cluster\")\n            values = [f\"C{i + 1}\" for i in range(len(maps))]\n\n            sel_clust_var = Orange.data.DiscreteVariable(\n                name=clust_name, values=values)\n            sel_domain = add_columns(domain, metas=(sel_clust_var,))\n            selected_data = LazyValue[Table](\n                lambda: items.add_column(\n                    sel_clust_var, c, to_metas=True)[c != len(maps)],\n                domain=sel_domain, length=len(selected_indices))\n\n            ann_clust_var = Orange.data.DiscreteVariable(\n                name=clust_name, values=values + [\"Other\"]\n            )\n            ann_domain = add_columns(\n                domain_with_annotation_column(data)[0], metas=(ann_clust_var, ))\n            annotated_data = LazyValue[Table](\n                lambda: create_annotated_table(\n                    data=items.add_column(ann_clust_var, c, to_metas=True),\n                    selected_indices=selected_indices),\n                domain=ann_domain, length=len(items)\n            )\n\n        elif isinstance(items, Orange.data.Table) and self.matrix.axis == 0:\n            # Select columns\n            attrs = []\n            unselected_indices = sorted(set(range(self.root.value.last)) -\n                                        set(selected_indices))\n            for clust, indices in chain(enumerate(maps, start=1),\n                                        [(0, unselected_indices)]):\n                for i in indices:\n                    attr = items.domain[i].copy()\n                    attr.attributes[\"cluster\"] = clust\n                    attrs.append(attr)\n            all_domain = Orange.data.Domain(\n                # len(unselected_indices) can be 0\n                attrs[:len(attrs) - len(unselected_indices)],\n                items.domain.class_vars, items.domain.metas)\n\n            selected_data = LazyValue[Table](\n                lambda: items.from_table(all_domain, items),\n                domain=all_domain, length=len(items))\n\n            sel_domain = Orange.data.Domain(\n                attrs,\n                items.domain.class_vars, items.domain.metas)\n            annotated_data = LazyValue[Table](\n                lambda: items.from_table(sel_domain, items),\n                domain=sel_domain, length=len(items))\n\n        self.Outputs.selected_data.send(selected_data)\n        self.Outputs.annotated_data.send(annotated_data)\n\n    @Slot(QPointF)\n    def _activate_cut_line(self, pos: QPointF):\n        \"\"\"Activate cut line selection an set cut value to `pos.x()`.\"\"\"\n        self.selection_method = 1\n        self.cut_line.setValue(pos.x())\n        self._selection_method_changed()\n\n    def onDeleteWidget(self):\n        super().onDeleteWidget()\n        self._clear_plot()\n        self.dendrogram.clear()\n        self.dendrogram.deleteLater()\n\n    def _dendrogram_geom_changed(self):\n        pos = self.dendrogram.pos_at_height(self.cutoff_height)\n        geom = self.dendrogram.geometry()\n        self._set_slider_value(pos.x(), geom.width())\n\n        self.cut_line.setLength(\n            self.bottom_axis.geometry().bottom()\n            - self.top_axis.geometry().top()\n        )\n\n        geom = self._main_graphics.geometry()\n        assert geom.topLeft() == QPointF(0, 0)\n\n        def adjustLeft(rect):\n            rect = QRectF(rect)\n            rect.setLeft(geom.left())\n            return rect\n        margin = 3\n        self.scene.setSceneRect(geom)\n        self.view.setSceneRect(geom)\n        self.view.setHeaderSceneRect(\n            adjustLeft(self.top_axis.geometry()).adjusted(0, 0, 0, margin)\n        )\n        self.view.setFooterSceneRect(\n            adjustLeft(self.bottom_axis.geometry()).adjusted(0, -margin, 0, 0)\n        )\n\n    def _dendrogram_slider_changed(self, value):\n        p = QPointF(value, 0)\n        cl_height = self.dendrogram.height_at(p)\n\n        self.set_cutoff_height(cl_height)\n\n    def _set_slider_value(self, value, span):\n        with blocked(self.cut_line):\n            self.cut_line.setRange(0, span)\n            self.cut_line.setValue(value)\n\n    def set_cutoff_height(self, height):\n        self.cutoff_height = height\n        if self.root:\n            self.cut_ratio = 100 * height / self.root.value.height\n        self.select_max_height(height)\n\n    def _set_cut_line_visible(self, visible):\n        self.cut_line.setVisible(visible)\n\n    def select_top_n(self, n):\n        root = self._displayed_root\n        if root:\n            clusters = top_clusters(root, n)\n            self.dendrogram.set_selected_clusters(clusters)\n\n    def select_max_height(self, height):\n        root = self._displayed_root\n        if root:\n            clusters = clusters_at_height(root, height)\n            self.dendrogram.set_selected_clusters(clusters)\n\n    def _cut_ratio_changed(self):\n        self.selection_method = 1\n        self._selection_method_changed()\n\n    def _cut_ratio_return(self):\n        if self.selection_method != 1:\n            self.selection_method = 1\n            self._selection_method_changed()\n\n    def _top_n_changed(self):\n        self.selection_method = 2\n        self._selection_method_changed()\n\n    def _top_n_return(self):\n        if self.selection_method != 2:\n            self.selection_method = 2\n            self._selection_method_changed()\n\n    def _selection_method_changed(self):\n        self._set_cut_line_visible(self.selection_method == 1)\n        if self.root:\n            self._apply_selection()\n\n    def _apply_selection(self):\n        if not self.root:\n            return\n\n        if self.selection_method == 0:\n            pass\n        elif self.selection_method == 1:\n            height = self.cut_ratio * self.root.value.height / 100\n            self.set_cutoff_height(height)\n            pos = self.dendrogram.pos_at_height(height)\n            self._set_slider_value(pos.x(), self.dendrogram.size().width())\n        elif self.selection_method == 2:\n            self.select_top_n(self.top_n)\n\n    def _selection_edited(self):\n        # Selection was edited by clicking on a cluster in the\n        # dendrogram view.\n        self.selection_method = 0\n        self._selection_method_changed()\n        self._invalidate_output()\n\n    def _save_selection(self):\n        # Save the current manual node selection state\n        selection_state = None\n        if self.selection_method == 0 and self.root:\n            assert self.linkmatrix is not None\n            linkmat = [(int(_0), int(_1), _2)\n                       for _0, _1, _2 in self.linkmatrix[:, :3].tolist()]\n            nodes_ = self.dendrogram.selected_nodes()\n            # match the display (pruned) nodes back (by ranges)\n            mapping = {node.value.range: node for node in postorder(self.root)}\n            nodes = [mapping[node.value.range] for node in nodes_]\n            indices = [tuple(node.value.index for node in leaves(node))\n                       for node in nodes]\n            if nodes:\n                selection_state = (indices, linkmat)\n        return selection_state\n\n    def save_state(self):\n        # type: () -> Dict[str, Any]\n        \"\"\"\n        Save state for `set_restore_state`\n        \"\"\"\n        selection = self._save_selection()\n        res = {\"version\": (0, 0, 0)}\n        if selection is not None:\n            res[\"selection_state\"] = selection\n        return res\n\n    def set_restore_state(self, state):\n        # type: (Dict[str, Any]) -> bool\n        \"\"\"\n        Restore session data from a saved state.\n\n        Parameters\n        ----------\n        state : Dict[str, Any]\n\n        NOTE\n        ----\n        This is method called while the instance (self) is being constructed,\n        even before its `__init__` is called. Consider `self` to be only a\n        `QObject` at this stage.\n        \"\"\"\n        if \"selection_state\" in state:\n            selection = state[\"selection_state\"]\n            self.__pending_selection_restore = selection\n        return True\n\n    def __zoom_in(self):\n        def clip(minval, maxval, val):\n            return min(max(val, minval), maxval)\n        self.zoom_factor = clip(self.zoom_slider.minimum(),\n                                self.zoom_slider.maximum(),\n                                self.zoom_factor + 1)\n        self.__update_font_scale()\n\n    def __zoom_out(self):\n        def clip(minval, maxval, val):\n            return min(max(val, minval), maxval)\n        self.zoom_factor = clip(self.zoom_slider.minimum(),\n                                self.zoom_slider.maximum(),\n                                self.zoom_factor - 1)\n        self.__update_font_scale()\n\n    def __zoom_reset(self):\n        self.zoom_factor = 0\n        self.__update_font_scale()\n\n    def __update_font_scale(self):\n        font = self.scene.font()\n        factor = (1.25 ** self.zoom_factor)\n        font = qfont_scaled(font, factor)\n        self._main_graphics.setFont(font)\n        self.label_model.setFont(font)\n\n    def send_report(self):\n        annot = self.label_cb.currentText()\n        if isinstance(self.annotation, str):\n            annot = annot.lower()\n        if self.selection_method == 0:\n            sel = \"manual\"\n        elif self.selection_method == 1:\n            sel = \"at {:.1f} of height\".format(self.cut_ratio)\n        else:\n            sel = f\"top {self.top_n} {pl(self.top_n, 'cluster')}\"\n        self.report_items((\n            (\"Linkage\", LINKAGE[self.linkage]),\n            (\"Annotation\", annot),\n            (\"Pruning\",\n             self.pruning != 0 and \"{} levels\".format(self.max_depth)),\n            (\"Selection\", sel),\n        ))\n        self.report_plot()\n\n    @classmethod\n    def migrate_context(cls, context, version):\n        if version < 2:\n            if context.values[\"annotation\"] == \"None\":\n                context.values[\"annotation\"] = None",
                                "ast_type_str": "ClassDef",
                                "start_lineno": 177,
                                "end_lineno": 1074,
                                "parent": null
                            }
                        }
                    ],
                    "added_entities": [],
                    "deleted_entities": []
                },
                {
                    "descriptor": {
                        "old_range": {
                            "start": 778,
                            "length": 6
                        },
                        "new_range": {
                            "start": 782,
                            "length": 8
                        },
                        "section": "class OWHierarchicalClustering(widget.OWWidget):"
                    },
                    "line_group": {
                        "all_lines": [
                            {
                                "content": "",
                                "type": "context"
                            },
                            {
                                "content": "        maps = [indices[node.value.first:node.value.last]",
                                "type": "context"
                            },
                            {
                                "content": "                for node in selection]",
                                "type": "context"
                            },
                            {
                                "content": "        if len(maps) > 20:",
                                "type": "added"
                            },
                            {
                                "content": "            self.Warning.many_clusters()",
                                "type": "added"
                            },
                            {
                                "content": "",
                                "type": "context"
                            },
                            {
                                "content": "        selected_indices = list(chain(*maps))",
                                "type": "context"
                            },
                            {
                                "content": "",
                                "type": "context"
                            }
                        ]
                    },
                    "modified_entities": [
                        {
                            "file_name": "Orange/widgets/unsupervised/owhierarchicalclustering.py",
                            "type": "class",
                            "name": "OWHierarchicalClustering",
                            "content": "class OWHierarchicalClustering(widget.OWWidget):\n    name = \"Hierarchical Clustering\"\n    description = \"Display a dendrogram of a hierarchical clustering \" \\\n                  \"constructed from the input distance matrix.\"\n    icon = \"icons/HierarchicalClustering.svg\"\n    priority = 2100\n    keywords = \"hierarchical clustering\"\n\n    class Inputs:\n        distances = Input(\"Distances\", Orange.misc.DistMatrix)\n        subset = Input(\"Data Subset\", Orange.data.Table, explicit=True)\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    settings_version = 2\n    settingsHandler = _DomainContextHandler()\n\n    #: Selected linkage\n    linkage = settings.Setting(LINKAGE.index(DEFAULT_LINKAGE))\n    #: Index of the selected annotation item (variable, ...)\n    annotation = settings.ContextSetting(\"Enumeration\")\n    #: Out-of-context setting for the case when the \"Name\" option is available\n    annotation_if_names = settings.Setting(\"Name\")\n    #: Out-of-context setting for the case with just \"Enumeration\" and \"None\"\n    annotation_if_enumerate = settings.Setting(\"Enumeration\")\n    #: Selected tree pruning (none/max depth)\n    pruning = settings.Setting(0)\n    #: Maximum depth when max depth pruning is selected\n    max_depth = settings.Setting(10)\n\n    #: Selected cluster selection method (none, cut distance, top n)\n    selection_method = settings.Setting(0)\n    #: Cut height ratio wrt root height\n    cut_ratio = settings.Setting(75.0)\n    #: Number of top clusters to select\n    top_n = settings.Setting(3)\n    #: Dendrogram zoom factor\n    zoom_factor = settings.Setting(0)\n    #: Show labels only for subset (if present)\n    label_only_subset = settings.Setting(False)\n    #: Color for label decoration\n    color_by: Union[DiscreteVariable, ContinuousVariable, None] = \\\n        settings.ContextSetting(None)\n\n    autocommit = settings.Setting(True)\n\n    graph_name = \"scene\"  # QGraphicsScene\n\n    basic_annotations = [None, \"Enumeration\"]\n\n    class Error(widget.OWWidget.Error):\n        empty_matrix = Msg(\"Distance matrix is empty.\")\n        not_finite_distances = Msg(\"Some distances are infinite\")\n        not_symmetric = widget.Msg(\"Distance matrix is not symmetric.\")\n\n    class Warning(widget.OWWidget.Warning):\n        subset_on_no_table = \\\n            Msg(\"Unused data subset: distances do not refer to data instances\")\n        subset_not_subset = \\\n            Msg(\"Some data from the subset does not appear in distance matrix\")\n        subset_wrong = \\\n            Msg(\"Subset data refers to a different table\")\n        pruning_disables_colors = \\\n            Msg(\"Pruned cluster doesn't show colors and indicate subset\")\n        many_clusters = \\\n            Msg(\"Variables with too many values may \"\n                \"degrade the performance of downstream widgets.\")\n\n    #: Stored (manual) selection state (from a saved workflow) to restore.\n    __pending_selection_restore = None  # type: Optional[SelectionState]\n\n    def __init__(self):\n        super().__init__()\n\n        self.matrix = None\n        self.items = None\n        self.subset = None\n        self.subset_rows = set()\n        self.linkmatrix = None\n        self.root = None\n        self._displayed_root = None\n        self.cutoff_height = 0.0\n\n        spin_width = QFontMetrics(self.font()).horizontalAdvance(\"M\" * 7)\n        gui.comboBox(\n            self.controlArea, self, \"linkage\", items=LINKAGE, box=\"Linkage\",\n            callback=self._invalidate_clustering)\n\n        model = itemmodels.VariableListModel(placeholder=\"None\")\n        model[:] = self.basic_annotations\n\n        grid = QGridLayout()\n        gui.widgetBox(self.controlArea, \"Annotations\", orientation=grid)\n        self.label_cb = cb = combobox.ComboBoxSearch(\n            minimumContentsLength=14,\n            sizeAdjustPolicy=QComboBox.AdjustToMinimumContentsLengthWithIcon\n        )\n        cb.setModel(model)\n        cb.setCurrentIndex(cb.findData(self.annotation, Qt.EditRole))\n\n        def on_annotation_activated():\n            self.annotation = self.label_cb.currentData(Qt.EditRole)\n            self._update_labels()\n        cb.activated.connect(on_annotation_activated)\n\n        def on_annotation_changed(value):\n            self.label_cb.setCurrentIndex(\n                self.label_cb.findData(value, Qt.EditRole))\n        self.connect_control(\"annotation\", on_annotation_changed)\n\n        grid.addWidget(self.label_cb, 0, 0, 1, 2)\n\n        cb = gui.checkBox(\n            None, self, \"label_only_subset\", \"Show labels only for subset\",\n            disabled=True,\n            callback=self._update_labels, stateWhenDisabled=False)\n        grid.addWidget(cb, 1, 0, 1, 2)\n\n        model = itemmodels.DomainModel(\n            valid_types=(DiscreteVariable, ContinuousVariable),\n            placeholder=\"None\")\n        cb = gui.comboBox(\n            None, self, \"color_by\", orientation=Qt.Horizontal,\n            model=model, callback=self._update_labels,\n            sizePolicy=QSizePolicy(QSizePolicy.MinimumExpanding,\n                                   QSizePolicy.Fixed))\n        self.color_by_label = QLabel(\"Color by:\")\n        grid.addWidget(self.color_by_label, 2, 0)\n        grid.addWidget(cb, 2, 1)\n\n        box = gui.radioButtons(\n            self.controlArea, self, \"pruning\", box=\"Pruning\",\n            callback=self._invalidate_pruning)\n        grid = QGridLayout()\n        box.layout().addLayout(grid)\n        grid.addWidget(\n            gui.appendRadioButton(box, \"None\", addToLayout=False),\n            0, 0\n        )\n        self.max_depth_spin = gui.spin(\n            box, self, \"max_depth\", minv=1, maxv=100,\n            controlWidth=spin_width, alignment=Qt.AlignRight,\n            callback=self._max_depth_changed,\n            keyboardTracking=False, addToLayout=False\n        )\n        self.max_depth_spin.lineEdit().returnPressed.connect(\n            self._max_depth_return)\n\n        grid.addWidget(\n            gui.appendRadioButton(box, \"Max depth:\", addToLayout=False),\n            1, 0)\n        grid.addWidget(self.max_depth_spin, 1, 1)\n\n        self.selection_box = gui.radioButtons(\n            self.controlArea, self, \"selection_method\",\n            box=\"Selection\",\n            callback=self._selection_method_changed)\n\n        grid = QGridLayout()\n        self.selection_box.layout().addLayout(grid)\n        grid.addWidget(\n            gui.appendRadioButton(\n                self.selection_box, \"Manual\", addToLayout=False),\n            0, 0\n        )\n        grid.addWidget(\n            gui.appendRadioButton(\n                self.selection_box, \"Height ratio:\", addToLayout=False),\n            1, 0\n        )\n        self.cut_ratio_spin = gui.spin(\n            self.selection_box, self, \"cut_ratio\", 0, 100, step=1e-1,\n            controlWidth=spin_width, alignment = Qt.AlignRight,\n            spinType=float, callback=self._cut_ratio_changed,\n            addToLayout=False\n        )\n        self.cut_ratio_spin.setSuffix(\" %\")\n        self.cut_ratio_spin.lineEdit().returnPressed.connect(\n            self._cut_ratio_return)\n\n        grid.addWidget(self.cut_ratio_spin, 1, 1)\n\n        grid.addWidget(\n            gui.appendRadioButton(\n                self.selection_box, \"Top N:\", addToLayout=False),\n            2, 0\n        )\n        self.top_n_spin = gui.spin(\n            self.selection_box, self, \"top_n\", 1, 1000,\n            controlWidth=spin_width, alignment=Qt.AlignRight,\n            callback=self._top_n_changed, addToLayout=False)\n        self.top_n_spin.lineEdit().returnPressed.connect(self._top_n_return)\n        grid.addWidget(self.top_n_spin, 2, 1)\n\n        self.zoom_slider = gui.hSlider(\n            self.controlArea, self, \"zoom_factor\", box=\"Zoom\",\n            minValue=-6, maxValue=3, step=1, ticks=True, createLabel=False,\n            callback=self.__update_font_scale)\n\n        zoom_in = QAction(\n            \"Zoom in\", self, shortcut=QKeySequence.ZoomIn,\n            triggered=self.__zoom_in\n        )\n        zoom_out = QAction(\n            \"Zoom out\", self, shortcut=QKeySequence.ZoomOut,\n            triggered=self.__zoom_out\n        )\n        zoom_reset = QAction(\n            \"Reset zoom\", self,\n            shortcut=QKeySequence(Qt.ControlModifier | Qt.Key_0),\n            triggered=self.__zoom_reset\n        )\n        self.addActions([zoom_in, zoom_out, zoom_reset])\n\n        self.controlArea.layout().addStretch()\n\n        gui.auto_send(self.buttonsArea, self, \"autocommit\")\n\n        self.scene = QGraphicsScene(self)\n        self.view = GraphicsView(\n            self.scene,\n            horizontalScrollBarPolicy=Qt.ScrollBarAlwaysOff,\n            verticalScrollBarPolicy=Qt.ScrollBarAlwaysOn,\n            alignment=Qt.AlignLeft | Qt.AlignVCenter,\n            widgetResizable=True,\n        )\n        # Disable conflicting action shortcuts. We define our own.\n        for a in self.view.viewActions():\n            a.setEnabled(False)\n        self.mainArea.layout().setSpacing(1)\n        self.mainArea.layout().addWidget(self.view)\n\n        def axis_view(orientation):\n            ax = AxisItem(orientation=orientation, maxTickLength=7)\n            ax.mousePressed.connect(self._activate_cut_line)\n            ax.mouseMoved.connect(self._activate_cut_line)\n            ax.mouseReleased.connect(self._activate_cut_line)\n            ax.setRange(1.0, 0.0)\n            return ax\n\n        self.top_axis = axis_view(\"top\")\n        self.bottom_axis = axis_view(\"bottom\")\n\n        self._main_graphics = QGraphicsWidget(\n            sizePolicy=QSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.Preferred)\n        )\n        scenelayout = QGraphicsGridLayout()\n        scenelayout.setHorizontalSpacing(10)\n        scenelayout.setVerticalSpacing(10)\n\n        self._main_graphics.setLayout(scenelayout)\n        self.scene.addItem(self._main_graphics)\n        self.view.setCentralWidget(self._main_graphics)\n        self.scene.addItem(self._main_graphics)\n\n        self.dendrogram = DendrogramWidget(pen_width=2)\n        self.dendrogram.setSizePolicy(QSizePolicy.MinimumExpanding,\n                                      QSizePolicy.MinimumExpanding)\n        self.dendrogram.selectionChanged.connect(self._invalidate_output)\n        self.dendrogram.selectionEdited.connect(self._selection_edited)\n\n        self.labels = TextListView()\n        self.label_model = SelectedLabelsModel()\n        self.labels.setModel(self.label_model)\n        self.labels.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Preferred)\n        self.labels.setAlignment(Qt.AlignLeft)\n        self.labels.setMaximumWidth(200)\n\n        scenelayout.addItem(self.top_axis, 0, 0,\n                            alignment=Qt.AlignLeft | Qt.AlignVCenter)\n        scenelayout.addItem(self.dendrogram, 1, 0,\n                            alignment=Qt.AlignLeft | Qt.AlignVCenter)\n        scenelayout.addItem(self.labels, 1, 1,\n                            alignment=Qt.AlignLeft | Qt.AlignVCenter)\n        scenelayout.addItem(self.bottom_axis, 2, 0,\n                            alignment=Qt.AlignLeft | Qt.AlignVCenter)\n        self.top_axis.setZValue(self.dendrogram.zValue() + 10)\n        self.bottom_axis.setZValue(self.dendrogram.zValue() + 10)\n        self.cut_line = SliderLine(self.top_axis,\n                                   orientation=Qt.Horizontal)\n        self.cut_line.valueChanged.connect(self._dendrogram_slider_changed)\n        self.dendrogram.geometryChanged.connect(self._dendrogram_geom_changed)\n        self._set_cut_line_visible(self.selection_method == 1)\n        self.__update_font_scale()\n\n    @Inputs.distances\n    def set_distances(self, matrix):\n        self.error()\n        self.Error.clear()\n\n        self.matrix = None\n        self.Error.clear()\n        if matrix is not None:\n            if len(matrix) < 2:\n                self.Error.empty_matrix()\n            elif not matrix.is_symmetric():\n                self.Error.not_symmetric()\n            elif not np.all(np.isfinite(matrix)):\n                self.Error.not_finite_distances()\n            else:\n                self.matrix = matrix\n\n    @Inputs.subset\n    def set_subset(self, subset):\n        self.subset = subset\n        self.controls.label_only_subset.setDisabled(subset is None)\n\n    def handleNewSignals(self):\n        if self.__pending_selection_restore is not None:\n            selection_state = self.__pending_selection_restore\n        else:\n            # save the current selection to (possibly) restore later\n            selection_state = self._save_selection()\n\n        matrix = self.matrix\n        if matrix is not None:\n            self._set_items(matrix.row_items, matrix.axis)\n        else:\n            self._set_items(None)\n        self._update()\n\n        # Can now attempt to restore session state from a saved workflow.\n        if self.root and selection_state is not None:\n            self._restore_selection(selection_state)\n            self.__pending_selection_restore = None\n\n        self.Warning.clear()\n        rows = set()\n        if self.subset:\n            subsetids = set(self.subset.ids)\n            if not isinstance(self.items, Orange.data.Table) \\\n                    or not self.matrix.axis:\n                self.Warning.subset_on_no_table()\n            elif (dataids := set(self.items.ids)) and not subsetids & dataids:\n                self.Warning.subset_wrong()\n            elif not subsetids <= dataids:\n                self.Warning.subset_not_subset()\n            else:\n                indices = [leaf.value.index for leaf in leaves(self.root)]\n                rows = {\n                    row for row, rowid in enumerate(self.items.ids[indices])\n                    if rowid in subsetids\n                }\n\n        self.subset_rows = rows\n        self._update_labels()\n        self.commit.now()\n\n    def _set_items(self, items, axis=1):\n        self.closeContext()\n        self.items = items\n        model = self.label_cb.model()\n        color_model = self.controls.color_by.model()\n        color_model.set_domain(None)\n        self.color_by = None\n        if len(model) == 2 and model[0] is None:\n            if model[1] == \"Name\":\n                self.annotation_if_names = self.annotation\n            if model[1] == self.basic_annotations[1]:\n                self.annotation_if_enumerate = self.annotation\n        if isinstance(items, Orange.data.Table) and axis:\n            metas_class = tuple(\n                filter_visible(chain(items.domain.metas,\n                                     items.domain.class_vars)))\n            visible_attrs = tuple(filter_visible(items.domain.attributes))\n            if not (metas_class or visible_attrs):\n                model[:] = self.basic_annotations\n            else:\n                model[:] = (\n                    (None, )\n                    + metas_class\n                    + (model.Separator, ) * bool(metas_class and visible_attrs)\n                    + visible_attrs)\n            for meta in items.domain.metas:\n                if isinstance(meta, StringVariable):\n                    self.annotation = meta\n                    break\n            else:\n                if items.domain.class_vars:\n                    # No string metas: show class\n                    self.annotation = items.domain.class_vars[0]\n                else:\n                    # No string metas and no class: show the first option\n                    # which is not None (in the worst case, Enumeration)\n                    self.annotation = model[1]\n\n            color_model.set_domain(items.domain)\n            if items.domain.class_vars:\n                self.color_by = items.domain.class_vars[0]\n            self.openContext(items.domain)\n        elif isinstance(items, Orange.data.Table) and not axis \\\n                or (isinstance(items, list) and \\\n                    all(isinstance(var, Orange.data.Variable)\n                        for var in items)):\n            model[:] = (None, \"Name\")\n            self.annotation = self.annotation_if_names\n        else:\n            model[:] = self.basic_annotations\n            self.annotation = self.annotation_if_enumerate\n\n        no_colors = len(color_model) == 1\n        self.controls.color_by.setDisabled(no_colors)\n        self.color_by_label.setDisabled(no_colors)\n\n    def _clear_plot(self):\n        self.dendrogram.set_root(None)\n        self.label_model.clear()\n\n    def _set_displayed_root(self, root):\n        self._clear_plot()\n        self._displayed_root = root\n        self.dendrogram.set_root(root)\n        self._update_labels()\n\n    def _update(self):\n        self._clear_plot()\n\n        distances = self.matrix\n\n        if distances is not None:\n            method = LINKAGE_ARGS[self.linkage]\n            Z = dist_matrix_linkage(distances, linkage=method)\n\n            tree = tree_from_linkage(Z)\n            self.linkmatrix = Z\n            self.root = tree\n\n            self.top_axis.setRange(tree.value.height, 0.0)\n            self.bottom_axis.setRange(tree.value.height, 0.0)\n\n            if self.pruning:\n                self._set_displayed_root(prune(tree, level=self.max_depth))\n            else:\n                self._set_displayed_root(tree)\n        else:\n            self.linkmatrix = None\n            self.root = None\n            self._set_displayed_root(None)\n\n        self._apply_selection()\n\n    def _update_labels(self):\n        if not hasattr(self, \"label_model\"):\n            # This method can be called during widget initialization when\n            # creating check box for label_only_subset, if it's value is\n            # initially True.\n            # See https://github.com/biolab/orange-widget-base/pull/213;\n            # if it's merged, this check can be removed.\n            return\n\n        self.Warning.pruning_disables_colors(\n            shown=self.pruning\n                  and (self.subset_rows or self.color_by is not None))\n        labels = []\n        if self.root and self._displayed_root:\n            indices = [leaf.value.index for leaf in leaves(self.root)]\n\n            if self.annotation is None:\n                if not self.pruning \\\n                        and self.subset_rows and self.color_by is None:\n                    # Model fails if number of labels and of colors mismatch\n                    labels = [\"\"] * len(indices)\n                else:\n                    labels = []\n            elif self.annotation == \"Enumeration\":\n                labels = [str(i+1) for i in indices]\n            elif self.annotation == \"Name\":\n                attr = self.matrix.row_items.domain.attributes\n                labels = [str(attr[i]) for i in indices]\n            elif isinstance(self.annotation, Orange.data.Variable):\n                col_data = self.items.get_column(self.annotation)\n                labels = [self.annotation.str_val(val) for val in col_data]\n                labels = [labels[idx] for idx in indices]\n            else:\n                labels = []\n            if not self.pruning and \\\n                    labels and self.label_only_subset and self.subset_rows:\n                labels = [label if row in self.subset_rows else \"\"\n                          for row, label in enumerate(labels)]\n\n            if labels and self._displayed_root is not self.root:\n                joined = leaves(self._displayed_root)\n                labels = [\", \".join(labels[leaf.value.first: leaf.value.last])\n                          for leaf in joined]\n\n        self.label_model[:] = labels\n        self.label_model.set_subset(set() if self.pruning else self.subset_rows)\n        self.labels.setMinimumWidth(1 if labels else -1)\n\n        if not self.pruning and self.color_by is not None:\n            col = self.items.get_column(self.color_by)\n            self.label_model.set_colors(\n                self.color_by.palette.values_to_qcolors(col[indices]))\n        else:\n            self.label_model.set_colors(None)\n\n    def _restore_selection(self, state):\n        # type: (SelectionState) -> bool\n        \"\"\"\n        Restore the (manual) node selection state.\n\n        Return True if successful; False otherwise.\n        \"\"\"\n        linkmatrix = self.linkmatrix\n        if self.selection_method == 0 and self.root:\n            selected, linksaved = state\n            linkstruct = np.array(linksaved, dtype=float)\n            selected = set(selected)  # type: Set[Tuple[int]]\n            if not selected:\n                return False\n            if linkmatrix.shape[0] != linkstruct.shape[0]:\n                return False\n            # check that the linkage matrix structure matches. Use isclose for\n            # the height column to account for inexact floating point math\n            # (e.g. summation order in different ?gemm implementations for\n            # euclidean distances, ...)\n            if np.any(linkstruct[:, :2] != linkmatrix[:, :2]) or \\\n                    not np.all(np.isclose(linkstruct[:, 2], linkstruct[:, 2])):\n                return False\n            selection = []\n            indices = np.array([n.value.index for n in leaves(self.root)],\n                               dtype=int)\n            # mapping from ranges to display (pruned) nodes\n            mapping = {node.value.range: node\n                       for node in postorder(self._displayed_root)}\n            for node in postorder(self.root):  # type: Tree\n                r = tuple(indices[node.value.first: node.value.last])\n                if r in selected:\n                    if node.value.range not in mapping:\n                        # the node was pruned from display and cannot be\n                        # selected\n                        break\n                    selection.append(mapping[node.value.range])\n                    selected.remove(r)\n                if not selected:\n                    break  # found all, nothing more to do\n            if selection and selected:\n                # Could not restore all selected nodes (only partial match)\n                return False\n\n            self._set_selected_nodes(selection)\n            return True\n        return False\n\n    def _set_selected_nodes(self, selection):\n        # type: (List[Tree]) -> None\n        \"\"\"\n        Set the nodes in `selection` to be the current selected nodes.\n\n        The selection nodes must be subtrees of the current `_displayed_root`.\n        \"\"\"\n        self.dendrogram.selectionChanged.disconnect(self._invalidate_output)\n        try:\n            self.dendrogram.set_selected_clusters(selection)\n        finally:\n            self.dendrogram.selectionChanged.connect(self._invalidate_output)\n\n    def _max_depth_return(self):\n        if self.pruning != 1:\n            self.pruning = 1\n            self._invalidate_pruning()\n\n    def _max_depth_changed(self):\n        self.pruning = 1\n        self._invalidate_pruning()\n\n    def _invalidate_clustering(self):\n        self._update()\n        self._update_labels()\n        self._invalidate_output()\n\n    def _invalidate_output(self):\n        self.commit.deferred()\n\n    def _invalidate_pruning(self):\n        if self.root:\n            selection = self.dendrogram.selected_nodes()\n            ranges = [node.value.range for node in selection]\n            if self.pruning:\n                self._set_displayed_root(\n                    prune(self.root, level=self.max_depth))\n            else:\n                self._set_displayed_root(self.root)\n            selected = [node for node in preorder(self._displayed_root)\n                        if node.value.range in ranges]\n\n            self.dendrogram.set_selected_clusters(selected)\n\n        self._apply_selection()\n\n    @gui.deferred\n    def commit(self):\n        items = getattr(self.matrix, \"items\", self.items)\n        self.Warning.many_clusters.clear()\n        if not items:\n            self.Outputs.selected_data.send(None)\n            self.Outputs.annotated_data.send(None)\n            return\n\n        selection = self.dendrogram.selected_nodes()\n        selection = sorted(selection, key=lambda c: c.value.first)\n\n        indices = [leaf.value.index for leaf in leaves(self.root)]\n\n        maps = [indices[node.value.first:node.value.last]\n                for node in selection]\n        if len(maps) > 20:\n            self.Warning.many_clusters()\n\n        selected_indices = list(chain(*maps))\n\n        if not selected_indices:\n            self.Outputs.selected_data.send(None)\n            annotated_data = lazy_annotated_table(items, []) \\\n                if self.selection_method == 0 and self.matrix.axis else None\n            self.Outputs.annotated_data.send(annotated_data)\n            return\n\n        selected_data = annotated_data = None\n\n        if isinstance(items, Orange.data.Table) and self.matrix.axis == 1:\n            # Select rows\n            data, domain = items, items.domain\n\n            c = np.full(self.matrix.shape[0], len(maps))\n            for i, indices in enumerate(maps):\n                c[indices] = i\n\n            clust_name = get_unique_names(domain, \"Cluster\")\n            values = [f\"C{i + 1}\" for i in range(len(maps))]\n\n            sel_clust_var = Orange.data.DiscreteVariable(\n                name=clust_name, values=values)\n            sel_domain = add_columns(domain, metas=(sel_clust_var,))\n            selected_data = LazyValue[Table](\n                lambda: items.add_column(\n                    sel_clust_var, c, to_metas=True)[c != len(maps)],\n                domain=sel_domain, length=len(selected_indices))\n\n            ann_clust_var = Orange.data.DiscreteVariable(\n                name=clust_name, values=values + [\"Other\"]\n            )\n            ann_domain = add_columns(\n                domain_with_annotation_column(data)[0], metas=(ann_clust_var, ))\n            annotated_data = LazyValue[Table](\n                lambda: create_annotated_table(\n                    data=items.add_column(ann_clust_var, c, to_metas=True),\n                    selected_indices=selected_indices),\n                domain=ann_domain, length=len(items)\n            )\n\n        elif isinstance(items, Orange.data.Table) and self.matrix.axis == 0:\n            # Select columns\n            attrs = []\n            unselected_indices = sorted(set(range(self.root.value.last)) -\n                                        set(selected_indices))\n            for clust, indices in chain(enumerate(maps, start=1),\n                                        [(0, unselected_indices)]):\n                for i in indices:\n                    attr = items.domain[i].copy()\n                    attr.attributes[\"cluster\"] = clust\n                    attrs.append(attr)\n            all_domain = Orange.data.Domain(\n                # len(unselected_indices) can be 0\n                attrs[:len(attrs) - len(unselected_indices)],\n                items.domain.class_vars, items.domain.metas)\n\n            selected_data = LazyValue[Table](\n                lambda: items.from_table(all_domain, items),\n                domain=all_domain, length=len(items))\n\n            sel_domain = Orange.data.Domain(\n                attrs,\n                items.domain.class_vars, items.domain.metas)\n            annotated_data = LazyValue[Table](\n                lambda: items.from_table(sel_domain, items),\n                domain=sel_domain, length=len(items))\n\n        self.Outputs.selected_data.send(selected_data)\n        self.Outputs.annotated_data.send(annotated_data)\n\n    @Slot(QPointF)\n    def _activate_cut_line(self, pos: QPointF):\n        \"\"\"Activate cut line selection an set cut value to `pos.x()`.\"\"\"\n        self.selection_method = 1\n        self.cut_line.setValue(pos.x())\n        self._selection_method_changed()\n\n    def onDeleteWidget(self):\n        super().onDeleteWidget()\n        self._clear_plot()\n        self.dendrogram.clear()\n        self.dendrogram.deleteLater()\n\n    def _dendrogram_geom_changed(self):\n        pos = self.dendrogram.pos_at_height(self.cutoff_height)\n        geom = self.dendrogram.geometry()\n        self._set_slider_value(pos.x(), geom.width())\n\n        self.cut_line.setLength(\n            self.bottom_axis.geometry().bottom()\n            - self.top_axis.geometry().top()\n        )\n\n        geom = self._main_graphics.geometry()\n        assert geom.topLeft() == QPointF(0, 0)\n\n        def adjustLeft(rect):\n            rect = QRectF(rect)\n            rect.setLeft(geom.left())\n            return rect\n        margin = 3\n        self.scene.setSceneRect(geom)\n        self.view.setSceneRect(geom)\n        self.view.setHeaderSceneRect(\n            adjustLeft(self.top_axis.geometry()).adjusted(0, 0, 0, margin)\n        )\n        self.view.setFooterSceneRect(\n            adjustLeft(self.bottom_axis.geometry()).adjusted(0, -margin, 0, 0)\n        )\n\n    def _dendrogram_slider_changed(self, value):\n        p = QPointF(value, 0)\n        cl_height = self.dendrogram.height_at(p)\n\n        self.set_cutoff_height(cl_height)\n\n    def _set_slider_value(self, value, span):\n        with blocked(self.cut_line):\n            self.cut_line.setRange(0, span)\n            self.cut_line.setValue(value)\n\n    def set_cutoff_height(self, height):\n        self.cutoff_height = height\n        if self.root:\n            self.cut_ratio = 100 * height / self.root.value.height\n        self.select_max_height(height)\n\n    def _set_cut_line_visible(self, visible):\n        self.cut_line.setVisible(visible)\n\n    def select_top_n(self, n):\n        root = self._displayed_root\n        if root:\n            clusters = top_clusters(root, n)\n            self.dendrogram.set_selected_clusters(clusters)\n\n    def select_max_height(self, height):\n        root = self._displayed_root\n        if root:\n            clusters = clusters_at_height(root, height)\n            self.dendrogram.set_selected_clusters(clusters)\n\n    def _cut_ratio_changed(self):\n        self.selection_method = 1\n        self._selection_method_changed()\n\n    def _cut_ratio_return(self):\n        if self.selection_method != 1:\n            self.selection_method = 1\n            self._selection_method_changed()\n\n    def _top_n_changed(self):\n        self.selection_method = 2\n        self._selection_method_changed()\n\n    def _top_n_return(self):\n        if self.selection_method != 2:\n            self.selection_method = 2\n            self._selection_method_changed()\n\n    def _selection_method_changed(self):\n        self._set_cut_line_visible(self.selection_method == 1)\n        if self.root:\n            self._apply_selection()\n\n    def _apply_selection(self):\n        if not self.root:\n            return\n\n        if self.selection_method == 0:\n            pass\n        elif self.selection_method == 1:\n            height = self.cut_ratio * self.root.value.height / 100\n            self.set_cutoff_height(height)\n            pos = self.dendrogram.pos_at_height(height)\n            self._set_slider_value(pos.x(), self.dendrogram.size().width())\n        elif self.selection_method == 2:\n            self.select_top_n(self.top_n)\n\n    def _selection_edited(self):\n        # Selection was edited by clicking on a cluster in the\n        # dendrogram view.\n        self.selection_method = 0\n        self._selection_method_changed()\n        self._invalidate_output()\n\n    def _save_selection(self):\n        # Save the current manual node selection state\n        selection_state = None\n        if self.selection_method == 0 and self.root:\n            assert self.linkmatrix is not None\n            linkmat = [(int(_0), int(_1), _2)\n                       for _0, _1, _2 in self.linkmatrix[:, :3].tolist()]\n            nodes_ = self.dendrogram.selected_nodes()\n            # match the display (pruned) nodes back (by ranges)\n            mapping = {node.value.range: node for node in postorder(self.root)}\n            nodes = [mapping[node.value.range] for node in nodes_]\n            indices = [tuple(node.value.index for node in leaves(node))\n                       for node in nodes]\n            if nodes:\n                selection_state = (indices, linkmat)\n        return selection_state\n\n    def save_state(self):\n        # type: () -> Dict[str, Any]\n        \"\"\"\n        Save state for `set_restore_state`\n        \"\"\"\n        selection = self._save_selection()\n        res = {\"version\": (0, 0, 0)}\n        if selection is not None:\n            res[\"selection_state\"] = selection\n        return res\n\n    def set_restore_state(self, state):\n        # type: (Dict[str, Any]) -> bool\n        \"\"\"\n        Restore session data from a saved state.\n\n        Parameters\n        ----------\n        state : Dict[str, Any]\n\n        NOTE\n        ----\n        This is method called while the instance (self) is being constructed,\n        even before its `__init__` is called. Consider `self` to be only a\n        `QObject` at this stage.\n        \"\"\"\n        if \"selection_state\" in state:\n            selection = state[\"selection_state\"]\n            self.__pending_selection_restore = selection\n        return True\n\n    def __zoom_in(self):\n        def clip(minval, maxval, val):\n            return min(max(val, minval), maxval)\n        self.zoom_factor = clip(self.zoom_slider.minimum(),\n                                self.zoom_slider.maximum(),\n                                self.zoom_factor + 1)\n        self.__update_font_scale()\n\n    def __zoom_out(self):\n        def clip(minval, maxval, val):\n            return min(max(val, minval), maxval)\n        self.zoom_factor = clip(self.zoom_slider.minimum(),\n                                self.zoom_slider.maximum(),\n                                self.zoom_factor - 1)\n        self.__update_font_scale()\n\n    def __zoom_reset(self):\n        self.zoom_factor = 0\n        self.__update_font_scale()\n\n    def __update_font_scale(self):\n        font = self.scene.font()\n        factor = (1.25 ** self.zoom_factor)\n        font = qfont_scaled(font, factor)\n        self._main_graphics.setFont(font)\n        self.label_model.setFont(font)\n\n    def send_report(self):\n        annot = self.label_cb.currentText()\n        if isinstance(self.annotation, str):\n            annot = annot.lower()\n        if self.selection_method == 0:\n            sel = \"manual\"\n        elif self.selection_method == 1:\n            sel = \"at {:.1f} of height\".format(self.cut_ratio)\n        else:\n            sel = f\"top {self.top_n} {pl(self.top_n, 'cluster')}\"\n        self.report_items((\n            (\"Linkage\", LINKAGE[self.linkage]),\n            (\"Annotation\", annot),\n            (\"Pruning\",\n             self.pruning != 0 and \"{} levels\".format(self.max_depth)),\n            (\"Selection\", sel),\n        ))\n        self.report_plot()\n\n    @classmethod\n    def migrate_context(cls, context, version):\n        if version < 2:\n            if context.values[\"annotation\"] == \"None\":\n                context.values[\"annotation\"] = None",
                            "ast_type_str": "ClassDef",
                            "start_lineno": 177,
                            "end_lineno": 1074,
                            "parent": null
                        },
                        {
                            "file_name": "Orange/widgets/unsupervised/owhierarchicalclustering.py",
                            "type": "method",
                            "name": "OWHierarchicalClustering.commit",
                            "content": "class OWHierarchicalClustering(widget.OWWidget):\n    name = \"Hierarchical Clustering\"\n    description = \"Display a dendrogram of a hierarchical clustering \" \\\n                  \"constructed from the input distance matrix.\"\n    icon = \"icons/HierarchicalClustering.svg\"\n    priority = 2100\n    keywords = \"hierarchical clustering\"\n\n    class Inputs:\n        distances = Input(\"Distances\", Orange.misc.DistMatrix)\n        subset = Input(\"Data Subset\", Orange.data.Table, explicit=True)\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    settings_version = 2\n    settingsHandler = _DomainContextHandler()\n\n    #: Selected linkage\n    linkage = settings.Setting(LINKAGE.index(DEFAULT_LINKAGE))\n    #: Index of the selected annotation item (variable, ...)\n    annotation = settings.ContextSetting(\"Enumeration\")\n    #: Out-of-context setting for the case when the \"Name\" option is available\n    annotation_if_names = settings.Setting(\"Name\")\n    #: Out-of-context setting for the case with just \"Enumeration\" and \"None\"\n    annotation_if_enumerate = settings.Setting(\"Enumeration\")\n    #: Selected tree pruning (none/max depth)\n    pruning = settings.Setting(0)\n    #: Maximum depth when max depth pruning is selected\n    max_depth = settings.Setting(10)\n\n    #: Selected cluster selection method (none, cut distance, top n)\n    selection_method = settings.Setting(0)\n    #: Cut height ratio wrt root height\n    cut_ratio = settings.Setting(75.0)\n    #: Number of top clusters to select\n    top_n = settings.Setting(3)\n    #: Dendrogram zoom factor\n    zoom_factor = settings.Setting(0)\n    #: Show labels only for subset (if present)\n    label_only_subset = settings.Setting(False)\n    #: Color for label decoration\n    color_by: Union[DiscreteVariable, ContinuousVariable, None] = \\\n        settings.ContextSetting(None)\n\n    autocommit = settings.Setting(True)\n\n    graph_name = \"scene\"  # QGraphicsScene\n\n    basic_annotations = [None, \"Enumeration\"]\n\n    class Error(widget.OWWidget.Error):\n        empty_matrix = Msg(\"Distance matrix is empty.\")\n        not_finite_distances = Msg(\"Some distances are infinite\")\n        not_symmetric = widget.Msg(\"Distance matrix is not symmetric.\")\n\n    class Warning(widget.OWWidget.Warning):\n        subset_on_no_table = \\\n            Msg(\"Unused data subset: distances do not refer to data instances\")\n        subset_not_subset = \\\n            Msg(\"Some data from the subset does not appear in distance matrix\")\n        subset_wrong = \\\n            Msg(\"Subset data refers to a different table\")\n        pruning_disables_colors = \\\n            Msg(\"Pruned cluster doesn't show colors and indicate subset\")\n        many_clusters = \\\n            Msg(\"Variables with too many values may \"\n                \"degrade the performance of downstream widgets.\")\n\n    #: Stored (manual) selection state (from a saved workflow) to restore.\n    __pending_selection_restore = None  # type: Optional[SelectionState]\n\n    def __init__(self):\n        super().__init__()\n\n        self.matrix = None\n        self.items = None\n        self.subset = None\n        self.subset_rows = set()\n        self.linkmatrix = None\n        self.root = None\n        self._displayed_root = None\n        self.cutoff_height = 0.0\n\n        spin_width = QFontMetrics(self.font()).horizontalAdvance(\"M\" * 7)\n        gui.comboBox(\n            self.controlArea, self, \"linkage\", items=LINKAGE, box=\"Linkage\",\n            callback=self._invalidate_clustering)\n\n        model = itemmodels.VariableListModel(placeholder=\"None\")\n        model[:] = self.basic_annotations\n\n        grid = QGridLayout()\n        gui.widgetBox(self.controlArea, \"Annotations\", orientation=grid)\n        self.label_cb = cb = combobox.ComboBoxSearch(\n            minimumContentsLength=14,\n            sizeAdjustPolicy=QComboBox.AdjustToMinimumContentsLengthWithIcon\n        )\n        cb.setModel(model)\n        cb.setCurrentIndex(cb.findData(self.annotation, Qt.EditRole))\n\n        def on_annotation_activated():\n            self.annotation = self.label_cb.currentData(Qt.EditRole)\n            self._update_labels()\n        cb.activated.connect(on_annotation_activated)\n\n        def on_annotation_changed(value):\n            self.label_cb.setCurrentIndex(\n                self.label_cb.findData(value, Qt.EditRole))\n        self.connect_control(\"annotation\", on_annotation_changed)\n\n        grid.addWidget(self.label_cb, 0, 0, 1, 2)\n\n        cb = gui.checkBox(\n            None, self, \"label_only_subset\", \"Show labels only for subset\",\n            disabled=True,\n            callback=self._update_labels, stateWhenDisabled=False)\n        grid.addWidget(cb, 1, 0, 1, 2)\n\n        model = itemmodels.DomainModel(\n            valid_types=(DiscreteVariable, ContinuousVariable),\n            placeholder=\"None\")\n        cb = gui.comboBox(\n            None, self, \"color_by\", orientation=Qt.Horizontal,\n            model=model, callback=self._update_labels,\n            sizePolicy=QSizePolicy(QSizePolicy.MinimumExpanding,\n                                   QSizePolicy.Fixed))\n        self.color_by_label = QLabel(\"Color by:\")\n        grid.addWidget(self.color_by_label, 2, 0)\n        grid.addWidget(cb, 2, 1)\n\n        box = gui.radioButtons(\n            self.controlArea, self, \"pruning\", box=\"Pruning\",\n            callback=self._invalidate_pruning)\n        grid = QGridLayout()\n        box.layout().addLayout(grid)\n        grid.addWidget(\n            gui.appendRadioButton(box, \"None\", addToLayout=False),\n            0, 0\n        )\n        self.max_depth_spin = gui.spin(\n            box, self, \"max_depth\", minv=1, maxv=100,\n            controlWidth=spin_width, alignment=Qt.AlignRight,\n            callback=self._max_depth_changed,\n            keyboardTracking=False, addToLayout=False\n        )\n        self.max_depth_spin.lineEdit().returnPressed.connect(\n            self._max_depth_return)\n\n        grid.addWidget(\n            gui.appendRadioButton(box, \"Max depth:\", addToLayout=False),\n            1, 0)\n        grid.addWidget(self.max_depth_spin, 1, 1)\n\n        self.selection_box = gui.radioButtons(\n            self.controlArea, self, \"selection_method\",\n            box=\"Selection\",\n            callback=self._selection_method_changed)\n\n        grid = QGridLayout()\n        self.selection_box.layout().addLayout(grid)\n        grid.addWidget(\n            gui.appendRadioButton(\n                self.selection_box, \"Manual\", addToLayout=False),\n            0, 0\n        )\n        grid.addWidget(\n            gui.appendRadioButton(\n                self.selection_box, \"Height ratio:\", addToLayout=False),\n            1, 0\n        )\n        self.cut_ratio_spin = gui.spin(\n            self.selection_box, self, \"cut_ratio\", 0, 100, step=1e-1,\n            controlWidth=spin_width, alignment = Qt.AlignRight,\n            spinType=float, callback=self._cut_ratio_changed,\n            addToLayout=False\n        )\n        self.cut_ratio_spin.setSuffix(\" %\")\n        self.cut_ratio_spin.lineEdit().returnPressed.connect(\n            self._cut_ratio_return)\n\n        grid.addWidget(self.cut_ratio_spin, 1, 1)\n\n        grid.addWidget(\n            gui.appendRadioButton(\n                self.selection_box, \"Top N:\", addToLayout=False),\n            2, 0\n        )\n        self.top_n_spin = gui.spin(\n            self.selection_box, self, \"top_n\", 1, 1000,\n            controlWidth=spin_width, alignment=Qt.AlignRight,\n            callback=self._top_n_changed, addToLayout=False)\n        self.top_n_spin.lineEdit().returnPressed.connect(self._top_n_return)\n        grid.addWidget(self.top_n_spin, 2, 1)\n\n        self.zoom_slider = gui.hSlider(\n            self.controlArea, self, \"zoom_factor\", box=\"Zoom\",\n            minValue=-6, maxValue=3, step=1, ticks=True, createLabel=False,\n            callback=self.__update_font_scale)\n\n        zoom_in = QAction(\n            \"Zoom in\", self, shortcut=QKeySequence.ZoomIn,\n            triggered=self.__zoom_in\n        )\n        zoom_out = QAction(\n            \"Zoom out\", self, shortcut=QKeySequence.ZoomOut,\n            triggered=self.__zoom_out\n        )\n        zoom_reset = QAction(\n            \"Reset zoom\", self,\n            shortcut=QKeySequence(Qt.ControlModifier | Qt.Key_0),\n            triggered=self.__zoom_reset\n        )\n        self.addActions([zoom_in, zoom_out, zoom_reset])\n\n        self.controlArea.layout().addStretch()\n\n        gui.auto_send(self.buttonsArea, self, \"autocommit\")\n\n        self.scene = QGraphicsScene(self)\n        self.view = GraphicsView(\n            self.scene,\n            horizontalScrollBarPolicy=Qt.ScrollBarAlwaysOff,\n            verticalScrollBarPolicy=Qt.ScrollBarAlwaysOn,\n            alignment=Qt.AlignLeft | Qt.AlignVCenter,\n            widgetResizable=True,\n        )\n        # Disable conflicting action shortcuts. We define our own.\n        for a in self.view.viewActions():\n            a.setEnabled(False)\n        self.mainArea.layout().setSpacing(1)\n        self.mainArea.layout().addWidget(self.view)\n\n        def axis_view(orientation):\n            ax = AxisItem(orientation=orientation, maxTickLength=7)\n            ax.mousePressed.connect(self._activate_cut_line)\n            ax.mouseMoved.connect(self._activate_cut_line)\n            ax.mouseReleased.connect(self._activate_cut_line)\n            ax.setRange(1.0, 0.0)\n            return ax\n\n        self.top_axis = axis_view(\"top\")\n        self.bottom_axis = axis_view(\"bottom\")\n\n        self._main_graphics = QGraphicsWidget(\n            sizePolicy=QSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.Preferred)\n        )\n        scenelayout = QGraphicsGridLayout()\n        scenelayout.setHorizontalSpacing(10)\n        scenelayout.setVerticalSpacing(10)\n\n        self._main_graphics.setLayout(scenelayout)\n        self.scene.addItem(self._main_graphics)\n        self.view.setCentralWidget(self._main_graphics)\n        self.scene.addItem(self._main_graphics)\n\n        self.dendrogram = DendrogramWidget(pen_width=2)\n        self.dendrogram.setSizePolicy(QSizePolicy.MinimumExpanding,\n                                      QSizePolicy.MinimumExpanding)\n        self.dendrogram.selectionChanged.connect(self._invalidate_output)\n        self.dendrogram.selectionEdited.connect(self._selection_edited)\n\n        self.labels = TextListView()\n        self.label_model = SelectedLabelsModel()\n        self.labels.setModel(self.label_model)\n        self.labels.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Preferred)\n        self.labels.setAlignment(Qt.AlignLeft)\n        self.labels.setMaximumWidth(200)\n\n        scenelayout.addItem(self.top_axis, 0, 0,\n                            alignment=Qt.AlignLeft | Qt.AlignVCenter)\n        scenelayout.addItem(self.dendrogram, 1, 0,\n                            alignment=Qt.AlignLeft | Qt.AlignVCenter)\n        scenelayout.addItem(self.labels, 1, 1,\n                            alignment=Qt.AlignLeft | Qt.AlignVCenter)\n        scenelayout.addItem(self.bottom_axis, 2, 0,\n                            alignment=Qt.AlignLeft | Qt.AlignVCenter)\n        self.top_axis.setZValue(self.dendrogram.zValue() + 10)\n        self.bottom_axis.setZValue(self.dendrogram.zValue() + 10)\n        self.cut_line = SliderLine(self.top_axis,\n                                   orientation=Qt.Horizontal)\n        self.cut_line.valueChanged.connect(self._dendrogram_slider_changed)\n        self.dendrogram.geometryChanged.connect(self._dendrogram_geom_changed)\n        self._set_cut_line_visible(self.selection_method == 1)\n        self.__update_font_scale()\n\n    @Inputs.distances\n    def set_distances(self, matrix):\n        self.error()\n        self.Error.clear()\n\n        self.matrix = None\n        self.Error.clear()\n        if matrix is not None:\n            if len(matrix) < 2:\n                self.Error.empty_matrix()\n            elif not matrix.is_symmetric():\n                self.Error.not_symmetric()\n            elif not np.all(np.isfinite(matrix)):\n                self.Error.not_finite_distances()\n            else:\n                self.matrix = matrix\n\n    @Inputs.subset\n    def set_subset(self, subset):\n        self.subset = subset\n        self.controls.label_only_subset.setDisabled(subset is None)\n\n    def handleNewSignals(self):\n        if self.__pending_selection_restore is not None:\n            selection_state = self.__pending_selection_restore\n        else:\n            # save the current selection to (possibly) restore later\n            selection_state = self._save_selection()\n\n        matrix = self.matrix\n        if matrix is not None:\n            self._set_items(matrix.row_items, matrix.axis)\n        else:\n            self._set_items(None)\n        self._update()\n\n        # Can now attempt to restore session state from a saved workflow.\n        if self.root and selection_state is not None:\n            self._restore_selection(selection_state)\n            self.__pending_selection_restore = None\n\n        self.Warning.clear()\n        rows = set()\n        if self.subset:\n            subsetids = set(self.subset.ids)\n            if not isinstance(self.items, Orange.data.Table) \\\n                    or not self.matrix.axis:\n                self.Warning.subset_on_no_table()\n            elif (dataids := set(self.items.ids)) and not subsetids & dataids:\n                self.Warning.subset_wrong()\n            elif not subsetids <= dataids:\n                self.Warning.subset_not_subset()\n            else:\n                indices = [leaf.value.index for leaf in leaves(self.root)]\n                rows = {\n                    row for row, rowid in enumerate(self.items.ids[indices])\n                    if rowid in subsetids\n                }\n\n        self.subset_rows = rows\n        self._update_labels()\n        self.commit.now()\n\n    def _set_items(self, items, axis=1):\n        self.closeContext()\n        self.items = items\n        model = self.label_cb.model()\n        color_model = self.controls.color_by.model()\n        color_model.set_domain(None)\n        self.color_by = None\n        if len(model) == 2 and model[0] is None:\n            if model[1] == \"Name\":\n                self.annotation_if_names = self.annotation\n            if model[1] == self.basic_annotations[1]:\n                self.annotation_if_enumerate = self.annotation\n        if isinstance(items, Orange.data.Table) and axis:\n            metas_class = tuple(\n                filter_visible(chain(items.domain.metas,\n                                     items.domain.class_vars)))\n            visible_attrs = tuple(filter_visible(items.domain.attributes))\n            if not (metas_class or visible_attrs):\n                model[:] = self.basic_annotations\n            else:\n                model[:] = (\n                    (None, )\n                    + metas_class\n                    + (model.Separator, ) * bool(metas_class and visible_attrs)\n                    + visible_attrs)\n            for meta in items.domain.metas:\n                if isinstance(meta, StringVariable):\n                    self.annotation = meta\n                    break\n            else:\n                if items.domain.class_vars:\n                    # No string metas: show class\n                    self.annotation = items.domain.class_vars[0]\n                else:\n                    # No string metas and no class: show the first option\n                    # which is not None (in the worst case, Enumeration)\n                    self.annotation = model[1]\n\n            color_model.set_domain(items.domain)\n            if items.domain.class_vars:\n                self.color_by = items.domain.class_vars[0]\n            self.openContext(items.domain)\n        elif isinstance(items, Orange.data.Table) and not axis \\\n                or (isinstance(items, list) and \\\n                    all(isinstance(var, Orange.data.Variable)\n                        for var in items)):\n            model[:] = (None, \"Name\")\n            self.annotation = self.annotation_if_names\n        else:\n            model[:] = self.basic_annotations\n            self.annotation = self.annotation_if_enumerate\n\n        no_colors = len(color_model) == 1\n        self.controls.color_by.setDisabled(no_colors)\n        self.color_by_label.setDisabled(no_colors)\n\n    def _clear_plot(self):\n        self.dendrogram.set_root(None)\n        self.label_model.clear()\n\n    def _set_displayed_root(self, root):\n        self._clear_plot()\n        self._displayed_root = root\n        self.dendrogram.set_root(root)\n        self._update_labels()\n\n    def _update(self):\n        self._clear_plot()\n\n        distances = self.matrix\n\n        if distances is not None:\n            method = LINKAGE_ARGS[self.linkage]\n            Z = dist_matrix_linkage(distances, linkage=method)\n\n            tree = tree_from_linkage(Z)\n            self.linkmatrix = Z\n            self.root = tree\n\n            self.top_axis.setRange(tree.value.height, 0.0)\n            self.bottom_axis.setRange(tree.value.height, 0.0)\n\n            if self.pruning:\n                self._set_displayed_root(prune(tree, level=self.max_depth))\n            else:\n                self._set_displayed_root(tree)\n        else:\n            self.linkmatrix = None\n            self.root = None\n            self._set_displayed_root(None)\n\n        self._apply_selection()\n\n    def _update_labels(self):\n        if not hasattr(self, \"label_model\"):\n            # This method can be called during widget initialization when\n            # creating check box for label_only_subset, if it's value is\n            # initially True.\n            # See https://github.com/biolab/orange-widget-base/pull/213;\n            # if it's merged, this check can be removed.\n            return\n\n        self.Warning.pruning_disables_colors(\n            shown=self.pruning\n                  and (self.subset_rows or self.color_by is not None))\n        labels = []\n        if self.root and self._displayed_root:\n            indices = [leaf.value.index for leaf in leaves(self.root)]\n\n            if self.annotation is None:\n                if not self.pruning \\\n                        and self.subset_rows and self.color_by is None:\n                    # Model fails if number of labels and of colors mismatch\n                    labels = [\"\"] * len(indices)\n                else:\n                    labels = []\n            elif self.annotation == \"Enumeration\":\n                labels = [str(i+1) for i in indices]\n            elif self.annotation == \"Name\":\n                attr = self.matrix.row_items.domain.attributes\n                labels = [str(attr[i]) for i in indices]\n            elif isinstance(self.annotation, Orange.data.Variable):\n                col_data = self.items.get_column(self.annotation)\n                labels = [self.annotation.str_val(val) for val in col_data]\n                labels = [labels[idx] for idx in indices]\n            else:\n                labels = []\n            if not self.pruning and \\\n                    labels and self.label_only_subset and self.subset_rows:\n                labels = [label if row in self.subset_rows else \"\"\n                          for row, label in enumerate(labels)]\n\n            if labels and self._displayed_root is not self.root:\n                joined = leaves(self._displayed_root)\n                labels = [\", \".join(labels[leaf.value.first: leaf.value.last])\n                          for leaf in joined]\n\n        self.label_model[:] = labels\n        self.label_model.set_subset(set() if self.pruning else self.subset_rows)\n        self.labels.setMinimumWidth(1 if labels else -1)\n\n        if not self.pruning and self.color_by is not None:\n            col = self.items.get_column(self.color_by)\n            self.label_model.set_colors(\n                self.color_by.palette.values_to_qcolors(col[indices]))\n        else:\n            self.label_model.set_colors(None)\n\n    def _restore_selection(self, state):\n        # type: (SelectionState) -> bool\n        \"\"\"\n        Restore the (manual) node selection state.\n\n        Return True if successful; False otherwise.\n        \"\"\"\n        linkmatrix = self.linkmatrix\n        if self.selection_method == 0 and self.root:\n            selected, linksaved = state\n            linkstruct = np.array(linksaved, dtype=float)\n            selected = set(selected)  # type: Set[Tuple[int]]\n            if not selected:\n                return False\n            if linkmatrix.shape[0] != linkstruct.shape[0]:\n                return False\n            # check that the linkage matrix structure matches. Use isclose for\n            # the height column to account for inexact floating point math\n            # (e.g. summation order in different ?gemm implementations for\n            # euclidean distances, ...)\n            if np.any(linkstruct[:, :2] != linkmatrix[:, :2]) or \\\n                    not np.all(np.isclose(linkstruct[:, 2], linkstruct[:, 2])):\n                return False\n            selection = []\n            indices = np.array([n.value.index for n in leaves(self.root)],\n                               dtype=int)\n            # mapping from ranges to display (pruned) nodes\n            mapping = {node.value.range: node\n                       for node in postorder(self._displayed_root)}\n            for node in postorder(self.root):  # type: Tree\n                r = tuple(indices[node.value.first: node.value.last])\n                if r in selected:\n                    if node.value.range not in mapping:\n                        # the node was pruned from display and cannot be\n                        # selected\n                        break\n                    selection.append(mapping[node.value.range])\n                    selected.remove(r)\n                if not selected:\n                    break  # found all, nothing more to do\n            if selection and selected:\n                # Could not restore all selected nodes (only partial match)\n                return False\n\n            self._set_selected_nodes(selection)\n            return True\n        return False\n\n    def _set_selected_nodes(self, selection):\n        # type: (List[Tree]) -> None\n        \"\"\"\n        Set the nodes in `selection` to be the current selected nodes.\n\n        The selection nodes must be subtrees of the current `_displayed_root`.\n        \"\"\"\n        self.dendrogram.selectionChanged.disconnect(self._invalidate_output)\n        try:\n            self.dendrogram.set_selected_clusters(selection)\n        finally:\n            self.dendrogram.selectionChanged.connect(self._invalidate_output)\n\n    def _max_depth_return(self):\n        if self.pruning != 1:\n            self.pruning = 1\n            self._invalidate_pruning()\n\n    def _max_depth_changed(self):\n        self.pruning = 1\n        self._invalidate_pruning()\n\n    def _invalidate_clustering(self):\n        self._update()\n        self._update_labels()\n        self._invalidate_output()\n\n    def _invalidate_output(self):\n        self.commit.deferred()\n\n    def _invalidate_pruning(self):\n        if self.root:\n            selection = self.dendrogram.selected_nodes()\n            ranges = [node.value.range for node in selection]\n            if self.pruning:\n                self._set_displayed_root(\n                    prune(self.root, level=self.max_depth))\n            else:\n                self._set_displayed_root(self.root)\n            selected = [node for node in preorder(self._displayed_root)\n                        if node.value.range in ranges]\n\n            self.dendrogram.set_selected_clusters(selected)\n\n        self._apply_selection()\n\n    @gui.deferred\n    def commit(self):\n        items = getattr(self.matrix, \"items\", self.items)\n        self.Warning.many_clusters.clear()\n        if not items:\n            self.Outputs.selected_data.send(None)\n            self.Outputs.annotated_data.send(None)\n            return\n\n        selection = self.dendrogram.selected_nodes()\n        selection = sorted(selection, key=lambda c: c.value.first)\n\n        indices = [leaf.value.index for leaf in leaves(self.root)]\n\n        maps = [indices[node.value.first:node.value.last]\n                for node in selection]\n        if len(maps) > 20:\n            self.Warning.many_clusters()\n\n        selected_indices = list(chain(*maps))\n\n        if not selected_indices:\n            self.Outputs.selected_data.send(None)\n            annotated_data = lazy_annotated_table(items, []) \\\n                if self.selection_method == 0 and self.matrix.axis else None\n            self.Outputs.annotated_data.send(annotated_data)\n            return\n\n        selected_data = annotated_data = None\n\n        if isinstance(items, Orange.data.Table) and self.matrix.axis == 1:\n            # Select rows\n            data, domain = items, items.domain\n\n            c = np.full(self.matrix.shape[0], len(maps))\n            for i, indices in enumerate(maps):\n                c[indices] = i\n\n            clust_name = get_unique_names(domain, \"Cluster\")\n            values = [f\"C{i + 1}\" for i in range(len(maps))]\n\n            sel_clust_var = Orange.data.DiscreteVariable(\n                name=clust_name, values=values)\n            sel_domain = add_columns(domain, metas=(sel_clust_var,))\n            selected_data = LazyValue[Table](\n                lambda: items.add_column(\n                    sel_clust_var, c, to_metas=True)[c != len(maps)],\n                domain=sel_domain, length=len(selected_indices))\n\n            ann_clust_var = Orange.data.DiscreteVariable(\n                name=clust_name, values=values + [\"Other\"]\n            )\n            ann_domain = add_columns(\n                domain_with_annotation_column(data)[0], metas=(ann_clust_var, ))\n            annotated_data = LazyValue[Table](\n                lambda: create_annotated_table(\n                    data=items.add_column(ann_clust_var, c, to_metas=True),\n                    selected_indices=selected_indices),\n                domain=ann_domain, length=len(items)\n            )\n\n        elif isinstance(items, Orange.data.Table) and self.matrix.axis == 0:\n            # Select columns\n            attrs = []\n            unselected_indices = sorted(set(range(self.root.value.last)) -\n                                        set(selected_indices))\n            for clust, indices in chain(enumerate(maps, start=1),\n                                        [(0, unselected_indices)]):\n                for i in indices:\n                    attr = items.domain[i].copy()\n                    attr.attributes[\"cluster\"] = clust\n                    attrs.append(attr)\n            all_domain = Orange.data.Domain(\n                # len(unselected_indices) can be 0\n                attrs[:len(attrs) - len(unselected_indices)],\n                items.domain.class_vars, items.domain.metas)\n\n            selected_data = LazyValue[Table](\n                lambda: items.from_table(all_domain, items),\n                domain=all_domain, length=len(items))\n\n            sel_domain = Orange.data.Domain(\n                attrs,\n                items.domain.class_vars, items.domain.metas)\n            annotated_data = LazyValue[Table](\n                lambda: items.from_table(sel_domain, items),\n                domain=sel_domain, length=len(items))\n\n        self.Outputs.selected_data.send(selected_data)\n        self.Outputs.annotated_data.send(annotated_data)\n\n    @Slot(QPointF)\n    def _activate_cut_line(self, pos: QPointF):\n        \"\"\"Activate cut line selection an set cut value to `pos.x()`.\"\"\"\n        self.selection_method = 1\n        self.cut_line.setValue(pos.x())\n        self._selection_method_changed()\n\n    def onDeleteWidget(self):\n        super().onDeleteWidget()\n        self._clear_plot()\n        self.dendrogram.clear()\n        self.dendrogram.deleteLater()\n\n    def _dendrogram_geom_changed(self):\n        pos = self.dendrogram.pos_at_height(self.cutoff_height)\n        geom = self.dendrogram.geometry()\n        self._set_slider_value(pos.x(), geom.width())\n\n        self.cut_line.setLength(\n            self.bottom_axis.geometry().bottom()\n            - self.top_axis.geometry().top()\n        )\n\n        geom = self._main_graphics.geometry()\n        assert geom.topLeft() == QPointF(0, 0)\n\n        def adjustLeft(rect):\n            rect = QRectF(rect)\n            rect.setLeft(geom.left())\n            return rect\n        margin = 3\n        self.scene.setSceneRect(geom)\n        self.view.setSceneRect(geom)\n        self.view.setHeaderSceneRect(\n            adjustLeft(self.top_axis.geometry()).adjusted(0, 0, 0, margin)\n        )\n        self.view.setFooterSceneRect(\n            adjustLeft(self.bottom_axis.geometry()).adjusted(0, -margin, 0, 0)\n        )\n\n    def _dendrogram_slider_changed(self, value):\n        p = QPointF(value, 0)\n        cl_height = self.dendrogram.height_at(p)\n\n        self.set_cutoff_height(cl_height)\n\n    def _set_slider_value(self, value, span):\n        with blocked(self.cut_line):\n            self.cut_line.setRange(0, span)\n            self.cut_line.setValue(value)\n\n    def set_cutoff_height(self, height):\n        self.cutoff_height = height\n        if self.root:\n            self.cut_ratio = 100 * height / self.root.value.height\n        self.select_max_height(height)\n\n    def _set_cut_line_visible(self, visible):\n        self.cut_line.setVisible(visible)\n\n    def select_top_n(self, n):\n        root = self._displayed_root\n        if root:\n            clusters = top_clusters(root, n)\n            self.dendrogram.set_selected_clusters(clusters)\n\n    def select_max_height(self, height):\n        root = self._displayed_root\n        if root:\n            clusters = clusters_at_height(root, height)\n            self.dendrogram.set_selected_clusters(clusters)\n\n    def _cut_ratio_changed(self):\n        self.selection_method = 1\n        self._selection_method_changed()\n\n    def _cut_ratio_return(self):\n        if self.selection_method != 1:\n            self.selection_method = 1\n            self._selection_method_changed()\n\n    def _top_n_changed(self):\n        self.selection_method = 2\n        self._selection_method_changed()\n\n    def _top_n_return(self):\n        if self.selection_method != 2:\n            self.selection_method = 2\n            self._selection_method_changed()\n\n    def _selection_method_changed(self):\n        self._set_cut_line_visible(self.selection_method == 1)\n        if self.root:\n            self._apply_selection()\n\n    def _apply_selection(self):\n        if not self.root:\n            return\n\n        if self.selection_method == 0:\n            pass\n        elif self.selection_method == 1:\n            height = self.cut_ratio * self.root.value.height / 100\n            self.set_cutoff_height(height)\n            pos = self.dendrogram.pos_at_height(height)\n            self._set_slider_value(pos.x(), self.dendrogram.size().width())\n        elif self.selection_method == 2:\n            self.select_top_n(self.top_n)\n\n    def _selection_edited(self):\n        # Selection was edited by clicking on a cluster in the\n        # dendrogram view.\n        self.selection_method = 0\n        self._selection_method_changed()\n        self._invalidate_output()\n\n    def _save_selection(self):\n        # Save the current manual node selection state\n        selection_state = None\n        if self.selection_method == 0 and self.root:\n            assert self.linkmatrix is not None\n            linkmat = [(int(_0), int(_1), _2)\n                       for _0, _1, _2 in self.linkmatrix[:, :3].tolist()]\n            nodes_ = self.dendrogram.selected_nodes()\n            # match the display (pruned) nodes back (by ranges)\n            mapping = {node.value.range: node for node in postorder(self.root)}\n            nodes = [mapping[node.value.range] for node in nodes_]\n            indices = [tuple(node.value.index for node in leaves(node))\n                       for node in nodes]\n            if nodes:\n                selection_state = (indices, linkmat)\n        return selection_state\n\n    def save_state(self):\n        # type: () -> Dict[str, Any]\n        \"\"\"\n        Save state for `set_restore_state`\n        \"\"\"\n        selection = self._save_selection()\n        res = {\"version\": (0, 0, 0)}\n        if selection is not None:\n            res[\"selection_state\"] = selection\n        return res\n\n    def set_restore_state(self, state):\n        # type: (Dict[str, Any]) -> bool\n        \"\"\"\n        Restore session data from a saved state.\n\n        Parameters\n        ----------\n        state : Dict[str, Any]\n\n        NOTE\n        ----\n        This is method called while the instance (self) is being constructed,\n        even before its `__init__` is called. Consider `self` to be only a\n        `QObject` at this stage.\n        \"\"\"\n        if \"selection_state\" in state:\n            selection = state[\"selection_state\"]\n            self.__pending_selection_restore = selection\n        return True\n\n    def __zoom_in(self):\n        def clip(minval, maxval, val):\n            return min(max(val, minval), maxval)\n        self.zoom_factor = clip(self.zoom_slider.minimum(),\n                                self.zoom_slider.maximum(),\n                                self.zoom_factor + 1)\n        self.__update_font_scale()\n\n    def __zoom_out(self):\n        def clip(minval, maxval, val):\n            return min(max(val, minval), maxval)\n        self.zoom_factor = clip(self.zoom_slider.minimum(),\n                                self.zoom_slider.maximum(),\n                                self.zoom_factor - 1)\n        self.__update_font_scale()\n\n    def __zoom_reset(self):\n        self.zoom_factor = 0\n        self.__update_font_scale()\n\n    def __update_font_scale(self):\n        font = self.scene.font()\n        factor = (1.25 ** self.zoom_factor)\n        font = qfont_scaled(font, factor)\n        self._main_graphics.setFont(font)\n        self.label_model.setFont(font)\n\n    def send_report(self):\n        annot = self.label_cb.currentText()\n        if isinstance(self.annotation, str):\n            annot = annot.lower()\n        if self.selection_method == 0:\n            sel = \"manual\"\n        elif self.selection_method == 1:\n            sel = \"at {:.1f} of height\".format(self.cut_ratio)\n        else:\n            sel = f\"top {self.top_n} {pl(self.top_n, 'cluster')}\"\n        self.report_items((\n            (\"Linkage\", LINKAGE[self.linkage]),\n            (\"Annotation\", annot),\n            (\"Pruning\",\n             self.pruning != 0 and \"{} levels\".format(self.max_depth)),\n            (\"Selection\", sel),\n        ))\n        self.report_plot()\n\n    @classmethod\n    def migrate_context(cls, context, version):\n        if version < 2:\n            if context.values[\"annotation\"] == \"None\":\n                context.values[\"annotation\"] = None",
                            "ast_type_str": "ClassDef",
                            "start_lineno": 770,
                            "end_lineno": 858,
                            "parent": {
                                "file_name": "Orange/widgets/unsupervised/owhierarchicalclustering.py",
                                "type": "class",
                                "name": "OWHierarchicalClustering",
                                "content": "class OWHierarchicalClustering(widget.OWWidget):\n    name = \"Hierarchical Clustering\"\n    description = \"Display a dendrogram of a hierarchical clustering \" \\\n                  \"constructed from the input distance matrix.\"\n    icon = \"icons/HierarchicalClustering.svg\"\n    priority = 2100\n    keywords = \"hierarchical clustering\"\n\n    class Inputs:\n        distances = Input(\"Distances\", Orange.misc.DistMatrix)\n        subset = Input(\"Data Subset\", Orange.data.Table, explicit=True)\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    settings_version = 2\n    settingsHandler = _DomainContextHandler()\n\n    #: Selected linkage\n    linkage = settings.Setting(LINKAGE.index(DEFAULT_LINKAGE))\n    #: Index of the selected annotation item (variable, ...)\n    annotation = settings.ContextSetting(\"Enumeration\")\n    #: Out-of-context setting for the case when the \"Name\" option is available\n    annotation_if_names = settings.Setting(\"Name\")\n    #: Out-of-context setting for the case with just \"Enumeration\" and \"None\"\n    annotation_if_enumerate = settings.Setting(\"Enumeration\")\n    #: Selected tree pruning (none/max depth)\n    pruning = settings.Setting(0)\n    #: Maximum depth when max depth pruning is selected\n    max_depth = settings.Setting(10)\n\n    #: Selected cluster selection method (none, cut distance, top n)\n    selection_method = settings.Setting(0)\n    #: Cut height ratio wrt root height\n    cut_ratio = settings.Setting(75.0)\n    #: Number of top clusters to select\n    top_n = settings.Setting(3)\n    #: Dendrogram zoom factor\n    zoom_factor = settings.Setting(0)\n    #: Show labels only for subset (if present)\n    label_only_subset = settings.Setting(False)\n    #: Color for label decoration\n    color_by: Union[DiscreteVariable, ContinuousVariable, None] = \\\n        settings.ContextSetting(None)\n\n    autocommit = settings.Setting(True)\n\n    graph_name = \"scene\"  # QGraphicsScene\n\n    basic_annotations = [None, \"Enumeration\"]\n\n    class Error(widget.OWWidget.Error):\n        empty_matrix = Msg(\"Distance matrix is empty.\")\n        not_finite_distances = Msg(\"Some distances are infinite\")\n        not_symmetric = widget.Msg(\"Distance matrix is not symmetric.\")\n\n    class Warning(widget.OWWidget.Warning):\n        subset_on_no_table = \\\n            Msg(\"Unused data subset: distances do not refer to data instances\")\n        subset_not_subset = \\\n            Msg(\"Some data from the subset does not appear in distance matrix\")\n        subset_wrong = \\\n            Msg(\"Subset data refers to a different table\")\n        pruning_disables_colors = \\\n            Msg(\"Pruned cluster doesn't show colors and indicate subset\")\n        many_clusters = \\\n            Msg(\"Variables with too many values may \"\n                \"degrade the performance of downstream widgets.\")\n\n    #: Stored (manual) selection state (from a saved workflow) to restore.\n    __pending_selection_restore = None  # type: Optional[SelectionState]\n\n    def __init__(self):\n        super().__init__()\n\n        self.matrix = None\n        self.items = None\n        self.subset = None\n        self.subset_rows = set()\n        self.linkmatrix = None\n        self.root = None\n        self._displayed_root = None\n        self.cutoff_height = 0.0\n\n        spin_width = QFontMetrics(self.font()).horizontalAdvance(\"M\" * 7)\n        gui.comboBox(\n            self.controlArea, self, \"linkage\", items=LINKAGE, box=\"Linkage\",\n            callback=self._invalidate_clustering)\n\n        model = itemmodels.VariableListModel(placeholder=\"None\")\n        model[:] = self.basic_annotations\n\n        grid = QGridLayout()\n        gui.widgetBox(self.controlArea, \"Annotations\", orientation=grid)\n        self.label_cb = cb = combobox.ComboBoxSearch(\n            minimumContentsLength=14,\n            sizeAdjustPolicy=QComboBox.AdjustToMinimumContentsLengthWithIcon\n        )\n        cb.setModel(model)\n        cb.setCurrentIndex(cb.findData(self.annotation, Qt.EditRole))\n\n        def on_annotation_activated():\n            self.annotation = self.label_cb.currentData(Qt.EditRole)\n            self._update_labels()\n        cb.activated.connect(on_annotation_activated)\n\n        def on_annotation_changed(value):\n            self.label_cb.setCurrentIndex(\n                self.label_cb.findData(value, Qt.EditRole))\n        self.connect_control(\"annotation\", on_annotation_changed)\n\n        grid.addWidget(self.label_cb, 0, 0, 1, 2)\n\n        cb = gui.checkBox(\n            None, self, \"label_only_subset\", \"Show labels only for subset\",\n            disabled=True,\n            callback=self._update_labels, stateWhenDisabled=False)\n        grid.addWidget(cb, 1, 0, 1, 2)\n\n        model = itemmodels.DomainModel(\n            valid_types=(DiscreteVariable, ContinuousVariable),\n            placeholder=\"None\")\n        cb = gui.comboBox(\n            None, self, \"color_by\", orientation=Qt.Horizontal,\n            model=model, callback=self._update_labels,\n            sizePolicy=QSizePolicy(QSizePolicy.MinimumExpanding,\n                                   QSizePolicy.Fixed))\n        self.color_by_label = QLabel(\"Color by:\")\n        grid.addWidget(self.color_by_label, 2, 0)\n        grid.addWidget(cb, 2, 1)\n\n        box = gui.radioButtons(\n            self.controlArea, self, \"pruning\", box=\"Pruning\",\n            callback=self._invalidate_pruning)\n        grid = QGridLayout()\n        box.layout().addLayout(grid)\n        grid.addWidget(\n            gui.appendRadioButton(box, \"None\", addToLayout=False),\n            0, 0\n        )\n        self.max_depth_spin = gui.spin(\n            box, self, \"max_depth\", minv=1, maxv=100,\n            controlWidth=spin_width, alignment=Qt.AlignRight,\n            callback=self._max_depth_changed,\n            keyboardTracking=False, addToLayout=False\n        )\n        self.max_depth_spin.lineEdit().returnPressed.connect(\n            self._max_depth_return)\n\n        grid.addWidget(\n            gui.appendRadioButton(box, \"Max depth:\", addToLayout=False),\n            1, 0)\n        grid.addWidget(self.max_depth_spin, 1, 1)\n\n        self.selection_box = gui.radioButtons(\n            self.controlArea, self, \"selection_method\",\n            box=\"Selection\",\n            callback=self._selection_method_changed)\n\n        grid = QGridLayout()\n        self.selection_box.layout().addLayout(grid)\n        grid.addWidget(\n            gui.appendRadioButton(\n                self.selection_box, \"Manual\", addToLayout=False),\n            0, 0\n        )\n        grid.addWidget(\n            gui.appendRadioButton(\n                self.selection_box, \"Height ratio:\", addToLayout=False),\n            1, 0\n        )\n        self.cut_ratio_spin = gui.spin(\n            self.selection_box, self, \"cut_ratio\", 0, 100, step=1e-1,\n            controlWidth=spin_width, alignment = Qt.AlignRight,\n            spinType=float, callback=self._cut_ratio_changed,\n            addToLayout=False\n        )\n        self.cut_ratio_spin.setSuffix(\" %\")\n        self.cut_ratio_spin.lineEdit().returnPressed.connect(\n            self._cut_ratio_return)\n\n        grid.addWidget(self.cut_ratio_spin, 1, 1)\n\n        grid.addWidget(\n            gui.appendRadioButton(\n                self.selection_box, \"Top N:\", addToLayout=False),\n            2, 0\n        )\n        self.top_n_spin = gui.spin(\n            self.selection_box, self, \"top_n\", 1, 1000,\n            controlWidth=spin_width, alignment=Qt.AlignRight,\n            callback=self._top_n_changed, addToLayout=False)\n        self.top_n_spin.lineEdit().returnPressed.connect(self._top_n_return)\n        grid.addWidget(self.top_n_spin, 2, 1)\n\n        self.zoom_slider = gui.hSlider(\n            self.controlArea, self, \"zoom_factor\", box=\"Zoom\",\n            minValue=-6, maxValue=3, step=1, ticks=True, createLabel=False,\n            callback=self.__update_font_scale)\n\n        zoom_in = QAction(\n            \"Zoom in\", self, shortcut=QKeySequence.ZoomIn,\n            triggered=self.__zoom_in\n        )\n        zoom_out = QAction(\n            \"Zoom out\", self, shortcut=QKeySequence.ZoomOut,\n            triggered=self.__zoom_out\n        )\n        zoom_reset = QAction(\n            \"Reset zoom\", self,\n            shortcut=QKeySequence(Qt.ControlModifier | Qt.Key_0),\n            triggered=self.__zoom_reset\n        )\n        self.addActions([zoom_in, zoom_out, zoom_reset])\n\n        self.controlArea.layout().addStretch()\n\n        gui.auto_send(self.buttonsArea, self, \"autocommit\")\n\n        self.scene = QGraphicsScene(self)\n        self.view = GraphicsView(\n            self.scene,\n            horizontalScrollBarPolicy=Qt.ScrollBarAlwaysOff,\n            verticalScrollBarPolicy=Qt.ScrollBarAlwaysOn,\n            alignment=Qt.AlignLeft | Qt.AlignVCenter,\n            widgetResizable=True,\n        )\n        # Disable conflicting action shortcuts. We define our own.\n        for a in self.view.viewActions():\n            a.setEnabled(False)\n        self.mainArea.layout().setSpacing(1)\n        self.mainArea.layout().addWidget(self.view)\n\n        def axis_view(orientation):\n            ax = AxisItem(orientation=orientation, maxTickLength=7)\n            ax.mousePressed.connect(self._activate_cut_line)\n            ax.mouseMoved.connect(self._activate_cut_line)\n            ax.mouseReleased.connect(self._activate_cut_line)\n            ax.setRange(1.0, 0.0)\n            return ax\n\n        self.top_axis = axis_view(\"top\")\n        self.bottom_axis = axis_view(\"bottom\")\n\n        self._main_graphics = QGraphicsWidget(\n            sizePolicy=QSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.Preferred)\n        )\n        scenelayout = QGraphicsGridLayout()\n        scenelayout.setHorizontalSpacing(10)\n        scenelayout.setVerticalSpacing(10)\n\n        self._main_graphics.setLayout(scenelayout)\n        self.scene.addItem(self._main_graphics)\n        self.view.setCentralWidget(self._main_graphics)\n        self.scene.addItem(self._main_graphics)\n\n        self.dendrogram = DendrogramWidget(pen_width=2)\n        self.dendrogram.setSizePolicy(QSizePolicy.MinimumExpanding,\n                                      QSizePolicy.MinimumExpanding)\n        self.dendrogram.selectionChanged.connect(self._invalidate_output)\n        self.dendrogram.selectionEdited.connect(self._selection_edited)\n\n        self.labels = TextListView()\n        self.label_model = SelectedLabelsModel()\n        self.labels.setModel(self.label_model)\n        self.labels.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Preferred)\n        self.labels.setAlignment(Qt.AlignLeft)\n        self.labels.setMaximumWidth(200)\n\n        scenelayout.addItem(self.top_axis, 0, 0,\n                            alignment=Qt.AlignLeft | Qt.AlignVCenter)\n        scenelayout.addItem(self.dendrogram, 1, 0,\n                            alignment=Qt.AlignLeft | Qt.AlignVCenter)\n        scenelayout.addItem(self.labels, 1, 1,\n                            alignment=Qt.AlignLeft | Qt.AlignVCenter)\n        scenelayout.addItem(self.bottom_axis, 2, 0,\n                            alignment=Qt.AlignLeft | Qt.AlignVCenter)\n        self.top_axis.setZValue(self.dendrogram.zValue() + 10)\n        self.bottom_axis.setZValue(self.dendrogram.zValue() + 10)\n        self.cut_line = SliderLine(self.top_axis,\n                                   orientation=Qt.Horizontal)\n        self.cut_line.valueChanged.connect(self._dendrogram_slider_changed)\n        self.dendrogram.geometryChanged.connect(self._dendrogram_geom_changed)\n        self._set_cut_line_visible(self.selection_method == 1)\n        self.__update_font_scale()\n\n    @Inputs.distances\n    def set_distances(self, matrix):\n        self.error()\n        self.Error.clear()\n\n        self.matrix = None\n        self.Error.clear()\n        if matrix is not None:\n            if len(matrix) < 2:\n                self.Error.empty_matrix()\n            elif not matrix.is_symmetric():\n                self.Error.not_symmetric()\n            elif not np.all(np.isfinite(matrix)):\n                self.Error.not_finite_distances()\n            else:\n                self.matrix = matrix\n\n    @Inputs.subset\n    def set_subset(self, subset):\n        self.subset = subset\n        self.controls.label_only_subset.setDisabled(subset is None)\n\n    def handleNewSignals(self):\n        if self.__pending_selection_restore is not None:\n            selection_state = self.__pending_selection_restore\n        else:\n            # save the current selection to (possibly) restore later\n            selection_state = self._save_selection()\n\n        matrix = self.matrix\n        if matrix is not None:\n            self._set_items(matrix.row_items, matrix.axis)\n        else:\n            self._set_items(None)\n        self._update()\n\n        # Can now attempt to restore session state from a saved workflow.\n        if self.root and selection_state is not None:\n            self._restore_selection(selection_state)\n            self.__pending_selection_restore = None\n\n        self.Warning.clear()\n        rows = set()\n        if self.subset:\n            subsetids = set(self.subset.ids)\n            if not isinstance(self.items, Orange.data.Table) \\\n                    or not self.matrix.axis:\n                self.Warning.subset_on_no_table()\n            elif (dataids := set(self.items.ids)) and not subsetids & dataids:\n                self.Warning.subset_wrong()\n            elif not subsetids <= dataids:\n                self.Warning.subset_not_subset()\n            else:\n                indices = [leaf.value.index for leaf in leaves(self.root)]\n                rows = {\n                    row for row, rowid in enumerate(self.items.ids[indices])\n                    if rowid in subsetids\n                }\n\n        self.subset_rows = rows\n        self._update_labels()\n        self.commit.now()\n\n    def _set_items(self, items, axis=1):\n        self.closeContext()\n        self.items = items\n        model = self.label_cb.model()\n        color_model = self.controls.color_by.model()\n        color_model.set_domain(None)\n        self.color_by = None\n        if len(model) == 2 and model[0] is None:\n            if model[1] == \"Name\":\n                self.annotation_if_names = self.annotation\n            if model[1] == self.basic_annotations[1]:\n                self.annotation_if_enumerate = self.annotation\n        if isinstance(items, Orange.data.Table) and axis:\n            metas_class = tuple(\n                filter_visible(chain(items.domain.metas,\n                                     items.domain.class_vars)))\n            visible_attrs = tuple(filter_visible(items.domain.attributes))\n            if not (metas_class or visible_attrs):\n                model[:] = self.basic_annotations\n            else:\n                model[:] = (\n                    (None, )\n                    + metas_class\n                    + (model.Separator, ) * bool(metas_class and visible_attrs)\n                    + visible_attrs)\n            for meta in items.domain.metas:\n                if isinstance(meta, StringVariable):\n                    self.annotation = meta\n                    break\n            else:\n                if items.domain.class_vars:\n                    # No string metas: show class\n                    self.annotation = items.domain.class_vars[0]\n                else:\n                    # No string metas and no class: show the first option\n                    # which is not None (in the worst case, Enumeration)\n                    self.annotation = model[1]\n\n            color_model.set_domain(items.domain)\n            if items.domain.class_vars:\n                self.color_by = items.domain.class_vars[0]\n            self.openContext(items.domain)\n        elif isinstance(items, Orange.data.Table) and not axis \\\n                or (isinstance(items, list) and \\\n                    all(isinstance(var, Orange.data.Variable)\n                        for var in items)):\n            model[:] = (None, \"Name\")\n            self.annotation = self.annotation_if_names\n        else:\n            model[:] = self.basic_annotations\n            self.annotation = self.annotation_if_enumerate\n\n        no_colors = len(color_model) == 1\n        self.controls.color_by.setDisabled(no_colors)\n        self.color_by_label.setDisabled(no_colors)\n\n    def _clear_plot(self):\n        self.dendrogram.set_root(None)\n        self.label_model.clear()\n\n    def _set_displayed_root(self, root):\n        self._clear_plot()\n        self._displayed_root = root\n        self.dendrogram.set_root(root)\n        self._update_labels()\n\n    def _update(self):\n        self._clear_plot()\n\n        distances = self.matrix\n\n        if distances is not None:\n            method = LINKAGE_ARGS[self.linkage]\n            Z = dist_matrix_linkage(distances, linkage=method)\n\n            tree = tree_from_linkage(Z)\n            self.linkmatrix = Z\n            self.root = tree\n\n            self.top_axis.setRange(tree.value.height, 0.0)\n            self.bottom_axis.setRange(tree.value.height, 0.0)\n\n            if self.pruning:\n                self._set_displayed_root(prune(tree, level=self.max_depth))\n            else:\n                self._set_displayed_root(tree)\n        else:\n            self.linkmatrix = None\n            self.root = None\n            self._set_displayed_root(None)\n\n        self._apply_selection()\n\n    def _update_labels(self):\n        if not hasattr(self, \"label_model\"):\n            # This method can be called during widget initialization when\n            # creating check box for label_only_subset, if it's value is\n            # initially True.\n            # See https://github.com/biolab/orange-widget-base/pull/213;\n            # if it's merged, this check can be removed.\n            return\n\n        self.Warning.pruning_disables_colors(\n            shown=self.pruning\n                  and (self.subset_rows or self.color_by is not None))\n        labels = []\n        if self.root and self._displayed_root:\n            indices = [leaf.value.index for leaf in leaves(self.root)]\n\n            if self.annotation is None:\n                if not self.pruning \\\n                        and self.subset_rows and self.color_by is None:\n                    # Model fails if number of labels and of colors mismatch\n                    labels = [\"\"] * len(indices)\n                else:\n                    labels = []\n            elif self.annotation == \"Enumeration\":\n                labels = [str(i+1) for i in indices]\n            elif self.annotation == \"Name\":\n                attr = self.matrix.row_items.domain.attributes\n                labels = [str(attr[i]) for i in indices]\n            elif isinstance(self.annotation, Orange.data.Variable):\n                col_data = self.items.get_column(self.annotation)\n                labels = [self.annotation.str_val(val) for val in col_data]\n                labels = [labels[idx] for idx in indices]\n            else:\n                labels = []\n            if not self.pruning and \\\n                    labels and self.label_only_subset and self.subset_rows:\n                labels = [label if row in self.subset_rows else \"\"\n                          for row, label in enumerate(labels)]\n\n            if labels and self._displayed_root is not self.root:\n                joined = leaves(self._displayed_root)\n                labels = [\", \".join(labels[leaf.value.first: leaf.value.last])\n                          for leaf in joined]\n\n        self.label_model[:] = labels\n        self.label_model.set_subset(set() if self.pruning else self.subset_rows)\n        self.labels.setMinimumWidth(1 if labels else -1)\n\n        if not self.pruning and self.color_by is not None:\n            col = self.items.get_column(self.color_by)\n            self.label_model.set_colors(\n                self.color_by.palette.values_to_qcolors(col[indices]))\n        else:\n            self.label_model.set_colors(None)\n\n    def _restore_selection(self, state):\n        # type: (SelectionState) -> bool\n        \"\"\"\n        Restore the (manual) node selection state.\n\n        Return True if successful; False otherwise.\n        \"\"\"\n        linkmatrix = self.linkmatrix\n        if self.selection_method == 0 and self.root:\n            selected, linksaved = state\n            linkstruct = np.array(linksaved, dtype=float)\n            selected = set(selected)  # type: Set[Tuple[int]]\n            if not selected:\n                return False\n            if linkmatrix.shape[0] != linkstruct.shape[0]:\n                return False\n            # check that the linkage matrix structure matches. Use isclose for\n            # the height column to account for inexact floating point math\n            # (e.g. summation order in different ?gemm implementations for\n            # euclidean distances, ...)\n            if np.any(linkstruct[:, :2] != linkmatrix[:, :2]) or \\\n                    not np.all(np.isclose(linkstruct[:, 2], linkstruct[:, 2])):\n                return False\n            selection = []\n            indices = np.array([n.value.index for n in leaves(self.root)],\n                               dtype=int)\n            # mapping from ranges to display (pruned) nodes\n            mapping = {node.value.range: node\n                       for node in postorder(self._displayed_root)}\n            for node in postorder(self.root):  # type: Tree\n                r = tuple(indices[node.value.first: node.value.last])\n                if r in selected:\n                    if node.value.range not in mapping:\n                        # the node was pruned from display and cannot be\n                        # selected\n                        break\n                    selection.append(mapping[node.value.range])\n                    selected.remove(r)\n                if not selected:\n                    break  # found all, nothing more to do\n            if selection and selected:\n                # Could not restore all selected nodes (only partial match)\n                return False\n\n            self._set_selected_nodes(selection)\n            return True\n        return False\n\n    def _set_selected_nodes(self, selection):\n        # type: (List[Tree]) -> None\n        \"\"\"\n        Set the nodes in `selection` to be the current selected nodes.\n\n        The selection nodes must be subtrees of the current `_displayed_root`.\n        \"\"\"\n        self.dendrogram.selectionChanged.disconnect(self._invalidate_output)\n        try:\n            self.dendrogram.set_selected_clusters(selection)\n        finally:\n            self.dendrogram.selectionChanged.connect(self._invalidate_output)\n\n    def _max_depth_return(self):\n        if self.pruning != 1:\n            self.pruning = 1\n            self._invalidate_pruning()\n\n    def _max_depth_changed(self):\n        self.pruning = 1\n        self._invalidate_pruning()\n\n    def _invalidate_clustering(self):\n        self._update()\n        self._update_labels()\n        self._invalidate_output()\n\n    def _invalidate_output(self):\n        self.commit.deferred()\n\n    def _invalidate_pruning(self):\n        if self.root:\n            selection = self.dendrogram.selected_nodes()\n            ranges = [node.value.range for node in selection]\n            if self.pruning:\n                self._set_displayed_root(\n                    prune(self.root, level=self.max_depth))\n            else:\n                self._set_displayed_root(self.root)\n            selected = [node for node in preorder(self._displayed_root)\n                        if node.value.range in ranges]\n\n            self.dendrogram.set_selected_clusters(selected)\n\n        self._apply_selection()\n\n    @gui.deferred\n    def commit(self):\n        items = getattr(self.matrix, \"items\", self.items)\n        self.Warning.many_clusters.clear()\n        if not items:\n            self.Outputs.selected_data.send(None)\n            self.Outputs.annotated_data.send(None)\n            return\n\n        selection = self.dendrogram.selected_nodes()\n        selection = sorted(selection, key=lambda c: c.value.first)\n\n        indices = [leaf.value.index for leaf in leaves(self.root)]\n\n        maps = [indices[node.value.first:node.value.last]\n                for node in selection]\n        if len(maps) > 20:\n            self.Warning.many_clusters()\n\n        selected_indices = list(chain(*maps))\n\n        if not selected_indices:\n            self.Outputs.selected_data.send(None)\n            annotated_data = lazy_annotated_table(items, []) \\\n                if self.selection_method == 0 and self.matrix.axis else None\n            self.Outputs.annotated_data.send(annotated_data)\n            return\n\n        selected_data = annotated_data = None\n\n        if isinstance(items, Orange.data.Table) and self.matrix.axis == 1:\n            # Select rows\n            data, domain = items, items.domain\n\n            c = np.full(self.matrix.shape[0], len(maps))\n            for i, indices in enumerate(maps):\n                c[indices] = i\n\n            clust_name = get_unique_names(domain, \"Cluster\")\n            values = [f\"C{i + 1}\" for i in range(len(maps))]\n\n            sel_clust_var = Orange.data.DiscreteVariable(\n                name=clust_name, values=values)\n            sel_domain = add_columns(domain, metas=(sel_clust_var,))\n            selected_data = LazyValue[Table](\n                lambda: items.add_column(\n                    sel_clust_var, c, to_metas=True)[c != len(maps)],\n                domain=sel_domain, length=len(selected_indices))\n\n            ann_clust_var = Orange.data.DiscreteVariable(\n                name=clust_name, values=values + [\"Other\"]\n            )\n            ann_domain = add_columns(\n                domain_with_annotation_column(data)[0], metas=(ann_clust_var, ))\n            annotated_data = LazyValue[Table](\n                lambda: create_annotated_table(\n                    data=items.add_column(ann_clust_var, c, to_metas=True),\n                    selected_indices=selected_indices),\n                domain=ann_domain, length=len(items)\n            )\n\n        elif isinstance(items, Orange.data.Table) and self.matrix.axis == 0:\n            # Select columns\n            attrs = []\n            unselected_indices = sorted(set(range(self.root.value.last)) -\n                                        set(selected_indices))\n            for clust, indices in chain(enumerate(maps, start=1),\n                                        [(0, unselected_indices)]):\n                for i in indices:\n                    attr = items.domain[i].copy()\n                    attr.attributes[\"cluster\"] = clust\n                    attrs.append(attr)\n            all_domain = Orange.data.Domain(\n                # len(unselected_indices) can be 0\n                attrs[:len(attrs) - len(unselected_indices)],\n                items.domain.class_vars, items.domain.metas)\n\n            selected_data = LazyValue[Table](\n                lambda: items.from_table(all_domain, items),\n                domain=all_domain, length=len(items))\n\n            sel_domain = Orange.data.Domain(\n                attrs,\n                items.domain.class_vars, items.domain.metas)\n            annotated_data = LazyValue[Table](\n                lambda: items.from_table(sel_domain, items),\n                domain=sel_domain, length=len(items))\n\n        self.Outputs.selected_data.send(selected_data)\n        self.Outputs.annotated_data.send(annotated_data)\n\n    @Slot(QPointF)\n    def _activate_cut_line(self, pos: QPointF):\n        \"\"\"Activate cut line selection an set cut value to `pos.x()`.\"\"\"\n        self.selection_method = 1\n        self.cut_line.setValue(pos.x())\n        self._selection_method_changed()\n\n    def onDeleteWidget(self):\n        super().onDeleteWidget()\n        self._clear_plot()\n        self.dendrogram.clear()\n        self.dendrogram.deleteLater()\n\n    def _dendrogram_geom_changed(self):\n        pos = self.dendrogram.pos_at_height(self.cutoff_height)\n        geom = self.dendrogram.geometry()\n        self._set_slider_value(pos.x(), geom.width())\n\n        self.cut_line.setLength(\n            self.bottom_axis.geometry().bottom()\n            - self.top_axis.geometry().top()\n        )\n\n        geom = self._main_graphics.geometry()\n        assert geom.topLeft() == QPointF(0, 0)\n\n        def adjustLeft(rect):\n            rect = QRectF(rect)\n            rect.setLeft(geom.left())\n            return rect\n        margin = 3\n        self.scene.setSceneRect(geom)\n        self.view.setSceneRect(geom)\n        self.view.setHeaderSceneRect(\n            adjustLeft(self.top_axis.geometry()).adjusted(0, 0, 0, margin)\n        )\n        self.view.setFooterSceneRect(\n            adjustLeft(self.bottom_axis.geometry()).adjusted(0, -margin, 0, 0)\n        )\n\n    def _dendrogram_slider_changed(self, value):\n        p = QPointF(value, 0)\n        cl_height = self.dendrogram.height_at(p)\n\n        self.set_cutoff_height(cl_height)\n\n    def _set_slider_value(self, value, span):\n        with blocked(self.cut_line):\n            self.cut_line.setRange(0, span)\n            self.cut_line.setValue(value)\n\n    def set_cutoff_height(self, height):\n        self.cutoff_height = height\n        if self.root:\n            self.cut_ratio = 100 * height / self.root.value.height\n        self.select_max_height(height)\n\n    def _set_cut_line_visible(self, visible):\n        self.cut_line.setVisible(visible)\n\n    def select_top_n(self, n):\n        root = self._displayed_root\n        if root:\n            clusters = top_clusters(root, n)\n            self.dendrogram.set_selected_clusters(clusters)\n\n    def select_max_height(self, height):\n        root = self._displayed_root\n        if root:\n            clusters = clusters_at_height(root, height)\n            self.dendrogram.set_selected_clusters(clusters)\n\n    def _cut_ratio_changed(self):\n        self.selection_method = 1\n        self._selection_method_changed()\n\n    def _cut_ratio_return(self):\n        if self.selection_method != 1:\n            self.selection_method = 1\n            self._selection_method_changed()\n\n    def _top_n_changed(self):\n        self.selection_method = 2\n        self._selection_method_changed()\n\n    def _top_n_return(self):\n        if self.selection_method != 2:\n            self.selection_method = 2\n            self._selection_method_changed()\n\n    def _selection_method_changed(self):\n        self._set_cut_line_visible(self.selection_method == 1)\n        if self.root:\n            self._apply_selection()\n\n    def _apply_selection(self):\n        if not self.root:\n            return\n\n        if self.selection_method == 0:\n            pass\n        elif self.selection_method == 1:\n            height = self.cut_ratio * self.root.value.height / 100\n            self.set_cutoff_height(height)\n            pos = self.dendrogram.pos_at_height(height)\n            self._set_slider_value(pos.x(), self.dendrogram.size().width())\n        elif self.selection_method == 2:\n            self.select_top_n(self.top_n)\n\n    def _selection_edited(self):\n        # Selection was edited by clicking on a cluster in the\n        # dendrogram view.\n        self.selection_method = 0\n        self._selection_method_changed()\n        self._invalidate_output()\n\n    def _save_selection(self):\n        # Save the current manual node selection state\n        selection_state = None\n        if self.selection_method == 0 and self.root:\n            assert self.linkmatrix is not None\n            linkmat = [(int(_0), int(_1), _2)\n                       for _0, _1, _2 in self.linkmatrix[:, :3].tolist()]\n            nodes_ = self.dendrogram.selected_nodes()\n            # match the display (pruned) nodes back (by ranges)\n            mapping = {node.value.range: node for node in postorder(self.root)}\n            nodes = [mapping[node.value.range] for node in nodes_]\n            indices = [tuple(node.value.index for node in leaves(node))\n                       for node in nodes]\n            if nodes:\n                selection_state = (indices, linkmat)\n        return selection_state\n\n    def save_state(self):\n        # type: () -> Dict[str, Any]\n        \"\"\"\n        Save state for `set_restore_state`\n        \"\"\"\n        selection = self._save_selection()\n        res = {\"version\": (0, 0, 0)}\n        if selection is not None:\n            res[\"selection_state\"] = selection\n        return res\n\n    def set_restore_state(self, state):\n        # type: (Dict[str, Any]) -> bool\n        \"\"\"\n        Restore session data from a saved state.\n\n        Parameters\n        ----------\n        state : Dict[str, Any]\n\n        NOTE\n        ----\n        This is method called while the instance (self) is being constructed,\n        even before its `__init__` is called. Consider `self` to be only a\n        `QObject` at this stage.\n        \"\"\"\n        if \"selection_state\" in state:\n            selection = state[\"selection_state\"]\n            self.__pending_selection_restore = selection\n        return True\n\n    def __zoom_in(self):\n        def clip(minval, maxval, val):\n            return min(max(val, minval), maxval)\n        self.zoom_factor = clip(self.zoom_slider.minimum(),\n                                self.zoom_slider.maximum(),\n                                self.zoom_factor + 1)\n        self.__update_font_scale()\n\n    def __zoom_out(self):\n        def clip(minval, maxval, val):\n            return min(max(val, minval), maxval)\n        self.zoom_factor = clip(self.zoom_slider.minimum(),\n                                self.zoom_slider.maximum(),\n                                self.zoom_factor - 1)\n        self.__update_font_scale()\n\n    def __zoom_reset(self):\n        self.zoom_factor = 0\n        self.__update_font_scale()\n\n    def __update_font_scale(self):\n        font = self.scene.font()\n        factor = (1.25 ** self.zoom_factor)\n        font = qfont_scaled(font, factor)\n        self._main_graphics.setFont(font)\n        self.label_model.setFont(font)\n\n    def send_report(self):\n        annot = self.label_cb.currentText()\n        if isinstance(self.annotation, str):\n            annot = annot.lower()\n        if self.selection_method == 0:\n            sel = \"manual\"\n        elif self.selection_method == 1:\n            sel = \"at {:.1f} of height\".format(self.cut_ratio)\n        else:\n            sel = f\"top {self.top_n} {pl(self.top_n, 'cluster')}\"\n        self.report_items((\n            (\"Linkage\", LINKAGE[self.linkage]),\n            (\"Annotation\", annot),\n            (\"Pruning\",\n             self.pruning != 0 and \"{} levels\".format(self.max_depth)),\n            (\"Selection\", sel),\n        ))\n        self.report_plot()\n\n    @classmethod\n    def migrate_context(cls, context, version):\n        if version < 2:\n            if context.values[\"annotation\"] == \"None\":\n                context.values[\"annotation\"] = None",
                                "ast_type_str": "ClassDef",
                                "start_lineno": 177,
                                "end_lineno": 1074,
                                "parent": null
                            }
                        }
                    ],
                    "added_entities": [],
                    "deleted_entities": []
                }
            ]
        },
        {
            "old_file_content": "# Test methods with long descriptive names can omit docstrings\n# pylint: disable=missing-docstring, protected-access\nimport warnings\n\nimport numpy as np\n\nfrom AnyQt.QtCore import QPoint, Qt\nfrom AnyQt.QtTest import QTest\n\nimport Orange.misc\nfrom Orange.data import Table, Domain, ContinuousVariable, DiscreteVariable\nfrom Orange.distance import Euclidean\nfrom Orange.misc import DistMatrix\nfrom Orange.widgets.tests.base import WidgetTest, WidgetOutputsTestMixin\nfrom Orange.widgets.unsupervised.owhierarchicalclustering import \\\n    OWHierarchicalClustering\n\n\nclass TestOWHierarchicalClustering(WidgetTest, WidgetOutputsTestMixin):\n    @classmethod\n    def setUpClass(cls):\n        super().setUpClass()\n        WidgetOutputsTestMixin.init(cls)\n\n        cls.distances = Euclidean(cls.data)\n        cls.signal_name = OWHierarchicalClustering.Inputs.distances\n        cls.signal_data = cls.distances\n        cls.same_input_output_domain = False\n\n        cls.distances_cols = Euclidean(cls.data, axis=0)\n\n    def setUp(self):\n        self.widget = self.create_widget(OWHierarchicalClustering)\n\n    def _select_data(self):\n        items = self.widget.dendrogram._items\n        cluster = items[sorted(list(items.keys()))[4]]\n        self.widget.dendrogram.set_selected_items([cluster])\n        return [14, 15, 32, 33]\n\n    def _select_data_columns(self):\n        items = self.widget.dendrogram._items\n        cluster = items[sorted(list(items.keys()))[5]]\n        self.widget.dendrogram.set_selected_items([cluster])\n\n    def _compare_selected_annotated_domains(self, selected, annotated):\n        self.assertEqual(annotated.domain.variables,\n                         selected.domain.variables)\n        self.assertNotIn(\"Other\", selected.domain.metas[0].values)\n        self.assertIn(\"Other\", annotated.domain.metas[0].values)\n        self.assertLess(set(var.name for var in selected.domain.metas),\n                        set(var.name for var in annotated.domain.metas))\n\n    def test_selection_box_output(self):\n        \"\"\"Check output if Selection method changes\"\"\"\n        self.send_signal(self.widget.Inputs.distances, self.distances)\n        self.assertIsNone(self.get_output(self.widget.Outputs.selected_data))\n        self.assertIsNotNone(self.get_output(self.widget.Outputs.annotated_data))\n\n        # change selection to 'Height ratio'\n        self.widget.selection_box.buttons[1].click()\n        self.assertIsNotNone(self.get_output(self.widget.Outputs.selected_data))\n        self.assertIsNotNone(self.get_output(self.widget.Outputs.annotated_data))\n\n        # change selection to 'Top N'\n        self.widget.selection_box.buttons[2].click()\n        self.assertIsNotNone(self.get_output(self.widget.Outputs.selected_data))\n        self.assertIsNotNone(self.get_output(self.widget.Outputs.annotated_data))\n\n    def test_all_zero_inputs(self):\n        d = Orange.misc.DistMatrix(np.zeros((10, 10)))\n        self.widget.set_distances(d)\n\n    def test_annotation_settings_retrieval(self):\n        \"\"\"Check whether widget retrieves correct settings for annotation\"\"\"\n        widget = self.widget\n\n        dist_names = Orange.misc.DistMatrix(\n            np.zeros((4, 4)), self.data, axis=0)\n        dist_no_names = Orange.misc.DistMatrix(np.zeros((10, 10)), axis=1)\n\n        self.send_signal(self.widget.Inputs.distances, self.distances)\n        # Check that default is set (class variable)\n        self.assertEqual(widget.annotation, self.data.domain.class_var)\n\n        var2 = self.data.domain[2]\n        widget.annotation = var2\n        # Iris now has var2 as annotation\n\n        self.send_signal(self.widget.Inputs.distances, dist_no_names)\n        self.assertEqual(widget.annotation, \"Enumeration\")  # Check default\n        widget.annotation = \"None\"\n        # Pure matrix with axis=1 now has None as annotation\n\n        self.send_signal(self.widget.Inputs.distances, self.distances)\n        self.assertIs(widget.annotation, var2)\n        self.send_signal(self.widget.Inputs.distances, dist_no_names)\n        self.assertEqual(widget.annotation, \"None\")\n\n        self.send_signal(self.widget.Inputs.distances, dist_names)\n        self.assertEqual(widget.annotation, \"Name\")  # Check default\n        widget.annotation = \"Enumeration\"\n        # Pure matrix with axis=1 has Enumerate as annotation\n\n        self.send_signal(self.widget.Inputs.distances, self.distances)\n        self.assertIs(widget.annotation, var2)\n        self.send_signal(self.widget.Inputs.distances, dist_no_names)\n        self.assertEqual(widget.annotation, \"None\")\n        self.send_signal(self.widget.Inputs.distances, dist_names)\n        self.assertEqual(widget.annotation, \"Enumeration\")\n        self.send_signal(self.widget.Inputs.distances, dist_no_names)\n        self.assertEqual(widget.annotation, \"None\")\n\n    def test_domain_loses_class(self):\n        widget = self.widget\n        self.send_signal(self.widget.Inputs.distances, self.distances)\n        data = self.data[:, :4]\n        distances = Euclidean(data)\n        self.send_signal(self.widget.Inputs.distances, distances)\n\n    def test_infinite_distances(self):\n        \"\"\"\n        Scipy does not accept infinite distances and neither does this widget.\n        Error is shown.\n        GH-2380\n        \"\"\"\n        table = Table.from_list(\n            Domain(\n                [ContinuousVariable(\"a\")],\n                [DiscreteVariable(\"b\", values=(\"y\", ))]),\n            list(zip([1.79e308, -1e120],\n                     \"yy\"))\n        )\n        with warnings.catch_warnings():\n            warnings.filterwarnings(\"ignore\", \".*\", RuntimeWarning)\n            distances = Euclidean(table)\n        self.assertFalse(self.widget.Error.not_finite_distances.is_shown())\n        self.send_signal(self.widget.Inputs.distances, distances)\n        self.assertTrue(self.widget.Error.not_finite_distances.is_shown())\n        self.send_signal(self.widget.Inputs.distances, self.distances)\n        self.assertFalse(self.widget.Error.not_finite_distances.is_shown())\n\n    def test_not_symmetric(self):\n        w = self.widget\n        self.send_signal(w.Inputs.distances, DistMatrix([[1, 2, 3], [4, 5, 6]]))\n        self.assertTrue(w.Error.not_symmetric.is_shown())\n        self.send_signal(w.Inputs.distances, None)\n        self.assertFalse(w.Error.not_symmetric.is_shown())\n\n    def test_empty_matrix(self):\n        w = self.widget\n        self.send_signal(w.Inputs.distances, DistMatrix([[]]))\n        self.assertTrue(w.Error.empty_matrix.is_shown())\n        self.send_signal(w.Inputs.distances, None)\n        self.assertFalse(w.Error.empty_matrix.is_shown())\n\n    def test_output_cut_ratio(self):\n        self.send_signal(self.widget.Inputs.distances, self.distances)\n\n        # no data is selected\n        self.assertIsNone(self.get_output(self.widget.Outputs.selected_data))\n        annotated = self.get_output(self.widget.Outputs.annotated_data)\n        self.assertIsNotNone(annotated)\n\n        self.widget.grab()  # Force layout\n        # selecting clusters with cutoff should select all data\n        QTest.mousePress(\n            self.widget.view.headerView().viewport(),\n            Qt.LeftButton, Qt.NoModifier,\n            QPoint(100, 10)\n        )\n        selected = self.get_output(self.widget.Outputs.selected_data)\n        annotated = self.get_output(self.widget.Outputs.annotated_data)\n        self.assertEqual(len(selected), len(self.data))\n        self.assertIsNotNone(annotated)\n\n    def test_retain_selection(self):\n        \"\"\"Hierarchical Clustering didn't retain selection. GH-1563\"\"\"\n        self.send_signal(self.widget.Inputs.distances, self.distances)\n        self._select_data()\n        self.assertIsNotNone(self.get_output(self.widget.Outputs.selected_data))\n        self.send_signal(self.widget.Inputs.distances, self.distances)\n        self.assertIsNotNone(self.get_output(self.widget.Outputs.selected_data))\n\n    def test_restore_state(self):\n        self.send_signal(self.widget.Inputs.distances, self.distances)\n        self._select_data()\n        ids_1 = self.get_output(self.widget.Outputs.selected_data).ids\n        state = self.widget.settingsHandler.pack_data(self.widget)\n        w = self.create_widget(\n            OWHierarchicalClustering, stored_settings=state\n        )\n        self.send_signal(w.Inputs.distances, self.distances, widget=w)\n        ids_2 = self.get_output(w.Outputs.selected_data, widget=w).ids\n        self.assertSequenceEqual(list(ids_1), list(ids_2))\n\n    def test_column_distances(self):\n        self.send_signal(self.widget.Inputs.distances, self.distances_cols)\n        self._select_data_columns()\n        o = self.get_output(self.widget.Outputs.annotated_data)\n        annotated = [(a.name, a.attributes['cluster']) for a in o.domain.attributes]\n        self.assertEqual(annotated, [('sepal width', 1), ('petal length', 1),\n                                     ('sepal length', 0), ('petal width', 0)])\n\n        self.widget.selection_box.buttons[2].click()  # top N\n        o = self.get_output(self.widget.Outputs.annotated_data)\n        annotated = [(a.name, a.attributes['cluster']) for a in o.domain.attributes]\n        self.assertEqual(annotated, [('sepal length', 1), ('petal width', 2),\n                                     ('sepal width', 3), ('petal length', 3)])\n",
            "new_file_content": "# Test methods with long descriptive names can omit docstrings\n# pylint: disable=missing-docstring, protected-access\nimport warnings\n\nimport numpy as np\n\nfrom AnyQt.QtCore import QPoint, Qt\nfrom AnyQt.QtTest import QTest\n\nimport Orange.misc\nfrom Orange.data import Table, Domain, ContinuousVariable, DiscreteVariable\nfrom Orange.distance import Euclidean\nfrom Orange.misc import DistMatrix\nfrom Orange.widgets.tests.base import WidgetTest, WidgetOutputsTestMixin\nfrom Orange.widgets.unsupervised.owhierarchicalclustering import \\\n    OWHierarchicalClustering\n\n\nclass TestOWHierarchicalClustering(WidgetTest, WidgetOutputsTestMixin):\n    @classmethod\n    def setUpClass(cls):\n        super().setUpClass()\n        WidgetOutputsTestMixin.init(cls)\n\n        cls.distances = Euclidean(cls.data)\n        cls.signal_name = OWHierarchicalClustering.Inputs.distances\n        cls.signal_data = cls.distances\n        cls.same_input_output_domain = False\n\n        cls.distances_cols = Euclidean(cls.data, axis=0)\n\n    def setUp(self):\n        self.widget = self.create_widget(OWHierarchicalClustering)\n\n    def _select_data(self):\n        items = self.widget.dendrogram._items\n        cluster = items[sorted(list(items.keys()))[4]]\n        self.widget.dendrogram.set_selected_items([cluster])\n        return [14, 15, 32, 33]\n\n    def _select_data_columns(self):\n        items = self.widget.dendrogram._items\n        cluster = items[sorted(list(items.keys()))[5]]\n        self.widget.dendrogram.set_selected_items([cluster])\n\n    def _compare_selected_annotated_domains(self, selected, annotated):\n        self.assertEqual(annotated.domain.variables,\n                         selected.domain.variables)\n        self.assertNotIn(\"Other\", selected.domain.metas[0].values)\n        self.assertIn(\"Other\", annotated.domain.metas[0].values)\n        self.assertLess(set(var.name for var in selected.domain.metas),\n                        set(var.name for var in annotated.domain.metas))\n\n    def test_selection_box_output(self):\n        \"\"\"Check output if Selection method changes\"\"\"\n        self.send_signal(self.widget.Inputs.distances, self.distances)\n        self.assertIsNone(self.get_output(self.widget.Outputs.selected_data))\n        self.assertIsNotNone(self.get_output(self.widget.Outputs.annotated_data))\n\n        # change selection to 'Height ratio'\n        self.widget.selection_box.buttons[1].click()\n        self.assertIsNotNone(self.get_output(self.widget.Outputs.selected_data))\n        self.assertIsNotNone(self.get_output(self.widget.Outputs.annotated_data))\n\n        # change selection to 'Top N'\n        self.widget.selection_box.buttons[2].click()\n        self.assertIsNotNone(self.get_output(self.widget.Outputs.selected_data))\n        self.assertIsNotNone(self.get_output(self.widget.Outputs.annotated_data))\n\n    def test_all_zero_inputs(self):\n        d = Orange.misc.DistMatrix(np.zeros((10, 10)))\n        self.widget.set_distances(d)\n\n    def test_annotation_settings_retrieval(self):\n        \"\"\"Check whether widget retrieves correct settings for annotation\"\"\"\n        widget = self.widget\n\n        dist_names = Orange.misc.DistMatrix(\n            np.zeros((4, 4)), self.data, axis=0)\n        dist_no_names = Orange.misc.DistMatrix(np.zeros((10, 10)), axis=1)\n\n        self.send_signal(self.widget.Inputs.distances, self.distances)\n        # Check that default is set (class variable)\n        self.assertEqual(widget.annotation, self.data.domain.class_var)\n\n        var2 = self.data.domain[2]\n        widget.annotation = var2\n        # Iris now has var2 as annotation\n\n        self.send_signal(self.widget.Inputs.distances, dist_no_names)\n        self.assertEqual(widget.annotation, \"Enumeration\")  # Check default\n        widget.annotation = \"None\"\n        # Pure matrix with axis=1 now has None as annotation\n\n        self.send_signal(self.widget.Inputs.distances, self.distances)\n        self.assertIs(widget.annotation, var2)\n        self.send_signal(self.widget.Inputs.distances, dist_no_names)\n        self.assertEqual(widget.annotation, \"None\")\n\n        self.send_signal(self.widget.Inputs.distances, dist_names)\n        self.assertEqual(widget.annotation, \"Name\")  # Check default\n        widget.annotation = \"Enumeration\"\n        # Pure matrix with axis=1 has Enumerate as annotation\n\n        self.send_signal(self.widget.Inputs.distances, self.distances)\n        self.assertIs(widget.annotation, var2)\n        self.send_signal(self.widget.Inputs.distances, dist_no_names)\n        self.assertEqual(widget.annotation, \"None\")\n        self.send_signal(self.widget.Inputs.distances, dist_names)\n        self.assertEqual(widget.annotation, \"Enumeration\")\n        self.send_signal(self.widget.Inputs.distances, dist_no_names)\n        self.assertEqual(widget.annotation, \"None\")\n\n    def test_domain_loses_class(self):\n        widget = self.widget\n        self.send_signal(self.widget.Inputs.distances, self.distances)\n        data = self.data[:, :4]\n        distances = Euclidean(data)\n        self.send_signal(self.widget.Inputs.distances, distances)\n\n    def test_infinite_distances(self):\n        \"\"\"\n        Scipy does not accept infinite distances and neither does this widget.\n        Error is shown.\n        GH-2380\n        \"\"\"\n        table = Table.from_list(\n            Domain(\n                [ContinuousVariable(\"a\")],\n                [DiscreteVariable(\"b\", values=(\"y\", ))]),\n            list(zip([1.79e308, -1e120],\n                     \"yy\"))\n        )\n        with warnings.catch_warnings():\n            warnings.filterwarnings(\"ignore\", \".*\", RuntimeWarning)\n            distances = Euclidean(table)\n        self.assertFalse(self.widget.Error.not_finite_distances.is_shown())\n        self.send_signal(self.widget.Inputs.distances, distances)\n        self.assertTrue(self.widget.Error.not_finite_distances.is_shown())\n        self.send_signal(self.widget.Inputs.distances, self.distances)\n        self.assertFalse(self.widget.Error.not_finite_distances.is_shown())\n\n    def test_not_symmetric(self):\n        w = self.widget\n        self.send_signal(w.Inputs.distances, DistMatrix([[1, 2, 3], [4, 5, 6]]))\n        self.assertTrue(w.Error.not_symmetric.is_shown())\n        self.send_signal(w.Inputs.distances, None)\n        self.assertFalse(w.Error.not_symmetric.is_shown())\n\n    def test_empty_matrix(self):\n        w = self.widget\n        self.send_signal(w.Inputs.distances, DistMatrix([[]]))\n        self.assertTrue(w.Error.empty_matrix.is_shown())\n        self.send_signal(w.Inputs.distances, None)\n        self.assertFalse(w.Error.empty_matrix.is_shown())\n\n    def test_output_cut_ratio(self):\n        self.send_signal(self.widget.Inputs.distances, self.distances)\n\n        # no data is selected\n        self.assertIsNone(self.get_output(self.widget.Outputs.selected_data))\n        annotated = self.get_output(self.widget.Outputs.annotated_data)\n        self.assertIsNotNone(annotated)\n\n        self.widget.grab()  # Force layout\n        # selecting clusters with cutoff should select all data\n        QTest.mousePress(\n            self.widget.view.headerView().viewport(),\n            Qt.LeftButton, Qt.NoModifier,\n            QPoint(100, 10)\n        )\n        selected = self.get_output(self.widget.Outputs.selected_data)\n        annotated = self.get_output(self.widget.Outputs.annotated_data)\n        self.assertEqual(len(selected), len(self.data))\n        self.assertIsNotNone(annotated)\n\n    def test_retain_selection(self):\n        \"\"\"Hierarchical Clustering didn't retain selection. GH-1563\"\"\"\n        self.send_signal(self.widget.Inputs.distances, self.distances)\n        self._select_data()\n        self.assertIsNotNone(self.get_output(self.widget.Outputs.selected_data))\n        self.send_signal(self.widget.Inputs.distances, self.distances)\n        self.assertIsNotNone(self.get_output(self.widget.Outputs.selected_data))\n\n    def test_restore_state(self):\n        self.send_signal(self.widget.Inputs.distances, self.distances)\n        self._select_data()\n        ids_1 = self.get_output(self.widget.Outputs.selected_data).ids\n        state = self.widget.settingsHandler.pack_data(self.widget)\n        w = self.create_widget(\n            OWHierarchicalClustering, stored_settings=state\n        )\n        self.send_signal(w.Inputs.distances, self.distances, widget=w)\n        ids_2 = self.get_output(w.Outputs.selected_data, widget=w).ids\n        self.assertSequenceEqual(list(ids_1), list(ids_2))\n\n    def test_column_distances(self):\n        self.send_signal(self.widget.Inputs.distances, self.distances_cols)\n        self._select_data_columns()\n        o = self.get_output(self.widget.Outputs.annotated_data)\n        annotated = [(a.name, a.attributes['cluster']) for a in o.domain.attributes]\n        self.assertEqual(annotated, [('sepal width', 1), ('petal length', 1),\n                                     ('sepal length', 0), ('petal width', 0)])\n\n        self.widget.selection_box.buttons[2].click()  # top N\n        o = self.get_output(self.widget.Outputs.annotated_data)\n        annotated = [(a.name, a.attributes['cluster']) for a in o.domain.attributes]\n        self.assertEqual(annotated, [('sepal length', 1), ('petal width', 2),\n                                     ('sepal width', 3), ('petal length', 3)])\n\n    def test_many_values_warning(self):\n        w = self.widget\n\n        self.send_signal(self.widget.Inputs.distances, self.distances)\n        w.top_n = 21\n        w.selection_box.buttons[2].click()\n        self.assertTrue(w.Warning.many_clusters.is_shown())\n\n        w.top_n = 20\n        w.selection_box.buttons[2].click()\n        self.assertFalse(w.Warning.many_clusters.is_shown())\n\n        w.top_n = 21\n        w.selection_box.buttons[2].click()\n        self.assertTrue(w.Warning.many_clusters.is_shown())\n\n        self.send_signal(self.widget.Inputs.distances, None)\n        self.assertFalse(w.Warning.many_clusters.is_shown())\n",
            "header": {
                "file": {
                    "path": "Orange/widgets/unsupervised/tests/test_owhierarchicalclustering.py"
                },
                "misc_line": null
            },
            "index_line": {
                "old_commit_hash": "c0b977b53",
                "new_commit_hash": "baca8198e",
                "mode": "100644"
            },
            "is_binary_file": false,
            "binary_line": null,
            "minus_file": {
                "path": "a/Orange/widgets/unsupervised/tests/test_owhierarchicalclustering.py"
            },
            "plus_file": {
                "path": "b/Orange/widgets/unsupervised/tests/test_owhierarchicalclustering.py"
            },
            "hunks": [
                {
                    "descriptor": {
                        "old_range": {
                            "start": 207,
                            "length": 3
                        },
                        "new_range": {
                            "start": 207,
                            "length": 22
                        },
                        "section": "class TestOWHierarchicalClustering(WidgetTest, WidgetOutputsTestMixin):"
                    },
                    "line_group": {
                        "all_lines": [
                            {
                                "content": "        annotated = [(a.name, a.attributes['cluster']) for a in o.domain.attributes]",
                                "type": "context"
                            },
                            {
                                "content": "        self.assertEqual(annotated, [('sepal length', 1), ('petal width', 2),",
                                "type": "context"
                            },
                            {
                                "content": "                                     ('sepal width', 3), ('petal length', 3)])",
                                "type": "context"
                            },
                            {
                                "content": "",
                                "type": "added"
                            },
                            {
                                "content": "    def test_many_values_warning(self):",
                                "type": "added"
                            },
                            {
                                "content": "        w = self.widget",
                                "type": "added"
                            },
                            {
                                "content": "",
                                "type": "added"
                            },
                            {
                                "content": "        self.send_signal(self.widget.Inputs.distances, self.distances)",
                                "type": "added"
                            },
                            {
                                "content": "        w.top_n = 21",
                                "type": "added"
                            },
                            {
                                "content": "        w.selection_box.buttons[2].click()",
                                "type": "added"
                            },
                            {
                                "content": "        self.assertTrue(w.Warning.many_clusters.is_shown())",
                                "type": "added"
                            },
                            {
                                "content": "",
                                "type": "added"
                            },
                            {
                                "content": "        w.top_n = 20",
                                "type": "added"
                            },
                            {
                                "content": "        w.selection_box.buttons[2].click()",
                                "type": "added"
                            },
                            {
                                "content": "        self.assertFalse(w.Warning.many_clusters.is_shown())",
                                "type": "added"
                            },
                            {
                                "content": "",
                                "type": "added"
                            },
                            {
                                "content": "        w.top_n = 21",
                                "type": "added"
                            },
                            {
                                "content": "        w.selection_box.buttons[2].click()",
                                "type": "added"
                            },
                            {
                                "content": "        self.assertTrue(w.Warning.many_clusters.is_shown())",
                                "type": "added"
                            },
                            {
                                "content": "",
                                "type": "added"
                            },
                            {
                                "content": "        self.send_signal(self.widget.Inputs.distances, None)",
                                "type": "added"
                            },
                            {
                                "content": "        self.assertFalse(w.Warning.many_clusters.is_shown())",
                                "type": "added"
                            },
                            {
                                "content": "",
                                "type": "context"
                            }
                        ]
                    },
                    "modified_entities": [
                        {
                            "file_name": "Orange/widgets/unsupervised/tests/test_owhierarchicalclustering.py",
                            "type": "class",
                            "name": "TestOWHierarchicalClustering",
                            "content": "class TestOWHierarchicalClustering(WidgetTest, WidgetOutputsTestMixin):\n    @classmethod\n    def setUpClass(cls):\n        super().setUpClass()\n        WidgetOutputsTestMixin.init(cls)\n\n        cls.distances = Euclidean(cls.data)\n        cls.signal_name = OWHierarchicalClustering.Inputs.distances\n        cls.signal_data = cls.distances\n        cls.same_input_output_domain = False\n\n        cls.distances_cols = Euclidean(cls.data, axis=0)\n\n    def setUp(self):\n        self.widget = self.create_widget(OWHierarchicalClustering)\n\n    def _select_data(self):\n        items = self.widget.dendrogram._items\n        cluster = items[sorted(list(items.keys()))[4]]\n        self.widget.dendrogram.set_selected_items([cluster])\n        return [14, 15, 32, 33]\n\n    def _select_data_columns(self):\n        items = self.widget.dendrogram._items\n        cluster = items[sorted(list(items.keys()))[5]]\n        self.widget.dendrogram.set_selected_items([cluster])\n\n    def _compare_selected_annotated_domains(self, selected, annotated):\n        self.assertEqual(annotated.domain.variables,\n                         selected.domain.variables)\n        self.assertNotIn(\"Other\", selected.domain.metas[0].values)\n        self.assertIn(\"Other\", annotated.domain.metas[0].values)\n        self.assertLess(set(var.name for var in selected.domain.metas),\n                        set(var.name for var in annotated.domain.metas))\n\n    def test_selection_box_output(self):\n        \"\"\"Check output if Selection method changes\"\"\"\n        self.send_signal(self.widget.Inputs.distances, self.distances)\n        self.assertIsNone(self.get_output(self.widget.Outputs.selected_data))\n        self.assertIsNotNone(self.get_output(self.widget.Outputs.annotated_data))\n\n        # change selection to 'Height ratio'\n        self.widget.selection_box.buttons[1].click()\n        self.assertIsNotNone(self.get_output(self.widget.Outputs.selected_data))\n        self.assertIsNotNone(self.get_output(self.widget.Outputs.annotated_data))\n\n        # change selection to 'Top N'\n        self.widget.selection_box.buttons[2].click()\n        self.assertIsNotNone(self.get_output(self.widget.Outputs.selected_data))\n        self.assertIsNotNone(self.get_output(self.widget.Outputs.annotated_data))\n\n    def test_all_zero_inputs(self):\n        d = Orange.misc.DistMatrix(np.zeros((10, 10)))\n        self.widget.set_distances(d)\n\n    def test_annotation_settings_retrieval(self):\n        \"\"\"Check whether widget retrieves correct settings for annotation\"\"\"\n        widget = self.widget\n\n        dist_names = Orange.misc.DistMatrix(\n            np.zeros((4, 4)), self.data, axis=0)\n        dist_no_names = Orange.misc.DistMatrix(np.zeros((10, 10)), axis=1)\n\n        self.send_signal(self.widget.Inputs.distances, self.distances)\n        # Check that default is set (class variable)\n        self.assertEqual(widget.annotation, self.data.domain.class_var)\n\n        var2 = self.data.domain[2]\n        widget.annotation = var2\n        # Iris now has var2 as annotation\n\n        self.send_signal(self.widget.Inputs.distances, dist_no_names)\n        self.assertEqual(widget.annotation, \"Enumeration\")  # Check default\n        widget.annotation = \"None\"\n        # Pure matrix with axis=1 now has None as annotation\n\n        self.send_signal(self.widget.Inputs.distances, self.distances)\n        self.assertIs(widget.annotation, var2)\n        self.send_signal(self.widget.Inputs.distances, dist_no_names)\n        self.assertEqual(widget.annotation, \"None\")\n\n        self.send_signal(self.widget.Inputs.distances, dist_names)\n        self.assertEqual(widget.annotation, \"Name\")  # Check default\n        widget.annotation = \"Enumeration\"\n        # Pure matrix with axis=1 has Enumerate as annotation\n\n        self.send_signal(self.widget.Inputs.distances, self.distances)\n        self.assertIs(widget.annotation, var2)\n        self.send_signal(self.widget.Inputs.distances, dist_no_names)\n        self.assertEqual(widget.annotation, \"None\")\n        self.send_signal(self.widget.Inputs.distances, dist_names)\n        self.assertEqual(widget.annotation, \"Enumeration\")\n        self.send_signal(self.widget.Inputs.distances, dist_no_names)\n        self.assertEqual(widget.annotation, \"None\")\n\n    def test_domain_loses_class(self):\n        widget = self.widget\n        self.send_signal(self.widget.Inputs.distances, self.distances)\n        data = self.data[:, :4]\n        distances = Euclidean(data)\n        self.send_signal(self.widget.Inputs.distances, distances)\n\n    def test_infinite_distances(self):\n        \"\"\"\n        Scipy does not accept infinite distances and neither does this widget.\n        Error is shown.\n        GH-2380\n        \"\"\"\n        table = Table.from_list(\n            Domain(\n                [ContinuousVariable(\"a\")],\n                [DiscreteVariable(\"b\", values=(\"y\", ))]),\n            list(zip([1.79e308, -1e120],\n                     \"yy\"))\n        )\n        with warnings.catch_warnings():\n            warnings.filterwarnings(\"ignore\", \".*\", RuntimeWarning)\n            distances = Euclidean(table)\n        self.assertFalse(self.widget.Error.not_finite_distances.is_shown())\n        self.send_signal(self.widget.Inputs.distances, distances)\n        self.assertTrue(self.widget.Error.not_finite_distances.is_shown())\n        self.send_signal(self.widget.Inputs.distances, self.distances)\n        self.assertFalse(self.widget.Error.not_finite_distances.is_shown())\n\n    def test_not_symmetric(self):\n        w = self.widget\n        self.send_signal(w.Inputs.distances, DistMatrix([[1, 2, 3], [4, 5, 6]]))\n        self.assertTrue(w.Error.not_symmetric.is_shown())\n        self.send_signal(w.Inputs.distances, None)\n        self.assertFalse(w.Error.not_symmetric.is_shown())\n\n    def test_empty_matrix(self):\n        w = self.widget\n        self.send_signal(w.Inputs.distances, DistMatrix([[]]))\n        self.assertTrue(w.Error.empty_matrix.is_shown())\n        self.send_signal(w.Inputs.distances, None)\n        self.assertFalse(w.Error.empty_matrix.is_shown())\n\n    def test_output_cut_ratio(self):\n        self.send_signal(self.widget.Inputs.distances, self.distances)\n\n        # no data is selected\n        self.assertIsNone(self.get_output(self.widget.Outputs.selected_data))\n        annotated = self.get_output(self.widget.Outputs.annotated_data)\n        self.assertIsNotNone(annotated)\n\n        self.widget.grab()  # Force layout\n        # selecting clusters with cutoff should select all data\n        QTest.mousePress(\n            self.widget.view.headerView().viewport(),\n            Qt.LeftButton, Qt.NoModifier,\n            QPoint(100, 10)\n        )\n        selected = self.get_output(self.widget.Outputs.selected_data)\n        annotated = self.get_output(self.widget.Outputs.annotated_data)\n        self.assertEqual(len(selected), len(self.data))\n        self.assertIsNotNone(annotated)\n\n    def test_retain_selection(self):\n        \"\"\"Hierarchical Clustering didn't retain selection. GH-1563\"\"\"\n        self.send_signal(self.widget.Inputs.distances, self.distances)\n        self._select_data()\n        self.assertIsNotNone(self.get_output(self.widget.Outputs.selected_data))\n        self.send_signal(self.widget.Inputs.distances, self.distances)\n        self.assertIsNotNone(self.get_output(self.widget.Outputs.selected_data))\n\n    def test_restore_state(self):\n        self.send_signal(self.widget.Inputs.distances, self.distances)\n        self._select_data()\n        ids_1 = self.get_output(self.widget.Outputs.selected_data).ids\n        state = self.widget.settingsHandler.pack_data(self.widget)\n        w = self.create_widget(\n            OWHierarchicalClustering, stored_settings=state\n        )\n        self.send_signal(w.Inputs.distances, self.distances, widget=w)\n        ids_2 = self.get_output(w.Outputs.selected_data, widget=w).ids\n        self.assertSequenceEqual(list(ids_1), list(ids_2))\n\n    def test_column_distances(self):\n        self.send_signal(self.widget.Inputs.distances, self.distances_cols)\n        self._select_data_columns()\n        o = self.get_output(self.widget.Outputs.annotated_data)\n        annotated = [(a.name, a.attributes['cluster']) for a in o.domain.attributes]\n        self.assertEqual(annotated, [('sepal width', 1), ('petal length', 1),\n                                     ('sepal length', 0), ('petal width', 0)])\n\n        self.widget.selection_box.buttons[2].click()  # top N\n        o = self.get_output(self.widget.Outputs.annotated_data)\n        annotated = [(a.name, a.attributes['cluster']) for a in o.domain.attributes]\n        self.assertEqual(annotated, [('sepal length', 1), ('petal width', 2),\n                                     ('sepal width', 3), ('petal length', 3)])\n\n    def test_many_values_warning(self):\n        w = self.widget\n\n        self.send_signal(self.widget.Inputs.distances, self.distances)\n        w.top_n = 21\n        w.selection_box.buttons[2].click()\n        self.assertTrue(w.Warning.many_clusters.is_shown())\n\n        w.top_n = 20\n        w.selection_box.buttons[2].click()\n        self.assertFalse(w.Warning.many_clusters.is_shown())\n\n        w.top_n = 21\n        w.selection_box.buttons[2].click()\n        self.assertTrue(w.Warning.many_clusters.is_shown())\n\n        self.send_signal(self.widget.Inputs.distances, None)\n        self.assertFalse(w.Warning.many_clusters.is_shown())",
                            "ast_type_str": "ClassDef",
                            "start_lineno": 19,
                            "end_lineno": 228,
                            "parent": null
                        }
                    ],
                    "added_entities": [
                        {
                            "file_name": "Orange/widgets/unsupervised/tests/test_owhierarchicalclustering.py",
                            "type": "method",
                            "name": "TestOWHierarchicalClustering.test_many_values_warning",
                            "content": "class TestOWHierarchicalClustering(WidgetTest, WidgetOutputsTestMixin):\n    @classmethod\n    def setUpClass(cls):\n        super().setUpClass()\n        WidgetOutputsTestMixin.init(cls)\n\n        cls.distances = Euclidean(cls.data)\n        cls.signal_name = OWHierarchicalClustering.Inputs.distances\n        cls.signal_data = cls.distances\n        cls.same_input_output_domain = False\n\n        cls.distances_cols = Euclidean(cls.data, axis=0)\n\n    def setUp(self):\n        self.widget = self.create_widget(OWHierarchicalClustering)\n\n    def _select_data(self):\n        items = self.widget.dendrogram._items\n        cluster = items[sorted(list(items.keys()))[4]]\n        self.widget.dendrogram.set_selected_items([cluster])\n        return [14, 15, 32, 33]\n\n    def _select_data_columns(self):\n        items = self.widget.dendrogram._items\n        cluster = items[sorted(list(items.keys()))[5]]\n        self.widget.dendrogram.set_selected_items([cluster])\n\n    def _compare_selected_annotated_domains(self, selected, annotated):\n        self.assertEqual(annotated.domain.variables,\n                         selected.domain.variables)\n        self.assertNotIn(\"Other\", selected.domain.metas[0].values)\n        self.assertIn(\"Other\", annotated.domain.metas[0].values)\n        self.assertLess(set(var.name for var in selected.domain.metas),\n                        set(var.name for var in annotated.domain.metas))\n\n    def test_selection_box_output(self):\n        \"\"\"Check output if Selection method changes\"\"\"\n        self.send_signal(self.widget.Inputs.distances, self.distances)\n        self.assertIsNone(self.get_output(self.widget.Outputs.selected_data))\n        self.assertIsNotNone(self.get_output(self.widget.Outputs.annotated_data))\n\n        # change selection to 'Height ratio'\n        self.widget.selection_box.buttons[1].click()\n        self.assertIsNotNone(self.get_output(self.widget.Outputs.selected_data))\n        self.assertIsNotNone(self.get_output(self.widget.Outputs.annotated_data))\n\n        # change selection to 'Top N'\n        self.widget.selection_box.buttons[2].click()\n        self.assertIsNotNone(self.get_output(self.widget.Outputs.selected_data))\n        self.assertIsNotNone(self.get_output(self.widget.Outputs.annotated_data))\n\n    def test_all_zero_inputs(self):\n        d = Orange.misc.DistMatrix(np.zeros((10, 10)))\n        self.widget.set_distances(d)\n\n    def test_annotation_settings_retrieval(self):\n        \"\"\"Check whether widget retrieves correct settings for annotation\"\"\"\n        widget = self.widget\n\n        dist_names = Orange.misc.DistMatrix(\n            np.zeros((4, 4)), self.data, axis=0)\n        dist_no_names = Orange.misc.DistMatrix(np.zeros((10, 10)), axis=1)\n\n        self.send_signal(self.widget.Inputs.distances, self.distances)\n        # Check that default is set (class variable)\n        self.assertEqual(widget.annotation, self.data.domain.class_var)\n\n        var2 = self.data.domain[2]\n        widget.annotation = var2\n        # Iris now has var2 as annotation\n\n        self.send_signal(self.widget.Inputs.distances, dist_no_names)\n        self.assertEqual(widget.annotation, \"Enumeration\")  # Check default\n        widget.annotation = \"None\"\n        # Pure matrix with axis=1 now has None as annotation\n\n        self.send_signal(self.widget.Inputs.distances, self.distances)\n        self.assertIs(widget.annotation, var2)\n        self.send_signal(self.widget.Inputs.distances, dist_no_names)\n        self.assertEqual(widget.annotation, \"None\")\n\n        self.send_signal(self.widget.Inputs.distances, dist_names)\n        self.assertEqual(widget.annotation, \"Name\")  # Check default\n        widget.annotation = \"Enumeration\"\n        # Pure matrix with axis=1 has Enumerate as annotation\n\n        self.send_signal(self.widget.Inputs.distances, self.distances)\n        self.assertIs(widget.annotation, var2)\n        self.send_signal(self.widget.Inputs.distances, dist_no_names)\n        self.assertEqual(widget.annotation, \"None\")\n        self.send_signal(self.widget.Inputs.distances, dist_names)\n        self.assertEqual(widget.annotation, \"Enumeration\")\n        self.send_signal(self.widget.Inputs.distances, dist_no_names)\n        self.assertEqual(widget.annotation, \"None\")\n\n    def test_domain_loses_class(self):\n        widget = self.widget\n        self.send_signal(self.widget.Inputs.distances, self.distances)\n        data = self.data[:, :4]\n        distances = Euclidean(data)\n        self.send_signal(self.widget.Inputs.distances, distances)\n\n    def test_infinite_distances(self):\n        \"\"\"\n        Scipy does not accept infinite distances and neither does this widget.\n        Error is shown.\n        GH-2380\n        \"\"\"\n        table = Table.from_list(\n            Domain(\n                [ContinuousVariable(\"a\")],\n                [DiscreteVariable(\"b\", values=(\"y\", ))]),\n            list(zip([1.79e308, -1e120],\n                     \"yy\"))\n        )\n        with warnings.catch_warnings():\n            warnings.filterwarnings(\"ignore\", \".*\", RuntimeWarning)\n            distances = Euclidean(table)\n        self.assertFalse(self.widget.Error.not_finite_distances.is_shown())\n        self.send_signal(self.widget.Inputs.distances, distances)\n        self.assertTrue(self.widget.Error.not_finite_distances.is_shown())\n        self.send_signal(self.widget.Inputs.distances, self.distances)\n        self.assertFalse(self.widget.Error.not_finite_distances.is_shown())\n\n    def test_not_symmetric(self):\n        w = self.widget\n        self.send_signal(w.Inputs.distances, DistMatrix([[1, 2, 3], [4, 5, 6]]))\n        self.assertTrue(w.Error.not_symmetric.is_shown())\n        self.send_signal(w.Inputs.distances, None)\n        self.assertFalse(w.Error.not_symmetric.is_shown())\n\n    def test_empty_matrix(self):\n        w = self.widget\n        self.send_signal(w.Inputs.distances, DistMatrix([[]]))\n        self.assertTrue(w.Error.empty_matrix.is_shown())\n        self.send_signal(w.Inputs.distances, None)\n        self.assertFalse(w.Error.empty_matrix.is_shown())\n\n    def test_output_cut_ratio(self):\n        self.send_signal(self.widget.Inputs.distances, self.distances)\n\n        # no data is selected\n        self.assertIsNone(self.get_output(self.widget.Outputs.selected_data))\n        annotated = self.get_output(self.widget.Outputs.annotated_data)\n        self.assertIsNotNone(annotated)\n\n        self.widget.grab()  # Force layout\n        # selecting clusters with cutoff should select all data\n        QTest.mousePress(\n            self.widget.view.headerView().viewport(),\n            Qt.LeftButton, Qt.NoModifier,\n            QPoint(100, 10)\n        )\n        selected = self.get_output(self.widget.Outputs.selected_data)\n        annotated = self.get_output(self.widget.Outputs.annotated_data)\n        self.assertEqual(len(selected), len(self.data))\n        self.assertIsNotNone(annotated)\n\n    def test_retain_selection(self):\n        \"\"\"Hierarchical Clustering didn't retain selection. GH-1563\"\"\"\n        self.send_signal(self.widget.Inputs.distances, self.distances)\n        self._select_data()\n        self.assertIsNotNone(self.get_output(self.widget.Outputs.selected_data))\n        self.send_signal(self.widget.Inputs.distances, self.distances)\n        self.assertIsNotNone(self.get_output(self.widget.Outputs.selected_data))\n\n    def test_restore_state(self):\n        self.send_signal(self.widget.Inputs.distances, self.distances)\n        self._select_data()\n        ids_1 = self.get_output(self.widget.Outputs.selected_data).ids\n        state = self.widget.settingsHandler.pack_data(self.widget)\n        w = self.create_widget(\n            OWHierarchicalClustering, stored_settings=state\n        )\n        self.send_signal(w.Inputs.distances, self.distances, widget=w)\n        ids_2 = self.get_output(w.Outputs.selected_data, widget=w).ids\n        self.assertSequenceEqual(list(ids_1), list(ids_2))\n\n    def test_column_distances(self):\n        self.send_signal(self.widget.Inputs.distances, self.distances_cols)\n        self._select_data_columns()\n        o = self.get_output(self.widget.Outputs.annotated_data)\n        annotated = [(a.name, a.attributes['cluster']) for a in o.domain.attributes]\n        self.assertEqual(annotated, [('sepal width', 1), ('petal length', 1),\n                                     ('sepal length', 0), ('petal width', 0)])\n\n        self.widget.selection_box.buttons[2].click()  # top N\n        o = self.get_output(self.widget.Outputs.annotated_data)\n        annotated = [(a.name, a.attributes['cluster']) for a in o.domain.attributes]\n        self.assertEqual(annotated, [('sepal length', 1), ('petal width', 2),\n                                     ('sepal width', 3), ('petal length', 3)])\n\n    def test_many_values_warning(self):\n        w = self.widget\n\n        self.send_signal(self.widget.Inputs.distances, self.distances)\n        w.top_n = 21\n        w.selection_box.buttons[2].click()\n        self.assertTrue(w.Warning.many_clusters.is_shown())\n\n        w.top_n = 20\n        w.selection_box.buttons[2].click()\n        self.assertFalse(w.Warning.many_clusters.is_shown())\n\n        w.top_n = 21\n        w.selection_box.buttons[2].click()\n        self.assertTrue(w.Warning.many_clusters.is_shown())\n\n        self.send_signal(self.widget.Inputs.distances, None)\n        self.assertFalse(w.Warning.many_clusters.is_shown())",
                            "ast_type_str": "ClassDef",
                            "start_lineno": 211,
                            "end_lineno": 228,
                            "parent": {
                                "file_name": "Orange/widgets/unsupervised/tests/test_owhierarchicalclustering.py",
                                "type": "class",
                                "name": "TestOWHierarchicalClustering",
                                "content": "class TestOWHierarchicalClustering(WidgetTest, WidgetOutputsTestMixin):\n    @classmethod\n    def setUpClass(cls):\n        super().setUpClass()\n        WidgetOutputsTestMixin.init(cls)\n\n        cls.distances = Euclidean(cls.data)\n        cls.signal_name = OWHierarchicalClustering.Inputs.distances\n        cls.signal_data = cls.distances\n        cls.same_input_output_domain = False\n\n        cls.distances_cols = Euclidean(cls.data, axis=0)\n\n    def setUp(self):\n        self.widget = self.create_widget(OWHierarchicalClustering)\n\n    def _select_data(self):\n        items = self.widget.dendrogram._items\n        cluster = items[sorted(list(items.keys()))[4]]\n        self.widget.dendrogram.set_selected_items([cluster])\n        return [14, 15, 32, 33]\n\n    def _select_data_columns(self):\n        items = self.widget.dendrogram._items\n        cluster = items[sorted(list(items.keys()))[5]]\n        self.widget.dendrogram.set_selected_items([cluster])\n\n    def _compare_selected_annotated_domains(self, selected, annotated):\n        self.assertEqual(annotated.domain.variables,\n                         selected.domain.variables)\n        self.assertNotIn(\"Other\", selected.domain.metas[0].values)\n        self.assertIn(\"Other\", annotated.domain.metas[0].values)\n        self.assertLess(set(var.name for var in selected.domain.metas),\n                        set(var.name for var in annotated.domain.metas))\n\n    def test_selection_box_output(self):\n        \"\"\"Check output if Selection method changes\"\"\"\n        self.send_signal(self.widget.Inputs.distances, self.distances)\n        self.assertIsNone(self.get_output(self.widget.Outputs.selected_data))\n        self.assertIsNotNone(self.get_output(self.widget.Outputs.annotated_data))\n\n        # change selection to 'Height ratio'\n        self.widget.selection_box.buttons[1].click()\n        self.assertIsNotNone(self.get_output(self.widget.Outputs.selected_data))\n        self.assertIsNotNone(self.get_output(self.widget.Outputs.annotated_data))\n\n        # change selection to 'Top N'\n        self.widget.selection_box.buttons[2].click()\n        self.assertIsNotNone(self.get_output(self.widget.Outputs.selected_data))\n        self.assertIsNotNone(self.get_output(self.widget.Outputs.annotated_data))\n\n    def test_all_zero_inputs(self):\n        d = Orange.misc.DistMatrix(np.zeros((10, 10)))\n        self.widget.set_distances(d)\n\n    def test_annotation_settings_retrieval(self):\n        \"\"\"Check whether widget retrieves correct settings for annotation\"\"\"\n        widget = self.widget\n\n        dist_names = Orange.misc.DistMatrix(\n            np.zeros((4, 4)), self.data, axis=0)\n        dist_no_names = Orange.misc.DistMatrix(np.zeros((10, 10)), axis=1)\n\n        self.send_signal(self.widget.Inputs.distances, self.distances)\n        # Check that default is set (class variable)\n        self.assertEqual(widget.annotation, self.data.domain.class_var)\n\n        var2 = self.data.domain[2]\n        widget.annotation = var2\n        # Iris now has var2 as annotation\n\n        self.send_signal(self.widget.Inputs.distances, dist_no_names)\n        self.assertEqual(widget.annotation, \"Enumeration\")  # Check default\n        widget.annotation = \"None\"\n        # Pure matrix with axis=1 now has None as annotation\n\n        self.send_signal(self.widget.Inputs.distances, self.distances)\n        self.assertIs(widget.annotation, var2)\n        self.send_signal(self.widget.Inputs.distances, dist_no_names)\n        self.assertEqual(widget.annotation, \"None\")\n\n        self.send_signal(self.widget.Inputs.distances, dist_names)\n        self.assertEqual(widget.annotation, \"Name\")  # Check default\n        widget.annotation = \"Enumeration\"\n        # Pure matrix with axis=1 has Enumerate as annotation\n\n        self.send_signal(self.widget.Inputs.distances, self.distances)\n        self.assertIs(widget.annotation, var2)\n        self.send_signal(self.widget.Inputs.distances, dist_no_names)\n        self.assertEqual(widget.annotation, \"None\")\n        self.send_signal(self.widget.Inputs.distances, dist_names)\n        self.assertEqual(widget.annotation, \"Enumeration\")\n        self.send_signal(self.widget.Inputs.distances, dist_no_names)\n        self.assertEqual(widget.annotation, \"None\")\n\n    def test_domain_loses_class(self):\n        widget = self.widget\n        self.send_signal(self.widget.Inputs.distances, self.distances)\n        data = self.data[:, :4]\n        distances = Euclidean(data)\n        self.send_signal(self.widget.Inputs.distances, distances)\n\n    def test_infinite_distances(self):\n        \"\"\"\n        Scipy does not accept infinite distances and neither does this widget.\n        Error is shown.\n        GH-2380\n        \"\"\"\n        table = Table.from_list(\n            Domain(\n                [ContinuousVariable(\"a\")],\n                [DiscreteVariable(\"b\", values=(\"y\", ))]),\n            list(zip([1.79e308, -1e120],\n                     \"yy\"))\n        )\n        with warnings.catch_warnings():\n            warnings.filterwarnings(\"ignore\", \".*\", RuntimeWarning)\n            distances = Euclidean(table)\n        self.assertFalse(self.widget.Error.not_finite_distances.is_shown())\n        self.send_signal(self.widget.Inputs.distances, distances)\n        self.assertTrue(self.widget.Error.not_finite_distances.is_shown())\n        self.send_signal(self.widget.Inputs.distances, self.distances)\n        self.assertFalse(self.widget.Error.not_finite_distances.is_shown())\n\n    def test_not_symmetric(self):\n        w = self.widget\n        self.send_signal(w.Inputs.distances, DistMatrix([[1, 2, 3], [4, 5, 6]]))\n        self.assertTrue(w.Error.not_symmetric.is_shown())\n        self.send_signal(w.Inputs.distances, None)\n        self.assertFalse(w.Error.not_symmetric.is_shown())\n\n    def test_empty_matrix(self):\n        w = self.widget\n        self.send_signal(w.Inputs.distances, DistMatrix([[]]))\n        self.assertTrue(w.Error.empty_matrix.is_shown())\n        self.send_signal(w.Inputs.distances, None)\n        self.assertFalse(w.Error.empty_matrix.is_shown())\n\n    def test_output_cut_ratio(self):\n        self.send_signal(self.widget.Inputs.distances, self.distances)\n\n        # no data is selected\n        self.assertIsNone(self.get_output(self.widget.Outputs.selected_data))\n        annotated = self.get_output(self.widget.Outputs.annotated_data)\n        self.assertIsNotNone(annotated)\n\n        self.widget.grab()  # Force layout\n        # selecting clusters with cutoff should select all data\n        QTest.mousePress(\n            self.widget.view.headerView().viewport(),\n            Qt.LeftButton, Qt.NoModifier,\n            QPoint(100, 10)\n        )\n        selected = self.get_output(self.widget.Outputs.selected_data)\n        annotated = self.get_output(self.widget.Outputs.annotated_data)\n        self.assertEqual(len(selected), len(self.data))\n        self.assertIsNotNone(annotated)\n\n    def test_retain_selection(self):\n        \"\"\"Hierarchical Clustering didn't retain selection. GH-1563\"\"\"\n        self.send_signal(self.widget.Inputs.distances, self.distances)\n        self._select_data()\n        self.assertIsNotNone(self.get_output(self.widget.Outputs.selected_data))\n        self.send_signal(self.widget.Inputs.distances, self.distances)\n        self.assertIsNotNone(self.get_output(self.widget.Outputs.selected_data))\n\n    def test_restore_state(self):\n        self.send_signal(self.widget.Inputs.distances, self.distances)\n        self._select_data()\n        ids_1 = self.get_output(self.widget.Outputs.selected_data).ids\n        state = self.widget.settingsHandler.pack_data(self.widget)\n        w = self.create_widget(\n            OWHierarchicalClustering, stored_settings=state\n        )\n        self.send_signal(w.Inputs.distances, self.distances, widget=w)\n        ids_2 = self.get_output(w.Outputs.selected_data, widget=w).ids\n        self.assertSequenceEqual(list(ids_1), list(ids_2))\n\n    def test_column_distances(self):\n        self.send_signal(self.widget.Inputs.distances, self.distances_cols)\n        self._select_data_columns()\n        o = self.get_output(self.widget.Outputs.annotated_data)\n        annotated = [(a.name, a.attributes['cluster']) for a in o.domain.attributes]\n        self.assertEqual(annotated, [('sepal width', 1), ('petal length', 1),\n                                     ('sepal length', 0), ('petal width', 0)])\n\n        self.widget.selection_box.buttons[2].click()  # top N\n        o = self.get_output(self.widget.Outputs.annotated_data)\n        annotated = [(a.name, a.attributes['cluster']) for a in o.domain.attributes]\n        self.assertEqual(annotated, [('sepal length', 1), ('petal width', 2),\n                                     ('sepal width', 3), ('petal length', 3)])\n\n    def test_many_values_warning(self):\n        w = self.widget\n\n        self.send_signal(self.widget.Inputs.distances, self.distances)\n        w.top_n = 21\n        w.selection_box.buttons[2].click()\n        self.assertTrue(w.Warning.many_clusters.is_shown())\n\n        w.top_n = 20\n        w.selection_box.buttons[2].click()\n        self.assertFalse(w.Warning.many_clusters.is_shown())\n\n        w.top_n = 21\n        w.selection_box.buttons[2].click()\n        self.assertTrue(w.Warning.many_clusters.is_shown())\n\n        self.send_signal(self.widget.Inputs.distances, None)\n        self.assertFalse(w.Warning.many_clusters.is_shown())",
                                "ast_type_str": "ClassDef",
                                "start_lineno": 19,
                                "end_lineno": 228,
                                "parent": null
                            }
                        }
                    ],
                    "deleted_entities": []
                }
            ]
        }
    ],
    "old_commit_hash": "2a6f878a820b39fb58a57d023999d8246a982074^",
    "new_commit_hash": "2a6f878a820b39fb58a57d023999d8246a982074",
    "commit_message": "Hierarchical clustering: Allow more than 20 clusters in Top n spin",
    "commit_date": "2024-09-06T14:16:56+02:00",
    "metadata": {}
}
{
    "repo_name": "orange3",
    "new_commit_hash": "2a6f878a820b39fb58a57d023999d8246a982074",
    "test_file_codes": [
        "# Test methods with long descriptive names can omit docstrings\n# pylint: disable=missing-docstring, protected-access\nimport warnings\n\nimport numpy as np\n\nfrom AnyQt.QtCore import QPoint, Qt\nfrom AnyQt.QtTest import QTest\n\nimport Orange.misc\nfrom Orange.data import Table, Domain, ContinuousVariable, DiscreteVariable\nfrom Orange.distance import Euclidean\nfrom Orange.misc import DistMatrix\nfrom Orange.widgets.tests.base import WidgetTest, WidgetOutputsTestMixin\nfrom Orange.widgets.unsupervised.owhierarchicalclustering import \\\n    OWHierarchicalClustering\n\n\nclass TestOWHierarchicalClustering(WidgetTest, WidgetOutputsTestMixin):\n    @classmethod\n    def setUpClass(cls):\n        super().setUpClass()\n        WidgetOutputsTestMixin.init(cls)\n\n        cls.distances = Euclidean(cls.data)\n        cls.signal_name = OWHierarchicalClustering.Inputs.distances\n        cls.signal_data = cls.distances\n        cls.same_input_output_domain = False\n\n        cls.distances_cols = Euclidean(cls.data, axis=0)\n\n    def setUp(self):\n        self.widget = self.create_widget(OWHierarchicalClustering)\n\n    def _select_data(self):\n        items = self.widget.dendrogram._items\n        cluster = items[sorted(list(items.keys()))[4]]\n        self.widget.dendrogram.set_selected_items([cluster])\n        return [14, 15, 32, 33]\n\n    def _select_data_columns(self):\n        items = self.widget.dendrogram._items\n        cluster = items[sorted(list(items.keys()))[5]]\n        self.widget.dendrogram.set_selected_items([cluster])\n\n    def _compare_selected_annotated_domains(self, selected, annotated):\n        self.assertEqual(annotated.domain.variables,\n                         selected.domain.variables)\n        self.assertNotIn(\"Other\", selected.domain.metas[0].values)\n        self.assertIn(\"Other\", annotated.domain.metas[0].values)\n        self.assertLess(set(var.name for var in selected.domain.metas),\n                        set(var.name for var in annotated.domain.metas))\n\n    def test_selection_box_output(self):\n        \"\"\"Check output if Selection method changes\"\"\"\n        self.send_signal(self.widget.Inputs.distances, self.distances)\n        self.assertIsNone(self.get_output(self.widget.Outputs.selected_data))\n        self.assertIsNotNone(self.get_output(self.widget.Outputs.annotated_data))\n\n        # change selection to 'Height ratio'\n        self.widget.selection_box.buttons[1].click()\n        self.assertIsNotNone(self.get_output(self.widget.Outputs.selected_data))\n        self.assertIsNotNone(self.get_output(self.widget.Outputs.annotated_data))\n\n        # change selection to 'Top N'\n        self.widget.selection_box.buttons[2].click()\n        self.assertIsNotNone(self.get_output(self.widget.Outputs.selected_data))\n        self.assertIsNotNone(self.get_output(self.widget.Outputs.annotated_data))\n\n    def test_all_zero_inputs(self):\n        d = Orange.misc.DistMatrix(np.zeros((10, 10)))\n        self.widget.set_distances(d)\n\n    def test_annotation_settings_retrieval(self):\n        \"\"\"Check whether widget retrieves correct settings for annotation\"\"\"\n        widget = self.widget\n\n        dist_names = Orange.misc.DistMatrix(\n            np.zeros((4, 4)), self.data, axis=0)\n        dist_no_names = Orange.misc.DistMatrix(np.zeros((10, 10)), axis=1)\n\n        self.send_signal(self.widget.Inputs.distances, self.distances)\n        # Check that default is set (class variable)\n        self.assertEqual(widget.annotation, self.data.domain.class_var)\n\n        var2 = self.data.domain[2]\n        widget.annotation = var2\n        # Iris now has var2 as annotation\n\n        self.send_signal(self.widget.Inputs.distances, dist_no_names)\n        self.assertEqual(widget.annotation, \"Enumeration\")  # Check default\n        widget.annotation = \"None\"\n        # Pure matrix with axis=1 now has None as annotation\n\n        self.send_signal(self.widget.Inputs.distances, self.distances)\n        self.assertIs(widget.annotation, var2)\n        self.send_signal(self.widget.Inputs.distances, dist_no_names)\n        self.assertEqual(widget.annotation, \"None\")\n\n        self.send_signal(self.widget.Inputs.distances, dist_names)\n        self.assertEqual(widget.annotation, \"Name\")  # Check default\n        widget.annotation = \"Enumeration\"\n        # Pure matrix with axis=1 has Enumerate as annotation\n\n        self.send_signal(self.widget.Inputs.distances, self.distances)\n        self.assertIs(widget.annotation, var2)\n        self.send_signal(self.widget.Inputs.distances, dist_no_names)\n        self.assertEqual(widget.annotation, \"None\")\n        self.send_signal(self.widget.Inputs.distances, dist_names)\n        self.assertEqual(widget.annotation, \"Enumeration\")\n        self.send_signal(self.widget.Inputs.distances, dist_no_names)\n        self.assertEqual(widget.annotation, \"None\")\n\n    def test_domain_loses_class(self):\n        widget = self.widget\n        self.send_signal(self.widget.Inputs.distances, self.distances)\n        data = self.data[:, :4]\n        distances = Euclidean(data)\n        self.send_signal(self.widget.Inputs.distances, distances)\n\n    def test_infinite_distances(self):\n        \"\"\"\n        Scipy does not accept infinite distances and neither does this widget.\n        Error is shown.\n        GH-2380\n        \"\"\"\n        table = Table.from_list(\n            Domain(\n                [ContinuousVariable(\"a\")],\n                [DiscreteVariable(\"b\", values=(\"y\", ))]),\n            list(zip([1.79e308, -1e120],\n                     \"yy\"))\n        )\n        with warnings.catch_warnings():\n            warnings.filterwarnings(\"ignore\", \".*\", RuntimeWarning)\n            distances = Euclidean(table)\n        self.assertFalse(self.widget.Error.not_finite_distances.is_shown())\n        self.send_signal(self.widget.Inputs.distances, distances)\n        self.assertTrue(self.widget.Error.not_finite_distances.is_shown())\n        self.send_signal(self.widget.Inputs.distances, self.distances)\n        self.assertFalse(self.widget.Error.not_finite_distances.is_shown())\n\n    def test_not_symmetric(self):\n        w = self.widget\n        self.send_signal(w.Inputs.distances, DistMatrix([[1, 2, 3], [4, 5, 6]]))\n        self.assertTrue(w.Error.not_symmetric.is_shown())\n        self.send_signal(w.Inputs.distances, None)\n        self.assertFalse(w.Error.not_symmetric.is_shown())\n\n    def test_empty_matrix(self):\n        w = self.widget\n        self.send_signal(w.Inputs.distances, DistMatrix([[]]))\n        self.assertTrue(w.Error.empty_matrix.is_shown())\n        self.send_signal(w.Inputs.distances, None)\n        self.assertFalse(w.Error.empty_matrix.is_shown())\n\n    def test_output_cut_ratio(self):\n        self.send_signal(self.widget.Inputs.distances, self.distances)\n\n        # no data is selected\n        self.assertIsNone(self.get_output(self.widget.Outputs.selected_data))\n        annotated = self.get_output(self.widget.Outputs.annotated_data)\n        self.assertIsNotNone(annotated)\n\n        self.widget.grab()  # Force layout\n        # selecting clusters with cutoff should select all data\n        QTest.mousePress(\n            self.widget.view.headerView().viewport(),\n            Qt.LeftButton, Qt.NoModifier,\n            QPoint(100, 10)\n        )\n        selected = self.get_output(self.widget.Outputs.selected_data)\n        annotated = self.get_output(self.widget.Outputs.annotated_data)\n        self.assertEqual(len(selected), len(self.data))\n        self.assertIsNotNone(annotated)\n\n    def test_retain_selection(self):\n        \"\"\"Hierarchical Clustering didn't retain selection. GH-1563\"\"\"\n        self.send_signal(self.widget.Inputs.distances, self.distances)\n        self._select_data()\n        self.assertIsNotNone(self.get_output(self.widget.Outputs.selected_data))\n        self.send_signal(self.widget.Inputs.distances, self.distances)\n        self.assertIsNotNone(self.get_output(self.widget.Outputs.selected_data))\n\n    def test_restore_state(self):\n        self.send_signal(self.widget.Inputs.distances, self.distances)\n        self._select_data()\n        ids_1 = self.get_output(self.widget.Outputs.selected_data).ids\n        state = self.widget.settingsHandler.pack_data(self.widget)\n        w = self.create_widget(\n            OWHierarchicalClustering, stored_settings=state\n        )\n        self.send_signal(w.Inputs.distances, self.distances, widget=w)\n        ids_2 = self.get_output(w.Outputs.selected_data, widget=w).ids\n        self.assertSequenceEqual(list(ids_1), list(ids_2))\n\n    def test_column_distances(self):\n        self.send_signal(self.widget.Inputs.distances, self.distances_cols)\n        self._select_data_columns()\n        o = self.get_output(self.widget.Outputs.annotated_data)\n        annotated = [(a.name, a.attributes['cluster']) for a in o.domain.attributes]\n        self.assertEqual(annotated, [('sepal width', 1), ('petal length', 1),\n                                     ('sepal length', 0), ('petal width', 0)])\n\n        self.widget.selection_box.buttons[2].click()  # top N\n        o = self.get_output(self.widget.Outputs.annotated_data)\n        annotated = [(a.name, a.attributes['cluster']) for a in o.domain.attributes]\n        self.assertEqual(annotated, [('sepal length', 1), ('petal width', 2),\n                                     ('sepal width', 3), ('petal length', 3)])\n\n    def test_many_values_warning(self):\n        w = self.widget\n\n        self.send_signal(self.widget.Inputs.distances, self.distances)\n        w.top_n = 21\n        w.selection_box.buttons[2].click()\n        self.assertTrue(w.Warning.many_clusters.is_shown())\n\n        w.top_n = 20\n        w.selection_box.buttons[2].click()\n        self.assertFalse(w.Warning.many_clusters.is_shown())\n\n        w.top_n = 21\n        w.selection_box.buttons[2].click()\n        self.assertTrue(w.Warning.many_clusters.is_shown())\n\n        self.send_signal(self.widget.Inputs.distances, None)\n        self.assertFalse(w.Warning.many_clusters.is_shown())\n"
    ],
    "test_file_names": [
        "test_1.py"
    ],
    "setup_res_code": 0,
    "setup_res_stdout": "Starting Orange installation attempts...\nAttempting installation with Python 3.7...\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\nrunning build_ext\nbuilding 'Orange.classification._simple_tree' 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_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.7/site-packages/numpy/core/include -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.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\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/_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_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.7/site-packages/numpy/core/include -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.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\nbuilding 'Orange.classification._tree_scorers' 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_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.7/site-packages/numpy/core/include -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.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\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_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.7/site-packages/numpy/core/include -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.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_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.7/site-packages/numpy/core/include -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.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_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.7/site-packages/numpy/core/include -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.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_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.7/site-packages/numpy/core/include -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.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_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.7/site-packages/numpy/core/include -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.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_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.7/site-packages/numpy/core/include -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.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_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.7/site-packages/numpy/core/include -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.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_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.7/site-packages/numpy/core/include -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.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\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\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\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/_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\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\nCreating /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.7/site-packages/Orange3.egg-link (link to .)\nAdding Orange3 3.38.0.dev0+2a6f878 to easy-install.pth file\nInstalling orange-canvas script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/bin\n\nInstalled /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074\nProcessing dependencies for Orange3==3.38.0.dev0+2a6f878\nSearching for xlsxwriter\nReading https://pypi.org/simple/xlsxwriter/\nDownloading https://files.pythonhosted.org/packages/a7/ea/53d1fe468e63e092cf16e2c18d16f50c29851242f9dd12d6a66e0d7f0d02/XlsxWriter-3.2.0-py3-none-any.whl#sha256=ecfd5405b3e0e228219bcaf24c2ca0915e012ca9464a14048021d21a995d490e\nBest match: XlsxWriter 3.2.0\nProcessing XlsxWriter-3.2.0-py3-none-any.whl\nInstalling XlsxWriter-3.2.0-py3-none-any.whl to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.7/site-packages\nAdding XlsxWriter 3.2.0 to easy-install.pth file\nInstalling vba_extract.py script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/bin\n\nInstalled /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.7/site-packages/XlsxWriter-3.2.0-py3.7.egg\nSearching for xlrd>=1.2.0\nReading https://pypi.org/simple/xlrd/\nDownloading https://files.pythonhosted.org/packages/a6/0c/c2a72d51fe56e08a08acc85d13013558a2d793028ae7385448a6ccdfae64/xlrd-2.0.1-py2.py3-none-any.whl#sha256=6a33ee89877bd9abc1158129f6e94be74e2679636b8a205b43b85206c3f0bbdd\nBest match: xlrd 2.0.1\nProcessing xlrd-2.0.1-py2.py3-none-any.whl\nInstalling xlrd-2.0.1-py2.py3-none-any.whl to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.7/site-packages\nAdding xlrd 2.0.1 to easy-install.pth file\nInstalling runxlrd.py script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/bin\n\nInstalled /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.7/site-packages/xlrd-2.0.1-py3.7.egg\nSearching for xgboost<2.1,>=1.7.4\nReading https://pypi.org/simple/xgboost/\nDownloading https://files.pythonhosted.org/packages/c3/eb/496aa2f5d356af4185f770bc76055307f8d1870e11016b10fd779b21769c/xgboost-2.0.3-py3-none-manylinux2014_x86_64.whl#sha256=30bd5f789fad467fd49e04e5d19e04238b931682c3951a514da5c2410b3bf59c\nBest match: xgboost 2.0.3\nProcessing xgboost-2.0.3-py3-none-manylinux2014_x86_64.whl\nInstalling xgboost-2.0.3-py3-none-manylinux2014_x86_64.whl to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.7/site-packages\nAdding xgboost 2.0.3 to easy-install.pth file\n\nInstalled /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.7/site-packages/xgboost-2.0.3-py3.7-linux-x86_64.egg\nSearching for serverfiles\nReading https://pypi.org/simple/serverfiles/\nDownloading https://files.pythonhosted.org/packages/33/36/794fe1c4ed29b0f56a26d72e956baf0285d8fe9e29faf36797a28a8ad45b/serverfiles-0.3.1.tar.gz#sha256=5e10fc32e758791e3735bc08bce2ed4702a83b1e45ab96c5d59cc8aeecfbebe1\nBest match: serverfiles 0.3.1\nProcessing serverfiles-0.3.1.tar.gz\nWriting /tmp/easy_install-l8npbzce/serverfiles-0.3.1/setup.cfg\nRunning serverfiles-0.3.1/setup.py -q bdist_egg --dist-dir /tmp/easy_install-l8npbzce/serverfiles-0.3.1/egg-dist-tmp-_d1n1fe4\ncreating /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.7/site-packages/serverfiles-0.3.1-py3.7.egg\nExtracting serverfiles-0.3.1-py3.7.egg to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.7/site-packages\nAdding serverfiles 0.3.1 to easy-install.pth file\n\nInstalled /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.7/site-packages/serverfiles-0.3.1-py3.7.egg\nSearching for scipy>=1.9\nReading https://pypi.org/simple/scipy/\nDownloading https://files.pythonhosted.org/packages/d9/7b/2b8ac283cf32465ed08bc20a83d559fe7b174a484781702ba8accea001d6/scipy-1.15.0.tar.gz#sha256=300742e2cc94e36a2880ebe464a1c8b4352a7b0f3e36ec3d2ac006cdbe0219ac\nBest match: scipy 1.15.0\nProcessing scipy-1.15.0.tar.gz\nVerifying Orange installation...\n❌ Orange verification failed\nPython 3.7 installation failed, trying Python 3.8...\nAttempting installation with Python 3.8...\nrunning build_ext\nbuilding 'Orange.classification._simple_tree' extension\ncreating build/temp.linux-x86_64-cpython-38/Orange/classification\ncc -pthread -Wno-unused-result -Wsign-compare -Wunreachable-code -DNDEBUG -g -fwrapv -O3 -Wall -fPIC -fPIC -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.8/site-packages/numpy/core/include -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/include -I/home/gcpuser/.local/share/uv/python/cpython-3.8.20-linux-x86_64-gnu/include/python3.8 -c Orange/classification/_simple_tree.c -o build/temp.linux-x86_64-cpython-38/Orange/classification/_simple_tree.o\ncreating build/lib.linux-x86_64-cpython-38/Orange/classification\ncc -pthread -shared -Wl,--exclude-libs,ALL build/temp.linux-x86_64-cpython-38/Orange/classification/_simple_tree.o -L/home/gcpuser/.local/share/uv/python/cpython-3.8.20-linux-x86_64-gnu/lib -lm -o build/lib.linux-x86_64-cpython-38/Orange/classification/_simple_tree.cpython-38-x86_64-linux-gnu.so\nbuilding 'Orange.widgets.utils._grid_density' extension\ncreating build/temp.linux-x86_64-cpython-38/Orange/widgets/utils\nc++ -pthread -Wno-unused-result -Wsign-compare -Wunreachable-code -DNDEBUG -g -fwrapv -O3 -Wall -fPIC -fPIC -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.8/site-packages/numpy/core/include -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/include -I/home/gcpuser/.local/share/uv/python/cpython-3.8.20-linux-x86_64-gnu/include/python3.8 -c Orange/widgets/utils/_grid_density.cpp -o build/temp.linux-x86_64-cpython-38/Orange/widgets/utils/_grid_density.o\ncreating build/lib.linux-x86_64-cpython-38/Orange/widgets/utils\nc++ -pthread -Wno-unused-result -Wsign-compare -Wunreachable-code -DNDEBUG -g -fwrapv -O3 -Wall -fPIC -pthread -shared build/temp.linux-x86_64-cpython-38/Orange/widgets/utils/_grid_density.o -L/home/gcpuser/.local/share/uv/python/cpython-3.8.20-linux-x86_64-gnu/lib -lm -o build/lib.linux-x86_64-cpython-38/Orange/widgets/utils/_grid_density.cpython-38-x86_64-linux-gnu.so\nbuilding 'Orange.classification._tree_scorers' extension\ncc -pthread -Wno-unused-result -Wsign-compare -Wunreachable-code -DNDEBUG -g -fwrapv -O3 -Wall -fPIC -fPIC -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.8/site-packages/numpy/core/include -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/include -I/home/gcpuser/.local/share/uv/python/cpython-3.8.20-linux-x86_64-gnu/include/python3.8 -c Orange/classification/_tree_scorers.c -o build/temp.linux-x86_64-cpython-38/Orange/classification/_tree_scorers.o\ncc -pthread -shared -Wl,--exclude-libs,ALL build/temp.linux-x86_64-cpython-38/Orange/classification/_tree_scorers.o -L/home/gcpuser/.local/share/uv/python/cpython-3.8.20-linux-x86_64-gnu/lib -lm -o build/lib.linux-x86_64-cpython-38/Orange/classification/_tree_scorers.cpython-38-x86_64-linux-gnu.so\nbuilding 'Orange.data._contingency' extension\ncreating build/temp.linux-x86_64-cpython-38/Orange/data\ncc -pthread -Wno-unused-result -Wsign-compare -Wunreachable-code -DNDEBUG -g -fwrapv -O3 -Wall -fPIC -fPIC -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.8/site-packages/numpy/core/include -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/include -I/home/gcpuser/.local/share/uv/python/cpython-3.8.20-linux-x86_64-gnu/include/python3.8 -c Orange/data/_contingency.c -o build/temp.linux-x86_64-cpython-38/Orange/data/_contingency.o\ncreating build/lib.linux-x86_64-cpython-38/Orange/data\ncc -pthread -shared -Wl,--exclude-libs,ALL build/temp.linux-x86_64-cpython-38/Orange/data/_contingency.o -L/home/gcpuser/.local/share/uv/python/cpython-3.8.20-linux-x86_64-gnu/lib -lm -o build/lib.linux-x86_64-cpython-38/Orange/data/_contingency.cpython-38-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 -fPIC -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.8/site-packages/numpy/core/include -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/include -I/home/gcpuser/.local/share/uv/python/cpython-3.8.20-linux-x86_64-gnu/include/python3.8 -c Orange/data/_io.c -o build/temp.linux-x86_64-cpython-38/Orange/data/_io.o\ncc -pthread -shared -Wl,--exclude-libs,ALL build/temp.linux-x86_64-cpython-38/Orange/data/_io.o -L/home/gcpuser/.local/share/uv/python/cpython-3.8.20-linux-x86_64-gnu/lib -lm -o build/lib.linux-x86_64-cpython-38/Orange/data/_io.cpython-38-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 -fPIC -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.8/site-packages/numpy/core/include -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/include -I/home/gcpuser/.local/share/uv/python/cpython-3.8.20-linux-x86_64-gnu/include/python3.8 -c Orange/data/_valuecount.c -o build/temp.linux-x86_64-cpython-38/Orange/data/_valuecount.o\ncc -pthread -shared -Wl,--exclude-libs,ALL build/temp.linux-x86_64-cpython-38/Orange/data/_valuecount.o -L/home/gcpuser/.local/share/uv/python/cpython-3.8.20-linux-x86_64-gnu/lib -lm -o build/lib.linux-x86_64-cpython-38/Orange/data/_valuecount.cpython-38-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 -fPIC -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.8/site-packages/numpy/core/include -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/include -I/home/gcpuser/.local/share/uv/python/cpython-3.8.20-linux-x86_64-gnu/include/python3.8 -c Orange/data/_variable.c -o build/temp.linux-x86_64-cpython-38/Orange/data/_variable.o\ncc -pthread -shared -Wl,--exclude-libs,ALL build/temp.linux-x86_64-cpython-38/Orange/data/_variable.o -L/home/gcpuser/.local/share/uv/python/cpython-3.8.20-linux-x86_64-gnu/lib -lm -o build/lib.linux-x86_64-cpython-38/Orange/data/_variable.cpython-38-x86_64-linux-gnu.so\nbuilding 'Orange.distance._distance' extension\ncreating build/temp.linux-x86_64-cpython-38/Orange/distance\ncc -pthread -Wno-unused-result -Wsign-compare -Wunreachable-code -DNDEBUG -g -fwrapv -O3 -Wall -fPIC -fPIC -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.8/site-packages/numpy/core/include -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/include -I/home/gcpuser/.local/share/uv/python/cpython-3.8.20-linux-x86_64-gnu/include/python3.8 -c Orange/distance/_distance.c -o build/temp.linux-x86_64-cpython-38/Orange/distance/_distance.o\ncreating build/lib.linux-x86_64-cpython-38/Orange/distance\ncc -pthread -shared -Wl,--exclude-libs,ALL build/temp.linux-x86_64-cpython-38/Orange/distance/_distance.o -L/home/gcpuser/.local/share/uv/python/cpython-3.8.20-linux-x86_64-gnu/lib -lm -o build/lib.linux-x86_64-cpython-38/Orange/distance/_distance.cpython-38-x86_64-linux-gnu.so\nbuilding 'Orange.preprocess._discretize' extension\ncreating build/temp.linux-x86_64-cpython-38/Orange/preprocess\ncc -pthread -Wno-unused-result -Wsign-compare -Wunreachable-code -DNDEBUG -g -fwrapv -O3 -Wall -fPIC -fPIC -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.8/site-packages/numpy/core/include -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/include -I/home/gcpuser/.local/share/uv/python/cpython-3.8.20-linux-x86_64-gnu/include/python3.8 -c Orange/preprocess/_discretize.c -o build/temp.linux-x86_64-cpython-38/Orange/preprocess/_discretize.o\ncreating build/lib.linux-x86_64-cpython-38/Orange/preprocess\ncc -pthread -shared -Wl,--exclude-libs,ALL build/temp.linux-x86_64-cpython-38/Orange/preprocess/_discretize.o -L/home/gcpuser/.local/share/uv/python/cpython-3.8.20-linux-x86_64-gnu/lib -lm -o build/lib.linux-x86_64-cpython-38/Orange/preprocess/_discretize.cpython-38-x86_64-linux-gnu.so\nbuilding 'Orange.preprocess._relieff' extension\nc++ -pthread -Wno-unused-result -Wsign-compare -Wunreachable-code -DNDEBUG -g -fwrapv -O3 -Wall -fPIC -fPIC -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.8/site-packages/numpy/core/include -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/include -I/home/gcpuser/.local/share/uv/python/cpython-3.8.20-linux-x86_64-gnu/include/python3.8 -c Orange/preprocess/_relieff.cpp -o build/temp.linux-x86_64-cpython-38/Orange/preprocess/_relieff.o\nc++ -pthread -Wno-unused-result -Wsign-compare -Wunreachable-code -DNDEBUG -g -fwrapv -O3 -Wall -fPIC -pthread -shared build/temp.linux-x86_64-cpython-38/Orange/preprocess/_relieff.o -L/home/gcpuser/.local/share/uv/python/cpython-3.8.20-linux-x86_64-gnu/lib -lm -o build/lib.linux-x86_64-cpython-38/Orange/preprocess/_relieff.cpython-38-x86_64-linux-gnu.so\nbuilding 'Orange.projection._som' extension\ncreating build/temp.linux-x86_64-cpython-38/Orange/projection\ncc -pthread -Wno-unused-result -Wsign-compare -Wunreachable-code -DNDEBUG -g -fwrapv -O3 -Wall -fPIC -fPIC -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.8/site-packages/numpy/core/include -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/include -I/home/gcpuser/.local/share/uv/python/cpython-3.8.20-linux-x86_64-gnu/include/python3.8 -c Orange/projection/_som.c -o build/temp.linux-x86_64-cpython-38/Orange/projection/_som.o\ncreating build/lib.linux-x86_64-cpython-38/Orange/projection\ncc -pthread -shared -Wl,--exclude-libs,ALL build/temp.linux-x86_64-cpython-38/Orange/projection/_som.o -L/home/gcpuser/.local/share/uv/python/cpython-3.8.20-linux-x86_64-gnu/lib -lm -o build/lib.linux-x86_64-cpython-38/Orange/projection/_som.cpython-38-x86_64-linux-gnu.so\ncopying build/lib.linux-x86_64-cpython-38/Orange/classification/_simple_tree.cpython-38-x86_64-linux-gnu.so -> Orange/classification\ncopying build/lib.linux-x86_64-cpython-38/Orange/widgets/utils/_grid_density.cpython-38-x86_64-linux-gnu.so -> Orange/widgets/utils\ncopying build/lib.linux-x86_64-cpython-38/Orange/classification/_tree_scorers.cpython-38-x86_64-linux-gnu.so -> Orange/classification\ncopying build/lib.linux-x86_64-cpython-38/Orange/data/_contingency.cpython-38-x86_64-linux-gnu.so -> Orange/data\ncopying build/lib.linux-x86_64-cpython-38/Orange/data/_io.cpython-38-x86_64-linux-gnu.so -> Orange/data\ncopying build/lib.linux-x86_64-cpython-38/Orange/data/_valuecount.cpython-38-x86_64-linux-gnu.so -> Orange/data\ncopying build/lib.linux-x86_64-cpython-38/Orange/data/_variable.cpython-38-x86_64-linux-gnu.so -> Orange/data\ncopying build/lib.linux-x86_64-cpython-38/Orange/distance/_distance.cpython-38-x86_64-linux-gnu.so -> Orange/distance\ncopying build/lib.linux-x86_64-cpython-38/Orange/preprocess/_discretize.cpython-38-x86_64-linux-gnu.so -> Orange/preprocess\ncopying build/lib.linux-x86_64-cpython-38/Orange/preprocess/_relieff.cpython-38-x86_64-linux-gnu.so -> Orange/preprocess\ncopying build/lib.linux-x86_64-cpython-38/Orange/projection/_som.cpython-38-x86_64-linux-gnu.so -> Orange/projection\nrunning develop\nrunning 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\ndependency /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.8/site-packages/numpy/core/include/numpy/arrayobject.h won't be automatically included in the manifest: the path must be relative\ndependency /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.8/site-packages/numpy/core/include/numpy/arrayscalars.h won't be automatically included in the manifest: the path must be relative\ndependency /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.8/site-packages/numpy/core/include/numpy/ndarrayobject.h won't be automatically included in the manifest: the path must be relative\ndependency /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.8/site-packages/numpy/core/include/numpy/ndarraytypes.h won't be automatically included in the manifest: the path must be relative\ndependency /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.8/site-packages/numpy/core/include/numpy/npy_math.h won't be automatically included in the manifest: the path must be relative\ndependency /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.8/site-packages/numpy/core/include/numpy/ufuncobject.h won't be automatically included in the manifest: the path must be relative\ndependency /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.8/site-packages/numpy/core/include/numpy/arrayobject.h won't be automatically included in the manifest: the path must be relative\ndependency /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.8/site-packages/numpy/core/include/numpy/arrayscalars.h won't be automatically included in the manifest: the path must be relative\ndependency /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.8/site-packages/numpy/core/include/numpy/ndarrayobject.h won't be automatically included in the manifest: the path must be relative\ndependency /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.8/site-packages/numpy/core/include/numpy/ndarraytypes.h won't be automatically included in the manifest: the path must be relative\ndependency /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.8/site-packages/numpy/core/include/numpy/npy_math.h won't be automatically included in the manifest: the path must be relative\ndependency /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.8/site-packages/numpy/core/include/numpy/ufuncobject.h won't be automatically included in the manifest: the path must be relative\ndependency /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.8/site-packages/numpy/core/include/numpy/arrayobject.h won't be automatically included in the manifest: the path must be relative\ndependency /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.8/site-packages/numpy/core/include/numpy/arrayscalars.h won't be automatically included in the manifest: the path must be relative\ndependency /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.8/site-packages/numpy/core/include/numpy/ndarrayobject.h won't be automatically included in the manifest: the path must be relative\ndependency /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.8/site-packages/numpy/core/include/numpy/ndarraytypes.h won't be automatically included in the manifest: the path must be relative\ndependency /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.8/site-packages/numpy/core/include/numpy/npy_math.h won't be automatically included in the manifest: the path must be relative\ndependency /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.8/site-packages/numpy/core/include/numpy/ufuncobject.h won't be automatically included in the manifest: the path must be relative\ndependency /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.8/site-packages/numpy/core/include/numpy/arrayobject.h won't be automatically included in the manifest: the path must be relative\ndependency /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.8/site-packages/numpy/core/include/numpy/arrayscalars.h won't be automatically included in the manifest: the path must be relative\ndependency /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.8/site-packages/numpy/core/include/numpy/ndarrayobject.h won't be automatically included in the manifest: the path must be relative\ndependency /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.8/site-packages/numpy/core/include/numpy/ndarraytypes.h won't be automatically included in the manifest: the path must be relative\ndependency /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.8/site-packages/numpy/core/include/numpy/npy_math.h won't be automatically included in the manifest: the path must be relative\ndependency /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.8/site-packages/numpy/core/include/numpy/ufuncobject.h won't be automatically included in the manifest: the path must be relative\ndependency /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.8/site-packages/numpy/core/include/numpy/arrayobject.h won't be automatically included in the manifest: the path must be relative\ndependency /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.8/site-packages/numpy/core/include/numpy/arrayscalars.h won't be automatically included in the manifest: the path must be relative\ndependency /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.8/site-packages/numpy/core/include/numpy/ndarrayobject.h won't be automatically included in the manifest: the path must be relative\ndependency /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.8/site-packages/numpy/core/include/numpy/ndarraytypes.h won't be automatically included in the manifest: the path must be relative\ndependency /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.8/site-packages/numpy/core/include/numpy/ufuncobject.h won't be automatically included in the manifest: the path must be relative\ndependency /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.8/site-packages/numpy/core/include/numpy/arrayobject.h won't be automatically included in the manifest: the path must be relative\ndependency /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.8/site-packages/numpy/core/include/numpy/arrayscalars.h won't be automatically included in the manifest: the path must be relative\ndependency /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.8/site-packages/numpy/core/include/numpy/ndarrayobject.h won't be automatically included in the manifest: the path must be relative\ndependency /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.8/site-packages/numpy/core/include/numpy/ndarraytypes.h won't be automatically included in the manifest: the path must be relative\ndependency /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.8/site-packages/numpy/core/include/numpy/npy_math.h won't be automatically included in the manifest: the path must be relative\ndependency /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.8/site-packages/numpy/core/include/numpy/ufuncobject.h won't be automatically included in the manifest: the path must be relative\ndependency /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.8/site-packages/numpy/core/include/numpy/arrayobject.h won't be automatically included in the manifest: the path must be relative\ndependency /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.8/site-packages/numpy/core/include/numpy/arrayscalars.h won't be automatically included in the manifest: the path must be relative\ndependency /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.8/site-packages/numpy/core/include/numpy/ndarrayobject.h won't be automatically included in the manifest: the path must be relative\ndependency /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.8/site-packages/numpy/core/include/numpy/ndarraytypes.h won't be automatically included in the manifest: the path must be relative\ndependency /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.8/site-packages/numpy/core/include/numpy/npy_math.h won't be automatically included in the manifest: the path must be relative\ndependency /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.8/site-packages/numpy/core/include/numpy/ufuncobject.h won't be automatically included in the manifest: the path must be relative\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-cpython-38/Orange/classification/_simple_tree.cpython-38-x86_64-linux-gnu.so -> Orange/classification\ncopying build/lib.linux-x86_64-cpython-38/Orange/widgets/utils/_grid_density.cpython-38-x86_64-linux-gnu.so -> Orange/widgets/utils\ncopying build/lib.linux-x86_64-cpython-38/Orange/classification/_tree_scorers.cpython-38-x86_64-linux-gnu.so -> Orange/classification\ncopying build/lib.linux-x86_64-cpython-38/Orange/data/_contingency.cpython-38-x86_64-linux-gnu.so -> Orange/data\ncopying build/lib.linux-x86_64-cpython-38/Orange/data/_io.cpython-38-x86_64-linux-gnu.so -> Orange/data\ncopying build/lib.linux-x86_64-cpython-38/Orange/data/_valuecount.cpython-38-x86_64-linux-gnu.so -> Orange/data\ncopying build/lib.linux-x86_64-cpython-38/Orange/data/_variable.cpython-38-x86_64-linux-gnu.so -> Orange/data\ncopying build/lib.linux-x86_64-cpython-38/Orange/distance/_distance.cpython-38-x86_64-linux-gnu.so -> Orange/distance\ncopying build/lib.linux-x86_64-cpython-38/Orange/preprocess/_discretize.cpython-38-x86_64-linux-gnu.so -> Orange/preprocess\ncopying build/lib.linux-x86_64-cpython-38/Orange/preprocess/_relieff.cpython-38-x86_64-linux-gnu.so -> Orange/preprocess\ncopying build/lib.linux-x86_64-cpython-38/Orange/projection/_som.cpython-38-x86_64-linux-gnu.so -> Orange/projection\nCreating /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.8/site-packages/Orange3.egg-link (link to .)\nAdding Orange3 3.38.0.dev0+2a6f878 to easy-install.pth file\nInstalling orange-canvas script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/bin\n\nInstalled /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074\nProcessing dependencies for Orange3==3.38.0.dev0+2a6f878\nSearching for xlsxwriter\nReading https://pypi.org/simple/xlsxwriter/\nDownloading https://files.pythonhosted.org/packages/a7/ea/53d1fe468e63e092cf16e2c18d16f50c29851242f9dd12d6a66e0d7f0d02/XlsxWriter-3.2.0-py3-none-any.whl#sha256=ecfd5405b3e0e228219bcaf24c2ca0915e012ca9464a14048021d21a995d490e\nBest match: XlsxWriter 3.2.0\nProcessing XlsxWriter-3.2.0-py3-none-any.whl\nInstalling XlsxWriter-3.2.0-py3-none-any.whl to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.8/site-packages\nAdding XlsxWriter 3.2.0 to easy-install.pth file\nInstalling vba_extract.py script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/bin\n\nInstalled /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.8/site-packages/XlsxWriter-3.2.0-py3.8.egg\nSearching for xlrd>=1.2.0\nReading https://pypi.org/simple/xlrd/\nDownloading https://files.pythonhosted.org/packages/a6/0c/c2a72d51fe56e08a08acc85d13013558a2d793028ae7385448a6ccdfae64/xlrd-2.0.1-py2.py3-none-any.whl#sha256=6a33ee89877bd9abc1158129f6e94be74e2679636b8a205b43b85206c3f0bbdd\nBest match: xlrd 2.0.1\nProcessing xlrd-2.0.1-py2.py3-none-any.whl\nInstalling xlrd-2.0.1-py2.py3-none-any.whl to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.8/site-packages\nAdding xlrd 2.0.1 to easy-install.pth file\ndetected new path './XlsxWriter-3.2.0-py3.8.egg'\nInstalling runxlrd.py script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/bin\n\nInstalled /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.8/site-packages/xlrd-2.0.1-py3.8.egg\nSearching for xgboost<2.1,>=1.7.4\nReading https://pypi.org/simple/xgboost/\nDownloading https://files.pythonhosted.org/packages/c3/eb/496aa2f5d356af4185f770bc76055307f8d1870e11016b10fd779b21769c/xgboost-2.0.3-py3-none-manylinux2014_x86_64.whl#sha256=30bd5f789fad467fd49e04e5d19e04238b931682c3951a514da5c2410b3bf59c\nBest match: xgboost 2.0.3\nProcessing xgboost-2.0.3-py3-none-manylinux2014_x86_64.whl\nInstalling xgboost-2.0.3-py3-none-manylinux2014_x86_64.whl to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.8/site-packages\nAdding xgboost 2.0.3 to easy-install.pth file\ndetected new path './xlrd-2.0.1-py3.8.egg'\n\nInstalled /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.8/site-packages/xgboost-2.0.3-py3.8-linux-x86_64.egg\nSearching for serverfiles\nReading https://pypi.org/simple/serverfiles/\nDownloading https://files.pythonhosted.org/packages/33/36/794fe1c4ed29b0f56a26d72e956baf0285d8fe9e29faf36797a28a8ad45b/serverfiles-0.3.1.tar.gz#sha256=5e10fc32e758791e3735bc08bce2ed4702a83b1e45ab96c5d59cc8aeecfbebe1\nBest match: serverfiles 0.3.1\nProcessing serverfiles-0.3.1.tar.gz\nWriting /tmp/easy_install-xov9pds4/serverfiles-0.3.1/setup.cfg\nRunning serverfiles-0.3.1/setup.py -q bdist_egg --dist-dir /tmp/easy_install-xov9pds4/serverfiles-0.3.1/egg-dist-tmp-ar4ma9mc\nAdding serverfiles 0.3.1 to easy-install.pth file\ndetected new path './xgboost-2.0.3-py3.8-linux-x86_64.egg'\n\nInstalled /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.8/site-packages/serverfiles-0.3.1-py3.8.egg\nSearching for scipy>=1.9\nReading https://pypi.org/simple/scipy/\nDownloading https://files.pythonhosted.org/packages/d9/7b/2b8ac283cf32465ed08bc20a83d559fe7b174a484781702ba8accea001d6/scipy-1.15.0.tar.gz#sha256=300742e2cc94e36a2880ebe464a1c8b4352a7b0f3e36ec3d2ac006cdbe0219ac\nBest match: scipy 1.15.0\nProcessing scipy-1.15.0.tar.gz\nVerifying Orange installation...\n❌ Orange verification failed\nPython 3.7,3.8 installation failed, trying Python 3.8...\nAttempting installation with Python 3.10...\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\nrunning build_ext\nbuilding 'Orange.classification._simple_tree' extension\ncreating build/temp.linux-x86_64-cpython-310/Orange/classification\ncc -pthread -Wno-unused-result -Wsign-compare -Wunreachable-code -DNDEBUG -g -fwrapv -O3 -Wall -fPIC -fPIC -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.10/site-packages/numpy/core/include -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/include -I/home/gcpuser/.local/share/uv/python/cpython-3.10.16-linux-x86_64-gnu/include/python3.10 -c Orange/classification/_simple_tree.c -o build/temp.linux-x86_64-cpython-310/Orange/classification/_simple_tree.o\ncreating build/lib.linux-x86_64-cpython-310/Orange/classification\ncc -pthread -shared -Wl,--exclude-libs,ALL build/temp.linux-x86_64-cpython-310/Orange/classification/_simple_tree.o -L/home/gcpuser/.local/share/uv/python/cpython-3.10.16-linux-x86_64-gnu/lib -lm -o build/lib.linux-x86_64-cpython-310/Orange/classification/_simple_tree.cpython-310-x86_64-linux-gnu.so\nbuilding 'Orange.widgets.utils._grid_density' extension\ncreating build/temp.linux-x86_64-cpython-310/Orange/widgets/utils\nc++ -pthread -Wno-unused-result -Wsign-compare -Wunreachable-code -DNDEBUG -g -fwrapv -O3 -Wall -fPIC -fPIC -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.10/site-packages/numpy/core/include -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/include -I/home/gcpuser/.local/share/uv/python/cpython-3.10.16-linux-x86_64-gnu/include/python3.10 -c Orange/widgets/utils/_grid_density.cpp -o build/temp.linux-x86_64-cpython-310/Orange/widgets/utils/_grid_density.o\ncreating build/lib.linux-x86_64-cpython-310/Orange/widgets/utils\nc++ -pthread -Wno-unused-result -Wsign-compare -Wunreachable-code -DNDEBUG -g -fwrapv -O3 -Wall -fPIC -pthread -shared build/temp.linux-x86_64-cpython-310/Orange/widgets/utils/_grid_density.o -L/home/gcpuser/.local/share/uv/python/cpython-3.10.16-linux-x86_64-gnu/lib -lm -o build/lib.linux-x86_64-cpython-310/Orange/widgets/utils/_grid_density.cpython-310-x86_64-linux-gnu.so\nbuilding 'Orange.classification._tree_scorers' extension\ncc -pthread -Wno-unused-result -Wsign-compare -Wunreachable-code -DNDEBUG -g -fwrapv -O3 -Wall -fPIC -fPIC -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.10/site-packages/numpy/core/include -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/include -I/home/gcpuser/.local/share/uv/python/cpython-3.10.16-linux-x86_64-gnu/include/python3.10 -c Orange/classification/_tree_scorers.c -o build/temp.linux-x86_64-cpython-310/Orange/classification/_tree_scorers.o\ncc -pthread -shared -Wl,--exclude-libs,ALL build/temp.linux-x86_64-cpython-310/Orange/classification/_tree_scorers.o -L/home/gcpuser/.local/share/uv/python/cpython-3.10.16-linux-x86_64-gnu/lib -lm -o build/lib.linux-x86_64-cpython-310/Orange/classification/_tree_scorers.cpython-310-x86_64-linux-gnu.so\nbuilding 'Orange.data._contingency' extension\ncreating build/temp.linux-x86_64-cpython-310/Orange/data\ncc -pthread -Wno-unused-result -Wsign-compare -Wunreachable-code -DNDEBUG -g -fwrapv -O3 -Wall -fPIC -fPIC -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.10/site-packages/numpy/core/include -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/include -I/home/gcpuser/.local/share/uv/python/cpython-3.10.16-linux-x86_64-gnu/include/python3.10 -c Orange/data/_contingency.c -o build/temp.linux-x86_64-cpython-310/Orange/data/_contingency.o\ncreating build/lib.linux-x86_64-cpython-310/Orange/data\ncc -pthread -shared -Wl,--exclude-libs,ALL build/temp.linux-x86_64-cpython-310/Orange/data/_contingency.o -L/home/gcpuser/.local/share/uv/python/cpython-3.10.16-linux-x86_64-gnu/lib -lm -o build/lib.linux-x86_64-cpython-310/Orange/data/_contingency.cpython-310-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 -fPIC -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.10/site-packages/numpy/core/include -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/include -I/home/gcpuser/.local/share/uv/python/cpython-3.10.16-linux-x86_64-gnu/include/python3.10 -c Orange/data/_io.c -o build/temp.linux-x86_64-cpython-310/Orange/data/_io.o\ncc -pthread -shared -Wl,--exclude-libs,ALL build/temp.linux-x86_64-cpython-310/Orange/data/_io.o -L/home/gcpuser/.local/share/uv/python/cpython-3.10.16-linux-x86_64-gnu/lib -lm -o build/lib.linux-x86_64-cpython-310/Orange/data/_io.cpython-310-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 -fPIC -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.10/site-packages/numpy/core/include -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/include -I/home/gcpuser/.local/share/uv/python/cpython-3.10.16-linux-x86_64-gnu/include/python3.10 -c Orange/data/_valuecount.c -o build/temp.linux-x86_64-cpython-310/Orange/data/_valuecount.o\ncc -pthread -shared -Wl,--exclude-libs,ALL build/temp.linux-x86_64-cpython-310/Orange/data/_valuecount.o -L/home/gcpuser/.local/share/uv/python/cpython-3.10.16-linux-x86_64-gnu/lib -lm -o build/lib.linux-x86_64-cpython-310/Orange/data/_valuecount.cpython-310-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 -fPIC -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.10/site-packages/numpy/core/include -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/include -I/home/gcpuser/.local/share/uv/python/cpython-3.10.16-linux-x86_64-gnu/include/python3.10 -c Orange/data/_variable.c -o build/temp.linux-x86_64-cpython-310/Orange/data/_variable.o\ncc -pthread -shared -Wl,--exclude-libs,ALL build/temp.linux-x86_64-cpython-310/Orange/data/_variable.o -L/home/gcpuser/.local/share/uv/python/cpython-3.10.16-linux-x86_64-gnu/lib -lm -o build/lib.linux-x86_64-cpython-310/Orange/data/_variable.cpython-310-x86_64-linux-gnu.so\nbuilding 'Orange.distance._distance' extension\ncreating build/temp.linux-x86_64-cpython-310/Orange/distance\ncc -pthread -Wno-unused-result -Wsign-compare -Wunreachable-code -DNDEBUG -g -fwrapv -O3 -Wall -fPIC -fPIC -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.10/site-packages/numpy/core/include -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/include -I/home/gcpuser/.local/share/uv/python/cpython-3.10.16-linux-x86_64-gnu/include/python3.10 -c Orange/distance/_distance.c -o build/temp.linux-x86_64-cpython-310/Orange/distance/_distance.o\ncreating build/lib.linux-x86_64-cpython-310/Orange/distance\ncc -pthread -shared -Wl,--exclude-libs,ALL build/temp.linux-x86_64-cpython-310/Orange/distance/_distance.o -L/home/gcpuser/.local/share/uv/python/cpython-3.10.16-linux-x86_64-gnu/lib -lm -o build/lib.linux-x86_64-cpython-310/Orange/distance/_distance.cpython-310-x86_64-linux-gnu.so\nbuilding 'Orange.preprocess._discretize' extension\ncreating build/temp.linux-x86_64-cpython-310/Orange/preprocess\ncc -pthread -Wno-unused-result -Wsign-compare -Wunreachable-code -DNDEBUG -g -fwrapv -O3 -Wall -fPIC -fPIC -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.10/site-packages/numpy/core/include -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/include -I/home/gcpuser/.local/share/uv/python/cpython-3.10.16-linux-x86_64-gnu/include/python3.10 -c Orange/preprocess/_discretize.c -o build/temp.linux-x86_64-cpython-310/Orange/preprocess/_discretize.o\ncreating build/lib.linux-x86_64-cpython-310/Orange/preprocess\ncc -pthread -shared -Wl,--exclude-libs,ALL build/temp.linux-x86_64-cpython-310/Orange/preprocess/_discretize.o -L/home/gcpuser/.local/share/uv/python/cpython-3.10.16-linux-x86_64-gnu/lib -lm -o build/lib.linux-x86_64-cpython-310/Orange/preprocess/_discretize.cpython-310-x86_64-linux-gnu.so\nbuilding 'Orange.preprocess._relieff' extension\nc++ -pthread -Wno-unused-result -Wsign-compare -Wunreachable-code -DNDEBUG -g -fwrapv -O3 -Wall -fPIC -fPIC -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.10/site-packages/numpy/core/include -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/include -I/home/gcpuser/.local/share/uv/python/cpython-3.10.16-linux-x86_64-gnu/include/python3.10 -c Orange/preprocess/_relieff.cpp -o build/temp.linux-x86_64-cpython-310/Orange/preprocess/_relieff.o\nc++ -pthread -Wno-unused-result -Wsign-compare -Wunreachable-code -DNDEBUG -g -fwrapv -O3 -Wall -fPIC -pthread -shared build/temp.linux-x86_64-cpython-310/Orange/preprocess/_relieff.o -L/home/gcpuser/.local/share/uv/python/cpython-3.10.16-linux-x86_64-gnu/lib -lm -o build/lib.linux-x86_64-cpython-310/Orange/preprocess/_relieff.cpython-310-x86_64-linux-gnu.so\nbuilding 'Orange.projection._som' extension\ncreating build/temp.linux-x86_64-cpython-310/Orange/projection\ncc -pthread -Wno-unused-result -Wsign-compare -Wunreachable-code -DNDEBUG -g -fwrapv -O3 -Wall -fPIC -fPIC -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.10/site-packages/numpy/core/include -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/include -I/home/gcpuser/.local/share/uv/python/cpython-3.10.16-linux-x86_64-gnu/include/python3.10 -c Orange/projection/_som.c -o build/temp.linux-x86_64-cpython-310/Orange/projection/_som.o\ncreating build/lib.linux-x86_64-cpython-310/Orange/projection\ncc -pthread -shared -Wl,--exclude-libs,ALL build/temp.linux-x86_64-cpython-310/Orange/projection/_som.o -L/home/gcpuser/.local/share/uv/python/cpython-3.10.16-linux-x86_64-gnu/lib -lm -o build/lib.linux-x86_64-cpython-310/Orange/projection/_som.cpython-310-x86_64-linux-gnu.so\ncopying build/lib.linux-x86_64-cpython-310/Orange/classification/_simple_tree.cpython-310-x86_64-linux-gnu.so -> Orange/classification\ncopying build/lib.linux-x86_64-cpython-310/Orange/widgets/utils/_grid_density.cpython-310-x86_64-linux-gnu.so -> Orange/widgets/utils\ncopying build/lib.linux-x86_64-cpython-310/Orange/classification/_tree_scorers.cpython-310-x86_64-linux-gnu.so -> Orange/classification\ncopying build/lib.linux-x86_64-cpython-310/Orange/data/_contingency.cpython-310-x86_64-linux-gnu.so -> Orange/data\ncopying build/lib.linux-x86_64-cpython-310/Orange/data/_io.cpython-310-x86_64-linux-gnu.so -> Orange/data\ncopying build/lib.linux-x86_64-cpython-310/Orange/data/_valuecount.cpython-310-x86_64-linux-gnu.so -> Orange/data\ncopying build/lib.linux-x86_64-cpython-310/Orange/data/_variable.cpython-310-x86_64-linux-gnu.so -> Orange/data\ncopying build/lib.linux-x86_64-cpython-310/Orange/distance/_distance.cpython-310-x86_64-linux-gnu.so -> Orange/distance\ncopying build/lib.linux-x86_64-cpython-310/Orange/preprocess/_discretize.cpython-310-x86_64-linux-gnu.so -> Orange/preprocess\ncopying build/lib.linux-x86_64-cpython-310/Orange/preprocess/_relieff.cpython-310-x86_64-linux-gnu.so -> Orange/preprocess\ncopying build/lib.linux-x86_64-cpython-310/Orange/projection/_som.cpython-310-x86_64-linux-gnu.so -> Orange/projection\nrunning develop\nrunning 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\ndependency /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.10/site-packages/numpy/core/include/numpy/arrayobject.h won't be automatically included in the manifest: the path must be relative\ndependency /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.10/site-packages/numpy/core/include/numpy/arrayscalars.h won't be automatically included in the manifest: the path must be relative\ndependency /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.10/site-packages/numpy/core/include/numpy/ndarrayobject.h won't be automatically included in the manifest: the path must be relative\ndependency /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.10/site-packages/numpy/core/include/numpy/ndarraytypes.h won't be automatically included in the manifest: the path must be relative\ndependency /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.10/site-packages/numpy/core/include/numpy/npy_math.h won't be automatically included in the manifest: the path must be relative\ndependency /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.10/site-packages/numpy/core/include/numpy/ufuncobject.h won't be automatically included in the manifest: the path must be relative\ndependency /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.10/site-packages/numpy/core/include/numpy/arrayobject.h won't be automatically included in the manifest: the path must be relative\ndependency /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.10/site-packages/numpy/core/include/numpy/arrayscalars.h won't be automatically included in the manifest: the path must be relative\ndependency /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.10/site-packages/numpy/core/include/numpy/ndarrayobject.h won't be automatically included in the manifest: the path must be relative\ndependency /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.10/site-packages/numpy/core/include/numpy/ndarraytypes.h won't be automatically included in the manifest: the path must be relative\ndependency /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.10/site-packages/numpy/core/include/numpy/npy_math.h won't be automatically included in the manifest: the path must be relative\ndependency /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.10/site-packages/numpy/core/include/numpy/ufuncobject.h won't be automatically included in the manifest: the path must be relative\ndependency /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.10/site-packages/numpy/core/include/numpy/arrayobject.h won't be automatically included in the manifest: the path must be relative\ndependency /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.10/site-packages/numpy/core/include/numpy/arrayscalars.h won't be automatically included in the manifest: the path must be relative\ndependency /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.10/site-packages/numpy/core/include/numpy/ndarrayobject.h won't be automatically included in the manifest: the path must be relative\ndependency /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.10/site-packages/numpy/core/include/numpy/ndarraytypes.h won't be automatically included in the manifest: the path must be relative\ndependency /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.10/site-packages/numpy/core/include/numpy/npy_math.h won't be automatically included in the manifest: the path must be relative\ndependency /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.10/site-packages/numpy/core/include/numpy/ufuncobject.h won't be automatically included in the manifest: the path must be relative\ndependency /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.10/site-packages/numpy/core/include/numpy/arrayobject.h won't be automatically included in the manifest: the path must be relative\ndependency /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.10/site-packages/numpy/core/include/numpy/arrayscalars.h won't be automatically included in the manifest: the path must be relative\ndependency /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.10/site-packages/numpy/core/include/numpy/ndarrayobject.h won't be automatically included in the manifest: the path must be relative\ndependency /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.10/site-packages/numpy/core/include/numpy/ndarraytypes.h won't be automatically included in the manifest: the path must be relative\ndependency /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.10/site-packages/numpy/core/include/numpy/npy_math.h won't be automatically included in the manifest: the path must be relative\ndependency /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.10/site-packages/numpy/core/include/numpy/ufuncobject.h won't be automatically included in the manifest: the path must be relative\ndependency /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.10/site-packages/numpy/core/include/numpy/arrayobject.h won't be automatically included in the manifest: the path must be relative\ndependency /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.10/site-packages/numpy/core/include/numpy/arrayscalars.h won't be automatically included in the manifest: the path must be relative\ndependency /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.10/site-packages/numpy/core/include/numpy/ndarrayobject.h won't be automatically included in the manifest: the path must be relative\ndependency /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.10/site-packages/numpy/core/include/numpy/ndarraytypes.h won't be automatically included in the manifest: the path must be relative\ndependency /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.10/site-packages/numpy/core/include/numpy/ufuncobject.h won't be automatically included in the manifest: the path must be relative\ndependency /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.10/site-packages/numpy/core/include/numpy/arrayobject.h won't be automatically included in the manifest: the path must be relative\ndependency /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.10/site-packages/numpy/core/include/numpy/arrayscalars.h won't be automatically included in the manifest: the path must be relative\ndependency /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.10/site-packages/numpy/core/include/numpy/ndarrayobject.h won't be automatically included in the manifest: the path must be relative\ndependency /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.10/site-packages/numpy/core/include/numpy/ndarraytypes.h won't be automatically included in the manifest: the path must be relative\ndependency /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.10/site-packages/numpy/core/include/numpy/npy_math.h won't be automatically included in the manifest: the path must be relative\ndependency /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.10/site-packages/numpy/core/include/numpy/ufuncobject.h won't be automatically included in the manifest: the path must be relative\ndependency /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.10/site-packages/numpy/core/include/numpy/arrayobject.h won't be automatically included in the manifest: the path must be relative\ndependency /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.10/site-packages/numpy/core/include/numpy/arrayscalars.h won't be automatically included in the manifest: the path must be relative\ndependency /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.10/site-packages/numpy/core/include/numpy/ndarrayobject.h won't be automatically included in the manifest: the path must be relative\ndependency /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.10/site-packages/numpy/core/include/numpy/ndarraytypes.h won't be automatically included in the manifest: the path must be relative\ndependency /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.10/site-packages/numpy/core/include/numpy/npy_math.h won't be automatically included in the manifest: the path must be relative\ndependency /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.10/site-packages/numpy/core/include/numpy/ufuncobject.h won't be automatically included in the manifest: the path must be relative\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-cpython-310/Orange/classification/_simple_tree.cpython-310-x86_64-linux-gnu.so -> Orange/classification\ncopying build/lib.linux-x86_64-cpython-310/Orange/widgets/utils/_grid_density.cpython-310-x86_64-linux-gnu.so -> Orange/widgets/utils\ncopying build/lib.linux-x86_64-cpython-310/Orange/classification/_tree_scorers.cpython-310-x86_64-linux-gnu.so -> Orange/classification\ncopying build/lib.linux-x86_64-cpython-310/Orange/data/_contingency.cpython-310-x86_64-linux-gnu.so -> Orange/data\ncopying build/lib.linux-x86_64-cpython-310/Orange/data/_io.cpython-310-x86_64-linux-gnu.so -> Orange/data\ncopying build/lib.linux-x86_64-cpython-310/Orange/data/_valuecount.cpython-310-x86_64-linux-gnu.so -> Orange/data\ncopying build/lib.linux-x86_64-cpython-310/Orange/data/_variable.cpython-310-x86_64-linux-gnu.so -> Orange/data\ncopying build/lib.linux-x86_64-cpython-310/Orange/distance/_distance.cpython-310-x86_64-linux-gnu.so -> Orange/distance\ncopying build/lib.linux-x86_64-cpython-310/Orange/preprocess/_discretize.cpython-310-x86_64-linux-gnu.so -> Orange/preprocess\ncopying build/lib.linux-x86_64-cpython-310/Orange/preprocess/_relieff.cpython-310-x86_64-linux-gnu.so -> Orange/preprocess\ncopying build/lib.linux-x86_64-cpython-310/Orange/projection/_som.cpython-310-x86_64-linux-gnu.so -> Orange/projection\nCreating /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.10/site-packages/Orange3.egg-link (link to .)\nAdding Orange3 3.38.0.dev0+2a6f878 to easy-install.pth file\nInstalling orange-canvas script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/bin\n\nInstalled /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074\nProcessing dependencies for Orange3==3.38.0.dev0+2a6f878\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_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.10/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_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.10/site-packages\nSearching for xgboost==2.0.3\nBest match: xgboost 2.0.3\nAdding xgboost 2.0.3 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.10/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_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.10/site-packages\nSearching for scipy==1.15.0\nBest match: scipy 1.15.0\nAdding scipy 1.15.0 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.10/site-packages\nSearching for scikit-learn==1.6.0\nBest match: scikit-learn 1.6.0\nAdding scikit-learn 1.6.0 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.10/site-packages\nSearching for requests==2.32.3\nBest match: requests 2.32.3\nAdding requests 2.32.3 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.10/site-packages\nSearching for qtconsole==5.6.1\nBest match: qtconsole 5.6.1\nAdding qtconsole 5.6.1 to easy-install.pth file\nInstalling jupyter-qtconsole script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/bin\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.10/site-packages\nSearching for PyYAML==6.0.2\nBest match: PyYAML 6.0.2\nAdding PyYAML 6.0.2 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.10/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_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.10/site-packages\nSearching for pyqtgraph==0.13.7\nBest match: pyqtgraph 0.13.7\nAdding pyqtgraph 0.13.7 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.10/site-packages\nSearching for pygments==2.19.1\nBest match: pygments 2.19.1\nAdding pygments 2.19.1 to easy-install.pth file\nInstalling pygmentize script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/bin\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.10/site-packages\nSearching for pip==24.3.1\nBest match: pip 24.3.1\nAdding pip 24.3.1 to easy-install.pth file\nInstalling pip script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/bin\nInstalling pip3 script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/bin\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.10/site-packages\nSearching for pandas==2.2.3\nBest match: pandas 2.2.3\nAdding pandas 2.2.3 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.10/site-packages\nSearching for packaging==24.2\nBest match: packaging 24.2\nAdding packaging 24.2 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.10/site-packages/setuptools/_vendor\nSearching for orange-widget-base==4.25.1\nBest match: orange-widget-base 4.25.1\nAdding orange-widget-base 4.25.1 to easy-install.pth file\ndetected new path './setuptools/_vendor'\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.10/site-packages\nSearching for orange-canvas-core==0.2.5\nBest match: orange-canvas-core 0.2.5\nAdding orange-canvas-core 0.2.5 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.10/site-packages\nSearching for openpyxl==3.1.5\nBest match: openpyxl 3.1.5\nAdding openpyxl 3.1.5 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.10/site-packages\nSearching for openTSNE==1.0.2\nBest match: openTSNE 1.0.2\nAdding openTSNE 1.0.2 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.10/site-packages\nSearching for numpy==1.26.4\nBest match: numpy 1.26.4\nAdding numpy 1.26.4 to easy-install.pth file\nInstalling f2py script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/bin\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.10/site-packages\nSearching for networkx==3.4.2\nBest match: networkx 3.4.2\nAdding networkx 3.4.2 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.10/site-packages\nSearching for matplotlib==3.10.0\nBest match: matplotlib 3.10.0\nAdding matplotlib 3.10.0 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.10/site-packages\nSearching for keyrings.alt==5.0.2\nBest match: keyrings.alt 5.0.2\nAdding keyrings.alt 5.0.2 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.10/site-packages\nSearching for keyring==25.6.0\nBest match: keyring 25.6.0\nAdding keyring 25.6.0 to easy-install.pth file\nInstalling keyring script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/bin\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.10/site-packages\nSearching for joblib==1.4.2\nBest match: joblib 1.4.2\nAdding joblib 1.4.2 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.10/site-packages\nSearching for httpx==0.28.1\nBest match: httpx 0.28.1\nAdding httpx 0.28.1 to easy-install.pth file\nInstalling httpx script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/bin\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.10/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_2a6f878a820b39fb58a57d023999d8246a982074/.venv/bin\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.10/site-packages\nSearching for catboost==1.2.7\nBest match: catboost 1.2.7\nAdding catboost 1.2.7 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.10/site-packages\nSearching for Bottleneck==1.4.2\nBest match: Bottleneck 1.4.2\nAdding Bottleneck 1.4.2 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.10/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_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.10/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_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.10/site-packages\nSearching for threadpoolctl==3.5.0\nBest match: threadpoolctl 3.5.0\nAdding threadpoolctl 3.5.0 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.10/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_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.10/site-packages\nSearching for urllib3==2.3.0\nBest match: urllib3 2.3.0\nAdding urllib3 2.3.0 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.10/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_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.10/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_2a6f878a820b39fb58a57d023999d8246a982074/.venv/bin\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.10/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_2a6f878a820b39fb58a57d023999d8246a982074/.venv/bin\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.10/site-packages\nSearching for ipykernel==6.29.5\nBest match: ipykernel 6.29.5\nAdding ipykernel 6.29.5 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.10/site-packages\nSearching for jupyter-client==8.6.3\nBest match: jupyter-client 8.6.3\nAdding jupyter-client 8.6.3 to easy-install.pth file\nInstalling jupyter-kernel script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/bin\nInstalling jupyter-kernelspec script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/bin\nInstalling jupyter-run script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/bin\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.10/site-packages\nSearching for jupyter-core==5.7.2\nBest match: jupyter-core 5.7.2\nAdding jupyter-core 5.7.2 to easy-install.pth file\nInstalling jupyter script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/bin\nInstalling jupyter-migrate script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/bin\nInstalling jupyter-troubleshoot script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/bin\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.10/site-packages\nSearching for traitlets==5.14.3\nBest match: traitlets 5.14.3\nAdding traitlets 5.14.3 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.10/site-packages\nSearching for tzdata==2024.2\nBest match: tzdata 2024.2\nAdding tzdata 2024.2 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.10/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_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.10/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_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.10/site-packages\nSearching for typing-extensions==4.12.2\nBest match: typing-extensions 4.12.2\ntyping-extensions 4.12.2 is already the active version in easy-install.pth\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.10/site-packages/setuptools/_vendor\nSearching for qasync==0.27.1\nBest match: qasync 0.27.1\nAdding qasync 0.27.1 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.10/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_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.10/site-packages\nSearching for requests-cache==1.2.1\nBest match: requests-cache 1.2.1\nAdding requests-cache 1.2.1 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.10/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_2a6f878a820b39fb58a57d023999d8246a982074/.venv/bin\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.10/site-packages\nSearching for docutils==0.21.2\nBest match: docutils 0.21.2\nAdding docutils 0.21.2 to easy-install.pth file\nInstalling docutils script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/bin\nInstalling rst2html script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/bin\nInstalling rst2html4 script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/bin\nInstalling rst2html5 script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/bin\nInstalling rst2latex script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/bin\nInstalling rst2man script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/bin\nInstalling rst2odt script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/bin\nInstalling rst2pseudoxml script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/bin\nInstalling rst2s5 script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/bin\nInstalling rst2xetex script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/bin\nInstalling rst2xml script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/bin\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.10/site-packages\nSearching for et-xmlfile==2.0.0\nBest match: et-xmlfile 2.0.0\nAdding et-xmlfile 2.0.0 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.10/site-packages\nSearching for pyparsing==3.2.1\nBest match: pyparsing 3.2.1\nAdding pyparsing 3.2.1 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.10/site-packages\nSearching for pillow==11.1.0\nBest match: pillow 11.1.0\nAdding pillow 11.1.0 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.10/site-packages\nSearching for kiwisolver==1.4.8\nBest match: kiwisolver 1.4.8\nAdding kiwisolver 1.4.8 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.10/site-packages\nSearching for fonttools==4.55.3\nBest match: fonttools 4.55.3\nAdding fonttools 4.55.3 to easy-install.pth file\nInstalling fonttools script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/bin\nInstalling pyftmerge script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/bin\nInstalling pyftsubset script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/bin\nInstalling ttx script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/bin\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.10/site-packages\nSearching for cycler==0.12.1\nBest match: cycler 0.12.1\nAdding cycler 0.12.1 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.10/site-packages\nSearching for contourpy==1.3.1\nBest match: contourpy 1.3.1\nAdding contourpy 1.3.1 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.10/site-packages\nSearching for jaraco.context==6.0.1\nBest match: jaraco.context 6.0.1\nAdding jaraco.context 6.0.1 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.10/site-packages\nSearching for jaraco.classes==3.4.0\nBest match: jaraco.classes 3.4.0\nAdding jaraco.classes 3.4.0 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.10/site-packages\nSearching for jaraco.functools==4.1.0\nBest match: jaraco.functools 4.1.0\nAdding jaraco.functools 4.1.0 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.10/site-packages\nSearching for importlib-metadata==8.5.0\nBest match: importlib-metadata 8.5.0\nAdding importlib-metadata 8.5.0 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.10/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_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.10/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_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.10/site-packages\nSearching for httpcore==1.0.7\nBest match: httpcore 1.0.7\nAdding httpcore 1.0.7 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.10/site-packages\nSearching for anyio==4.8.0\nBest match: anyio 4.8.0\nAdding anyio 4.8.0 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.10/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_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.10/site-packages\nSearching for plotly==5.24.1\nBest match: plotly 5.24.1\nAdding plotly 5.24.1 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.10/site-packages\nSearching for graphviz==0.20.3\nBest match: graphviz 0.20.3\nAdding graphviz 0.20.3 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.10/site-packages\nSearching for tornado==6.4.2\nBest match: tornado 6.4.2\nAdding tornado 6.4.2 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.10/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_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.10/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_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.10/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_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.10/site-packages\nSearching for matplotlib-inline==0.1.7\nBest match: matplotlib-inline 0.1.7\nAdding matplotlib-inline 0.1.7 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.10/site-packages\nSearching for ipython==8.31.0\nBest match: ipython 8.31.0\nAdding ipython 8.31.0 to easy-install.pth file\nInstalling ipython script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/bin\nInstalling ipython3 script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/bin\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.10/site-packages\nSearching for debugpy==1.8.11\nBest match: debugpy 1.8.11\nAdding debugpy 1.8.11 to easy-install.pth file\nInstalling debugpy script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/bin\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.10/site-packages\nSearching for comm==0.2.2\nBest match: comm 0.2.2\nAdding comm 0.2.2 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.10/site-packages\nSearching for platformdirs==4.3.6\nBest match: platformdirs 4.3.6\nAdding platformdirs 4.3.6 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.10/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_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.10/site-packages\nSearching for cattrs==24.1.2\nBest match: cattrs 24.1.2\nAdding cattrs 24.1.2 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.10/site-packages\nSearching for attrs==24.3.0\nBest match: attrs 24.3.0\nAdding attrs 24.3.0 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.10/site-packages\nSearching for backports.tarfile==1.2.0\nBest match: backports.tarfile 1.2.0\nbackports.tarfile 1.2.0 is already the active version in easy-install.pth\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.10/site-packages/setuptools/_vendor\nSearching for more-itertools==10.5.0\nBest match: more-itertools 10.5.0\nAdding more-itertools 10.5.0 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.10/site-packages\nSearching for zipp==3.21.0\nBest match: zipp 3.21.0\nAdding zipp 3.21.0 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.10/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_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.10/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_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.10/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_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.10/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_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.10/site-packages\nSearching for tenacity==9.0.0\nBest match: tenacity 9.0.0\nAdding tenacity 9.0.0 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.10/site-packages\nSearching for stack-data==0.6.3\nBest match: stack-data 0.6.3\nAdding stack-data 0.6.3 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.10/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_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.10/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_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.10/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_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.10/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_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.10/site-packages\nSearching for cffi==1.17.1\nBest match: cffi 1.17.1\nAdding cffi 1.17.1 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.10/site-packages\nSearching for pure-eval==0.2.3\nBest match: pure-eval 0.2.3\nAdding pure-eval 0.2.3 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.10/site-packages\nSearching for asttokens==3.0.0\nBest match: asttokens 3.0.0\nAdding asttokens 3.0.0 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.10/site-packages\nSearching for executing==2.1.0\nBest match: executing 2.1.0\nAdding executing 2.1.0 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.10/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_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.10/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_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.10/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_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.10/site-packages\nSearching for pycparser==2.22\nBest match: pycparser 2.22\nAdding pycparser 2.22 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.10/site-packages\nFinished processing dependencies for Orange3==3.38.0.dev0+2a6f878\nVerifying Orange installation...\n✅ Orange installation successful!\nSuccessfully installed orange using Python 3.10\n",
    "setup_res_stderr": "Using CPython 3.7.9\nCreating virtual environment at: .venv\nActivate with: source .venv/bin/activate\nResolved 18 packages in 183ms\nPrepared 18 packages in 1ms\nInstalled 18 packages in 178ms\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\n  × No solution found when resolving dependencies:\n  ╰─▶ Because the current Python version (3.7.9) does not satisfy Python>=3.8 and all of:\n          pandas>=1.4.0,<=1.4.4\n          pandas>=1.5.1,<=1.5.3\n          pandas>=2.0.1,<=2.0.3\n      depend on Python>=3.8, we can conclude that all of:\n          pandas>=1.4.0,<=1.4.4\n          pandas>=1.5.1,<=1.5.3\n          pandas>=2.0.1,<=2.0.3\n       cannot be used.\n      And because only the following versions of pandas are available:\n          pandas<=1.4.0\n          pandas==1.4.1\n          pandas==1.4.2\n          pandas==1.4.3\n          pandas==1.4.4\n          pandas==1.5.0\n          pandas==1.5.1\n          pandas==1.5.2\n          pandas==1.5.3\n          pandas==2.0.0\n          pandas==2.0.1\n          pandas==2.0.2\n          pandas==2.0.3\n          pandas==2.1.0\n          pandas==2.1.1\n          pandas==2.1.2\n          pandas==2.1.3\n          pandas==2.1.4\n          pandas==2.2.0\n          pandas==2.2.1\n          pandas==2.2.2\n          pandas==2.2.3\n      we can conclude that all of:\n          pandas>=1.4.0,<1.5.0\n          pandas>1.5.0,<2.0.0\n          pandas>2.0.0,<2.1.0\n       cannot be used. (1)\n\n      Because the current Python version (3.7.9) does not satisfy Python>=3.9 and pandas>=2.1.0 depends on Python>=3.9, we can\n      conclude that pandas>=2.1.0 cannot be used.\n      And because we know from (1) that all of:\n          pandas>=1.4.0,<1.5.0\n          pandas>1.5.0,<2.0.0\n          pandas>2.0.0,<2.1.0\n       cannot be used, we can conclude that all of:\n          pandas>=1.4.0,<1.5.0\n          pandas>1.5.0,<2.0.0\n          pandas>2.0.0\n       cannot be used.\n      And because you require one of:\n          pandas>=1.4.0,<1.5.0\n          pandas>1.5.0,<2.0.0\n          pandas>2.0.0\n      we can conclude that your requirements are unsatisfiable.\n\n      hint: Pre-releases are available for `pandas` in the requested range (e.g., 2.2.0rc0), but pre-releases weren't enabled\n      (try: `--prerelease=allow`)\n  × No solution found when resolving dependencies:\n  ╰─▶ Because the current Python version (3.7.9) does not satisfy Python>=3.8 and pyqtgraph>=0.13.1,<=0.13.3 depends on\n      Python>=3.8, we can conclude that pyqtgraph>=0.13.1,<=0.13.3 cannot be used.\n      And because only the following versions of pyqtgraph are available:\n          pyqtgraph<=0.13.1\n          pyqtgraph==0.13.2\n          pyqtgraph==0.13.3\n          pyqtgraph==0.13.4\n          pyqtgraph==0.13.5\n          pyqtgraph==0.13.6\n          pyqtgraph==0.13.7\n      we can conclude that pyqtgraph>=0.13.1,<0.13.4 cannot be used. (1)\n\n      Because the current Python version (3.7.9) does not satisfy Python>=3.9 and pyqtgraph>=0.13.4 depends on Python>=3.9, we\n      can conclude that pyqtgraph>=0.13.4 cannot be used.\n      And because we know from (1) that pyqtgraph>=0.13.1,<0.13.4 cannot be used, we can conclude that pyqtgraph>=0.13.1 cannot\n      be used.\n      And because you require pyqtgraph>=0.13.1, we can conclude that your requirements are unsatisfiable.\nResolved 2 packages in 54ms\nInstalled 2 packages in 18ms\n + psycopg2==2.9.9\n + pymssql==2.3.2\nResolved 5 packages in 36ms\nInstalled 4 packages in 587ms\n + joblib==1.3.2\n + scikit-learn==1.0.2\n + scipy==1.7.3\n + threadpoolctl==3.1.0\nIn file included from /home/gcpuser/.local/share/uv/python/cpython-3.7.9-linux-x86_64-gnu/include/python3.7m/Python.h:8,\n                 from Orange/classification/_simple_tree.c:781:\n/home/gcpuser/.local/share/uv/python/cpython-3.7.9-linux-x86_64-gnu/include/python3.7m/pyconfig.h:1504: warning: \"_GNU_SOURCE\" redefined\n 1504 | #define _GNU_SOURCE 1\n      | \nOrange/classification/_simple_tree.c:2: note: this is the location of the previous definition\n    2 | #define _GNU_SOURCE\n      | \nIn file included from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.7/site-packages/numpy/core/include/numpy/ndarraytypes.h:1830,\n                 from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.7/site-packages/numpy/core/include/numpy/ndarrayobject.h:12,\n                 from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.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_2a6f878a820b39fb58a57d023999d8246a982074/.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:3426:27: warning: comparison of integer expressions of different signedness: ‘unsigned int’ and ‘int’ [-Wsign-compare]\n 3426 |   __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:4872:27: warning: comparison of integer expressions of different signedness: ‘unsigned int’ and ‘int’ [-Wsign-compare]\n 4872 |   __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:5618:43: warning: comparison of integer expressions of different signedness: ‘unsigned int’ and ‘int’ [-Wsign-compare]\n 5618 |           for (__pyx_t_19 = 0; __pyx_t_19 < __pyx_t_18; __pyx_t_19+=1) {\n      |                                           ^\nOrange/classification/_tree_scorers.c:5739:39: warning: comparison of integer expressions of different signedness: ‘unsigned int’ and ‘int’ [-Wsign-compare]\n 5739 |           __pyx_t_20 = ((__pyx_v_left >= __pyx_v_min_leaf) != 0);\n      |                                       ^~\nOrange/classification/_tree_scorers.c:5745:53: warning: comparison of integer expressions of different signedness: ‘unsigned int’ and ‘int’ [-Wsign-compare]\n 5745 |           __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:7206:41: warning: comparison of integer expressions of different signedness: ‘unsigned int’ and ‘int’ [-Wsign-compare]\n 7206 |         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:7863:41: warning: comparison of integer expressions of different signedness: ‘unsigned int’ and ‘int’ [-Wsign-compare]\n 7863 |         for (__pyx_t_12 = 0; __pyx_t_12 < __pyx_t_11; __pyx_t_12+=1) {\n      |                                         ^\nOrange/classification/_tree_scorers.c:7923:150: warning: comparison of integer expressions of different signedness: ‘__pyx_t_5numpy_int32_t’ {aka ‘int’} and ‘unsigned int’ [-Wsign-compare]\n 7923 |               __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_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.7/site-packages/numpy/core/include/numpy/ndarraytypes.h:1830,\n                 from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.7/site-packages/numpy/core/include/numpy/ndarrayobject.h:12,\n                 from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.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_2a6f878a820b39fb58a57d023999d8246a982074/.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_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.7/site-packages/numpy/core/include/numpy/ndarraytypes.h:1830,\n                 from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.7/site-packages/numpy/core/include/numpy/ndarrayobject.h:12,\n                 from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.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_2a6f878a820b39fb58a57d023999d8246a982074/.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_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.7/site-packages/numpy/core/include/numpy/ndarraytypes.h:1830,\n                 from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.7/site-packages/numpy/core/include/numpy/ndarrayobject.h:12,\n                 from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.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_2a6f878a820b39fb58a57d023999d8246a982074/.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_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.7/site-packages/numpy/core/include/numpy/ndarraytypes.h:1830,\n                 from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.7/site-packages/numpy/core/include/numpy/ndarrayobject.h:12,\n                 from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.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_2a6f878a820b39fb58a57d023999d8246a982074/.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_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.7/site-packages/numpy/core/include/numpy/ndarraytypes.h:1830,\n                 from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.7/site-packages/numpy/core/include/numpy/ndarrayobject.h:12,\n                 from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.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_2a6f878a820b39fb58a57d023999d8246a982074/.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_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.7/site-packages/numpy/core/include/numpy/ndarraytypes.h:1830,\n                 from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.7/site-packages/numpy/core/include/numpy/ndarrayobject.h:12,\n                 from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.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_2a6f878a820b39fb58a57d023999d8246a982074/.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:3944:7: warning: ‘__pyx_v_win_y’ may be used uninitialized in this function [-Wmaybe-uninitialized]\n 3944 |   int __pyx_v_win_y;\n      |       ^~~~~~~~~~~~~\nOrange/projection/_som.c:3943:7: warning: ‘__pyx_v_win_x’ may be used uninitialized in this function [-Wmaybe-uninitialized]\n 3943 |   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:5094:7: warning: ‘__pyx_v_win_y’ may be used uninitialized in this function [-Wmaybe-uninitialized]\n 5094 |   int __pyx_v_win_y;\n      |       ^~~~~~~~~~~~~\nOrange/projection/_som.c:5093:7: warning: ‘__pyx_v_win_x’ may be used uninitialized in this function [-Wmaybe-uninitialized]\n 5093 |   int __pyx_v_win_x;\n      |       ^~~~~~~~~~~~~\nOrange/projection/_som.c: In function ‘__pyx_pw_6Orange_10projection_4_som_1get_winners’:\nOrange/projection/_som.c:3244:223: warning: ‘__pyx_v_win_y’ may be used uninitialized in this function [-Wmaybe-uninitialized]\n 3244 |           *__Pyx_BufPtrStrided2d(__pyx_t_5numpy_int16_t *, __pyx_pybuffernd_winners.rcbuffer->pybuffer.buf, __pyx_t_23, __pyx_pybuffernd_winners.diminfo[0].strides, __pyx_t_24, __pyx_pybuffernd_winners.diminfo[1].strides) = __pyx_v_win_y;\n      |           ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^~~~~~~~~~~~~~~\nOrange/projection/_som.c:2892:7: note: ‘__pyx_v_win_y’ was declared here\n 2892 |   int __pyx_v_win_y;\n      |       ^~~~~~~~~~~~~\nOrange/projection/_som.c:3233:223: warning: ‘__pyx_v_win_x’ may be used uninitialized in this function [-Wmaybe-uninitialized]\n 3233 |           *__Pyx_BufPtrStrided2d(__pyx_t_5numpy_int16_t *, __pyx_pybuffernd_winners.rcbuffer->pybuffer.buf, __pyx_t_24, __pyx_pybuffernd_winners.diminfo[0].strides, __pyx_t_23, __pyx_pybuffernd_winners.diminfo[1].strides) = __pyx_v_win_x;\n      |           ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^~~~~~~~~~~~~~~\nOrange/projection/_som.c:2891:7: note: ‘__pyx_v_win_x’ was declared here\n 2891 |   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:5842:7: warning: ‘__pyx_v_win_y’ may be used uninitialized in this function [-Wmaybe-uninitialized]\n 5842 |   int __pyx_v_win_y;\n      |       ^~~~~~~~~~~~~\nOrange/projection/_som.c:5841:7: warning: ‘__pyx_v_win_x’ may be used uninitialized in this function [-Wmaybe-uninitialized]\n 5841 |   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:4871:223: warning: ‘__pyx_v_win_y’ may be used uninitialized in this function [-Wmaybe-uninitialized]\n 4871 |           *__Pyx_BufPtrStrided2d(__pyx_t_5numpy_int16_t *, __pyx_pybuffernd_winners.rcbuffer->pybuffer.buf, __pyx_t_27, __pyx_pybuffernd_winners.diminfo[0].strides, __pyx_t_28, __pyx_pybuffernd_winners.diminfo[1].strides) = __pyx_v_win_y;\n      |           ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^~~~~~~~~~~~~~~\nOrange/projection/_som.c:4389:7: note: ‘__pyx_v_win_y’ was declared here\n 4389 |   int __pyx_v_win_y;\n      |       ^~~~~~~~~~~~~\nOrange/projection/_som.c:4860:223: warning: ‘__pyx_v_win_x’ may be used uninitialized in this function [-Wmaybe-uninitialized]\n 4860 |           *__Pyx_BufPtrStrided2d(__pyx_t_5numpy_int16_t *, __pyx_pybuffernd_winners.rcbuffer->pybuffer.buf, __pyx_t_28, __pyx_pybuffernd_winners.diminfo[0].strides, __pyx_t_27, __pyx_pybuffernd_winners.diminfo[1].strides) = __pyx_v_win_x;\n      |           ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^~~~~~~~~~~~~~~\nOrange/projection/_som.c:4388:7: note: ‘__pyx_v_win_x’ was declared here\n 4388 |   int __pyx_v_win_x;\n      |       ^~~~~~~~~~~~~\nOrange/projection/_som.c: In function ‘__pyx_pw_6Orange_10projection_4_som_3update’:\nOrange/projection/_som.c:3438:7: warning: ‘__pyx_v_win_y’ may be used uninitialized in this function [-Wmaybe-uninitialized]\n 3438 |   int __pyx_v_win_y;\n      |       ^~~~~~~~~~~~~\n/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.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_2a6f878a820b39fb58a57d023999d8246a982074/.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/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.7/site-packages/pkg_resources/__init__.py:119: PkgResourcesDeprecationWarning:  is an invalid version and will not be supported in a future release\n  PkgResourcesDeprecationWarning,\n/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.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,\n/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.7/site-packages/pkg_resources/__init__.py:119: PkgResourcesDeprecationWarning:  is an invalid version and will not be supported in a future release\n  PkgResourcesDeprecationWarning,\nerror: Couldn't find a setup script in /tmp/easy_install-ykzzps8v/scipy-1.15.0.tar.gz\nUsing CPython 3.8.20\nCreating virtual environment at: .venv\nActivate with: source .venv/bin/activate\nResolved 15 packages in 80ms\nPrepared 15 packages in 0.98ms\nInstalled 15 packages in 30ms\n + cython==0.29.37\n + exceptiongroup==1.2.2\n + iniconfig==2.0.0\n + numpy==1.24.4\n + packaging==24.2\n + pluggy==1.5.0\n + pyqt5==5.15.11\n + pyqt5-qt5==5.15.16\n + pyqt5-sip==12.15.0\n + pyqtwebengine==5.15.7\n + pyqtwebengine-qt5==5.15.16\n + pytest==8.3.4\n + setuptools==75.3.0\n + tomli==2.2.1\n + wheel==0.45.1\n  × No solution found when resolving dependencies:\n  ╰─▶ Because the current Python version (3.8.20) does not satisfy Python>=3.9 and scikit-learn>=1.4.0 depends on Python>=3.9,\n      we can conclude that scikit-learn>=1.4.0 cannot be used.\n      And because only the following versions of scikit-learn are available:\n          scikit-learn<=1.4.0\n          scikit-learn==1.4.1.post1\n          scikit-learn==1.4.2\n          scikit-learn==1.5.0\n          scikit-learn==1.5.1\n          scikit-learn==1.5.2\n          scikit-learn==1.6.0\n      and you require scikit-learn>=1.4.0, we can conclude that your requirements are unsatisfiable.\n\n      hint: Pre-releases are available for `scikit-learn` in the requested range (e.g., 1.6.0rc1), but pre-releases weren't\n      enabled (try: `--prerelease=allow`)\nResolved 63 packages in 100ms\nInstalled 60 packages in 65ms\n + anyqt==0.2.0\n + asttokens==3.0.0\n + attrs==24.3.0\n + backcall==0.2.0\n + cattrs==24.1.2\n + certifi==2024.12.14\n + charset-normalizer==3.4.1\n + comm==0.2.2\n + commonmark==0.9.1\n + contourpy==1.1.1\n + cycler==0.12.1\n + debugpy==1.8.11\n + decorator==5.1.1\n + dictdiffer==0.9.0\n + docutils==0.20.1\n + executing==2.1.0\n + fonttools==4.55.3\n + idna==3.10\n + importlib-metadata==8.5.0\n + importlib-resources==6.4.5\n + ipykernel==6.29.5\n + ipython==8.12.3\n + jedi==0.19.2\n + jupyter-client==8.6.3\n + jupyter-core==5.7.2\n + kiwisolver==1.4.7\n + matplotlib==3.7.5\n + matplotlib-inline==0.1.7\n + nest-asyncio==1.6.0\n + orange-canvas-core==0.2.2\n + orange-widget-base==4.24.0\n + parso==0.8.4\n + pexpect==4.9.0\n + pickleshare==0.7.5\n + pillow==10.4.0\n + pip==24.3.1\n + platformdirs==4.3.6\n + prompt-toolkit==3.0.48\n + psutil==6.1.1\n + ptyprocess==0.7.0\n + pure-eval==0.2.3\n + pygments==2.19.1\n + pyparsing==3.1.4\n + pyqtgraph==0.13.3\n + python-dateutil==2.9.0.post0\n + pyzmq==26.2.0\n + qasync==0.27.1\n + qtconsole==5.6.1\n + qtpy==2.4.2\n + requests==2.32.3\n + requests-cache==1.2.1\n + six==1.17.0\n + stack-data==0.6.3\n + tornado==6.4.2\n + traitlets==5.14.3\n + typing-extensions==4.12.2\n + url-normalize==1.4.3\n + urllib3==2.2.3\n + wcwidth==0.2.13\n + zipp==3.20.2\nResolved 2 packages in 2ms\nInstalled 2 packages in 4ms\n + psycopg2==2.9.10\n + pymssql==2.3.2\nIn file included from /home/gcpuser/.local/share/uv/python/cpython-3.8.20-linux-x86_64-gnu/include/python3.8/Python.h:8,\n                 from Orange/classification/_simple_tree.c:781:\n/home/gcpuser/.local/share/uv/python/cpython-3.8.20-linux-x86_64-gnu/include/python3.8/pyconfig.h:1578: warning: \"_GNU_SOURCE\" redefined\n 1578 | #define _GNU_SOURCE 1\n      | \nOrange/classification/_simple_tree.c:2: note: this is the location of the previous definition\n    2 | #define _GNU_SOURCE\n      | \nIn file included from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.8/site-packages/numpy/core/include/numpy/ndarraytypes.h:1940,\n                 from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.8/site-packages/numpy/core/include/numpy/ndarrayobject.h:12,\n                 from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.8/site-packages/numpy/core/include/numpy/arrayobject.h:5,\n                 from Orange/classification/_tree_scorers.c:775:\n/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.8/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:3426:27: warning: comparison of integer expressions of different signedness: ‘unsigned int’ and ‘int’ [-Wsign-compare]\n 3426 |   __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:4872:27: warning: comparison of integer expressions of different signedness: ‘unsigned int’ and ‘int’ [-Wsign-compare]\n 4872 |   __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:5618:43: warning: comparison of integer expressions of different signedness: ‘unsigned int’ and ‘int’ [-Wsign-compare]\n 5618 |           for (__pyx_t_19 = 0; __pyx_t_19 < __pyx_t_18; __pyx_t_19+=1) {\n      |                                           ^\nOrange/classification/_tree_scorers.c:5739:39: warning: comparison of integer expressions of different signedness: ‘unsigned int’ and ‘int’ [-Wsign-compare]\n 5739 |           __pyx_t_20 = ((__pyx_v_left >= __pyx_v_min_leaf) != 0);\n      |                                       ^~\nOrange/classification/_tree_scorers.c:5745:53: warning: comparison of integer expressions of different signedness: ‘unsigned int’ and ‘int’ [-Wsign-compare]\n 5745 |           __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:7206:41: warning: comparison of integer expressions of different signedness: ‘unsigned int’ and ‘int’ [-Wsign-compare]\n 7206 |         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:7863:41: warning: comparison of integer expressions of different signedness: ‘unsigned int’ and ‘int’ [-Wsign-compare]\n 7863 |         for (__pyx_t_12 = 0; __pyx_t_12 < __pyx_t_11; __pyx_t_12+=1) {\n      |                                         ^\nOrange/classification/_tree_scorers.c:7923:150: warning: comparison of integer expressions of different signedness: ‘__pyx_t_5numpy_int32_t’ {aka ‘int’} and ‘unsigned int’ [-Wsign-compare]\n 7923 |               __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_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.8/site-packages/numpy/core/include/numpy/ndarraytypes.h:1940,\n                 from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.8/site-packages/numpy/core/include/numpy/ndarrayobject.h:12,\n                 from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.8/site-packages/numpy/core/include/numpy/arrayobject.h:5,\n                 from Orange/data/_contingency.c:775:\n/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.8/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_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.8/site-packages/numpy/core/include/numpy/ndarraytypes.h:1940,\n                 from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.8/site-packages/numpy/core/include/numpy/ndarrayobject.h:12,\n                 from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.8/site-packages/numpy/core/include/numpy/arrayobject.h:5,\n                 from Orange/data/_valuecount.c:775:\n/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.8/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_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.8/site-packages/numpy/core/include/numpy/ndarraytypes.h:1940,\n                 from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.8/site-packages/numpy/core/include/numpy/ndarrayobject.h:12,\n                 from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.8/site-packages/numpy/core/include/numpy/arrayobject.h:5,\n                 from Orange/distance/_distance.c:775:\n/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.8/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_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.8/site-packages/numpy/core/include/numpy/ndarraytypes.h:1940,\n                 from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.8/site-packages/numpy/core/include/numpy/ndarrayobject.h:12,\n                 from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.8/site-packages/numpy/core/include/numpy/arrayobject.h:5,\n                 from Orange/preprocess/_discretize.c:774:\n/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.8/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_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.8/site-packages/numpy/core/include/numpy/ndarraytypes.h:1940,\n                 from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.8/site-packages/numpy/core/include/numpy/ndarrayobject.h:12,\n                 from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.8/site-packages/numpy/core/include/numpy/arrayobject.h:5,\n                 from Orange/preprocess/_relieff.cpp:790:\n/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.8/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_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.8/site-packages/numpy/core/include/numpy/ndarraytypes.h:1940,\n                 from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.8/site-packages/numpy/core/include/numpy/ndarrayobject.h:12,\n                 from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.8/site-packages/numpy/core/include/numpy/arrayobject.h:5,\n                 from Orange/projection/_som.c:775:\n/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.8/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:3944:7: warning: ‘__pyx_v_win_y’ may be used uninitialized in this function [-Wmaybe-uninitialized]\n 3944 |   int __pyx_v_win_y;\n      |       ^~~~~~~~~~~~~\nOrange/projection/_som.c:3943:7: warning: ‘__pyx_v_win_x’ may be used uninitialized in this function [-Wmaybe-uninitialized]\n 3943 |   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:5094:7: warning: ‘__pyx_v_win_y’ may be used uninitialized in this function [-Wmaybe-uninitialized]\n 5094 |   int __pyx_v_win_y;\n      |       ^~~~~~~~~~~~~\nOrange/projection/_som.c:5093:7: warning: ‘__pyx_v_win_x’ may be used uninitialized in this function [-Wmaybe-uninitialized]\n 5093 |   int __pyx_v_win_x;\n      |       ^~~~~~~~~~~~~\nOrange/projection/_som.c: In function ‘__pyx_pw_6Orange_10projection_4_som_3update’:\nOrange/projection/_som.c:3438:7: warning: ‘__pyx_v_win_y’ may be used uninitialized in this function [-Wmaybe-uninitialized]\n 3438 |   int __pyx_v_win_y;\n      |       ^~~~~~~~~~~~~\nOrange/projection/_som.c: In function ‘__pyx_pw_6Orange_10projection_4_som_1get_winners’:\nOrange/projection/_som.c:3244:223: warning: ‘__pyx_v_win_y’ may be used uninitialized in this function [-Wmaybe-uninitialized]\n 3244 |           *__Pyx_BufPtrStrided2d(__pyx_t_5numpy_int16_t *, __pyx_pybuffernd_winners.rcbuffer->pybuffer.buf, __pyx_t_23, __pyx_pybuffernd_winners.diminfo[0].strides, __pyx_t_24, __pyx_pybuffernd_winners.diminfo[1].strides) = __pyx_v_win_y;\n      |           ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^~~~~~~~~~~~~~~\nOrange/projection/_som.c:2892:7: note: ‘__pyx_v_win_y’ was declared here\n 2892 |   int __pyx_v_win_y;\n      |       ^~~~~~~~~~~~~\nOrange/projection/_som.c:3233:223: warning: ‘__pyx_v_win_x’ may be used uninitialized in this function [-Wmaybe-uninitialized]\n 3233 |           *__Pyx_BufPtrStrided2d(__pyx_t_5numpy_int16_t *, __pyx_pybuffernd_winners.rcbuffer->pybuffer.buf, __pyx_t_24, __pyx_pybuffernd_winners.diminfo[0].strides, __pyx_t_23, __pyx_pybuffernd_winners.diminfo[1].strides) = __pyx_v_win_x;\n      |           ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^~~~~~~~~~~~~~~\nOrange/projection/_som.c:2891:7: note: ‘__pyx_v_win_x’ was declared here\n 2891 |   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:5842:7: warning: ‘__pyx_v_win_y’ may be used uninitialized in this function [-Wmaybe-uninitialized]\n 5842 |   int __pyx_v_win_y;\n      |       ^~~~~~~~~~~~~\nOrange/projection/_som.c:5841:7: warning: ‘__pyx_v_win_x’ may be used uninitialized in this function [-Wmaybe-uninitialized]\n 5841 |   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:4871:223: warning: ‘__pyx_v_win_y’ may be used uninitialized in this function [-Wmaybe-uninitialized]\n 4871 |           *__Pyx_BufPtrStrided2d(__pyx_t_5numpy_int16_t *, __pyx_pybuffernd_winners.rcbuffer->pybuffer.buf, __pyx_t_27, __pyx_pybuffernd_winners.diminfo[0].strides, __pyx_t_28, __pyx_pybuffernd_winners.diminfo[1].strides) = __pyx_v_win_y;\n      |           ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^~~~~~~~~~~~~~~\nOrange/projection/_som.c:4389:7: note: ‘__pyx_v_win_y’ was declared here\n 4389 |   int __pyx_v_win_y;\n      |       ^~~~~~~~~~~~~\nOrange/projection/_som.c:4860:223: warning: ‘__pyx_v_win_x’ may be used uninitialized in this function [-Wmaybe-uninitialized]\n 4860 |           *__Pyx_BufPtrStrided2d(__pyx_t_5numpy_int16_t *, __pyx_pybuffernd_winners.rcbuffer->pybuffer.buf, __pyx_t_28, __pyx_pybuffernd_winners.diminfo[0].strides, __pyx_t_27, __pyx_pybuffernd_winners.diminfo[1].strides) = __pyx_v_win_x;\n      |           ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^~~~~~~~~~~~~~~\nOrange/projection/_som.c:4388:7: note: ‘__pyx_v_win_x’ was declared here\n 4388 |   int __pyx_v_win_x;\n      |       ^~~~~~~~~~~~~\n/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.8/site-packages/setuptools/_distutils/dist.py:261: UserWarning: Unknown distribution option: 'test_suite'\n  warnings.warn(msg)\n/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.8/site-packages/setuptools/_distutils/dist.py:261: UserWarning: Unknown distribution option: 'test_suite'\n  warnings.warn(msg)\n/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.8/site-packages/setuptools/command/develop.py:41: EasyInstallDeprecationWarning: easy_install command is deprecated.\n!!\n\n        ********************************************************************************\n        Please avoid running ``setup.py`` and ``easy_install``.\n        Instead, use pypa/build, pypa/installer or other\n        standards-based tools.\n\n        See https://github.com/pypa/setuptools/issues/917 for details.\n        ********************************************************************************\n\n!!\n  easy_install.initialize_options(self)\n/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.8/site-packages/setuptools/_distutils/cmd.py:66: SetuptoolsDeprecationWarning: setup.py install is deprecated.\n!!\n\n        ********************************************************************************\n        Please avoid running ``setup.py`` directly.\n        Instead, use pypa/build, pypa/installer or other\n        standards-based tools.\n\n        See https://blog.ganssle.io/articles/2021/10/setup-py-deprecated.html for details.\n        ********************************************************************************\n\n!!\n  self.initialize_options()\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/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.8/site-packages/setuptools/_distutils/dist.py:261: UserWarning: Unknown distribution option: 'test_suite'\n  warnings.warn(msg)\n/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.8/site-packages/setuptools/_distutils/cmd.py:66: SetuptoolsDeprecationWarning: setup.py install is deprecated.\n!!\n\n        ********************************************************************************\n        Please avoid running ``setup.py`` directly.\n        Instead, use pypa/build, pypa/installer or other\n        standards-based tools.\n\n        See https://blog.ganssle.io/articles/2021/10/setup-py-deprecated.html for details.\n        ********************************************************************************\n\n!!\n  self.initialize_options()\nerror: Couldn't find a setup script in /tmp/easy_install-hax1ap46/scipy-1.15.0.tar.gz\nUsing CPython 3.10.16\nCreating virtual environment at: .venv\nActivate with: source .venv/bin/activate\nResolved 15 packages in 10ms\nPrepared 15 packages in 1ms\nInstalled 15 packages in 47ms\n + cython==3.0.11\n + exceptiongroup==1.2.2\n + iniconfig==2.0.0\n + numpy==2.2.1\n + packaging==24.2\n + pluggy==1.5.0\n + pyqt5==5.15.11\n + pyqt5-qt5==5.15.16\n + pyqt5-sip==12.16.1\n + pyqtwebengine==5.15.7\n + pyqtwebengine-qt5==5.15.16\n + pytest==8.3.4\n + setuptools==75.8.0\n + tomli==2.2.1\n + wheel==0.45.1\nResolved 62 packages in 26ms\nUninstalled 1 package in 10ms\nInstalled 60 packages in 400ms\n + anyio==4.8.0\n + backports-tarfile==1.2.0\n + baycomp==1.0.3\n + bottleneck==1.4.2\n + catboost==1.2.7\n + certifi==2024.12.14\n + cffi==1.17.1\n + chardet==5.2.0\n + charset-normalizer==3.4.1\n + contourpy==1.3.1\n + cryptography==44.0.0\n + cycler==0.12.1\n + et-xmlfile==2.0.0\n + fonttools==4.55.3\n + graphviz==0.20.3\n + h11==0.14.0\n + httpcore==1.0.7\n + httpx==0.28.1\n + idna==3.10\n + importlib-metadata==8.5.0\n + jaraco-classes==3.4.0\n + jaraco-context==6.0.1\n + jaraco-functools==4.1.0\n + jeepney==0.8.0\n + joblib==1.4.2\n + keyring==25.6.0\n + keyrings-alt==5.0.2\n + kiwisolver==1.4.8\n + matplotlib==3.10.0\n + more-itertools==10.5.0\n + networkx==3.4.2\n - numpy==2.2.1\n + numpy==1.26.4\n + openpyxl==3.1.5\n + opentsne==1.0.2\n + pandas==2.2.3\n + pillow==11.1.0\n + pip==24.3.1\n + plotly==5.24.1\n + pycparser==2.22\n + pyparsing==3.2.1\n + python-dateutil==2.9.0.post0\n + python-louvain==0.16\n + pytz==2024.2\n + pyyaml==6.0.2\n + requests==2.32.3\n + scikit-learn==1.6.0\n + scipy==1.15.0\n + secretstorage==3.3.3\n + serverfiles==0.3.1\n + six==1.17.0\n + sniffio==1.3.1\n + tenacity==9.0.0\n + threadpoolctl==3.5.0\n + typing-extensions==4.12.2\n + tzdata==2024.2\n + urllib3==2.3.0\n + xgboost==2.0.3\n + xlrd==2.0.1\n + xlsxwriter==3.2.0\n + zipp==3.21.0\nResolved 58 packages in 22ms\nInstalled 39 packages in 76ms\n + anyqt==0.2.0\n + asttokens==3.0.0\n + attrs==24.3.0\n + cattrs==24.1.2\n + comm==0.2.2\n + commonmark==0.9.1\n + debugpy==1.8.11\n + decorator==5.1.1\n + dictdiffer==0.9.0\n + docutils==0.21.2\n + executing==2.1.0\n + ipykernel==6.29.5\n + ipython==8.31.0\n + jedi==0.19.2\n + jupyter-client==8.6.3\n + jupyter-core==5.7.2\n + matplotlib-inline==0.1.7\n + nest-asyncio==1.6.0\n + orange-canvas-core==0.2.5\n + orange-widget-base==4.25.1\n + parso==0.8.4\n + pexpect==4.9.0\n + platformdirs==4.3.6\n + prompt-toolkit==3.0.48\n + psutil==6.1.1\n + ptyprocess==0.7.0\n + pure-eval==0.2.3\n + pygments==2.19.1\n + pyqtgraph==0.13.7\n + pyzmq==26.2.0\n + qasync==0.27.1\n + qtconsole==5.6.1\n + qtpy==2.4.2\n + requests-cache==1.2.1\n + stack-data==0.6.3\n + tornado==6.4.2\n + traitlets==5.14.3\n + url-normalize==1.4.3\n + wcwidth==0.2.13\nResolved 39 packages in 99ms\nInstalled 25 packages in 59ms\n + alabaster==1.0.0\n + astroid==3.3.8\n + babel==2.16.0\n + colorama==0.4.6\n + dill==0.3.9\n + imagesize==1.4.1\n + isort==5.13.2\n + jinja2==3.1.5\n + libcst==1.5.1\n + mando==0.7.1\n + markupsafe==3.0.2\n + mccabe==0.7.0\n + pylint==3.3.3\n + radon==6.0.1\n + recommonmark==0.7.1\n + snowballstemmer==2.2.0\n + sphinx==8.1.3\n + sphinxcontrib-applehelp==2.0.0\n + sphinxcontrib-devhelp==2.0.0\n + sphinxcontrib-htmlhelp==2.1.0\n + sphinxcontrib-jsmath==1.0.1\n + sphinxcontrib-qthelp==2.0.0\n + sphinxcontrib-serializinghtml==2.0.0\n + tomlkit==0.13.2\n + trubar==0.3.4\nResolved 2 packages in 2ms\nInstalled 2 packages in 4ms\n + psycopg2==2.9.10\n + pymssql==2.3.2\nperformance hint: Orange/preprocess/_relieff.pyx:63:5: Exception check on 'calc_difference' will always require the GIL to be acquired.\nPossible solutions:\n\t1. Declare 'calc_difference' as 'noexcept' if you control the definition and you're sure you don't want the function to raise exceptions.\n\t2. Use an 'int' return type on 'calc_difference' to allow an error code to be returned.\nperformance hint: Orange/preprocess/_relieff.pyx:119:5: Exception check on 'k_nearest_reg' will always require the GIL to be acquired.\nPossible solutions:\n\t1. Declare 'k_nearest_reg' as 'noexcept' if you control the definition and you're sure you don't want the function to raise exceptions.\n\t2. Use an 'int' return type on 'k_nearest_reg' to allow an error code to be returned.\nperformance hint: Orange/preprocess/_relieff.pyx:183:5: Exception check on 'k_nearest_per_class' will always require the GIL to be acquired.\nPossible solutions:\n\t1. Declare 'k_nearest_per_class' as 'noexcept' if you control the definition and you're sure you don't want the function to raise exceptions.\n\t2. Use an 'int' return type on 'k_nearest_per_class' to allow an error code to be returned.\nperformance hint: Orange/preprocess/_relieff.pyx:161:23: Exception check after calling 'calc_difference' will always require the GIL to be acquired.\nPossible solutions:\n\t1. Declare 'calc_difference' as 'noexcept' if you control the definition and you're sure you don't want the function to raise exceptions.\n\t2. Use an 'int' return type on 'calc_difference' to allow an error code to be returned.\nperformance hint: Orange/preprocess/_relieff.pyx:177:23: Exception check after calling 'calc_difference' will always require the GIL to be acquired.\nPossible solutions:\n\t1. Declare 'calc_difference' as 'noexcept' if you control the definition and you're sure you don't want the function to raise exceptions.\n\t2. Use an 'int' return type on 'calc_difference' to allow an error code to be returned.\nperformance hint: Orange/preprocess/_relieff.pyx:201:23: Exception check after calling 'calc_difference' will always require the GIL to be acquired.\nPossible solutions:\n\t1. Declare 'calc_difference' as 'noexcept' if you control the definition and you're sure you don't want the function to raise exceptions.\n\t2. Use an 'int' return type on 'calc_difference' to allow an error code to be returned.\nperformance hint: Orange/preprocess/_relieff.pyx:217:27: Exception check after calling 'calc_difference' will always require the GIL to be acquired.\nPossible solutions:\n\t1. Declare 'calc_difference' as 'noexcept' if you control the definition and you're sure you don't want the function to raise exceptions.\n\t2. Use an 'int' return type on 'calc_difference' to allow an error code to be returned.\nperformance hint: Orange/preprocess/_relieff.pyx:250:25: Exception check after calling 'k_nearest_reg' will always require the GIL to be acquired.\nPossible solutions:\n\t1. Declare 'k_nearest_reg' as 'noexcept' if you control the definition and you're sure you don't want the function to raise exceptions.\n\t2. Use an 'int' return type on 'k_nearest_reg' to allow an error code to be returned.\nperformance hint: Orange/preprocess/_relieff.pyx:287:31: Exception check after calling 'k_nearest_per_class' will always require the GIL to be acquired.\nPossible solutions:\n\t1. Declare 'k_nearest_per_class' as 'noexcept' if you control the definition and you're sure you don't want the function to raise exceptions.\n\t2. Use an 'int' return type on 'k_nearest_per_class' to allow an error code to be returned.\n/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.10/site-packages/setuptools/_distutils/dist.py:270: UserWarning: Unknown distribution option: 'test_suite'\n  warnings.warn(msg)\nIn file included from /home/gcpuser/.local/share/uv/python/cpython-3.10.16-linux-x86_64-gnu/include/python3.10/Python.h:8,\n                 from Orange/classification/_simple_tree.c:781:\n/home/gcpuser/.local/share/uv/python/cpython-3.10.16-linux-x86_64-gnu/include/python3.10/pyconfig.h:1621: warning: \"_GNU_SOURCE\" redefined\n 1621 | #define _GNU_SOURCE 1\n      | \nOrange/classification/_simple_tree.c:2: note: this is the location of the previous definition\n    2 | #define _GNU_SOURCE\n      | \nIn file included from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.10/site-packages/numpy/core/include/numpy/ndarraytypes.h:1929,\n                 from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.10/site-packages/numpy/core/include/numpy/ndarrayobject.h:12,\n                 from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.10/site-packages/numpy/core/include/numpy/arrayobject.h:5,\n                 from Orange/classification/_tree_scorers.c:1253:\n/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.10/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:20278:26: warning: comparison of integer expressions of different signedness: ‘unsigned int’ and ‘int’ [-Wsign-compare]\n20278 |   __pyx_t_7 = (__pyx_v_N <= __pyx_v_min_leaf);\n      |                          ^~\nOrange/classification/_tree_scorers.c: In function ‘__pyx_pf_6Orange_14classification_13_tree_scorers_6find_threshold_MSE’:\nOrange/classification/_tree_scorers.c:21847:26: warning: comparison of integer expressions of different signedness: ‘unsigned int’ and ‘int’ [-Wsign-compare]\n21847 |   __pyx_t_1 = (__pyx_v_N <= __pyx_v_min_leaf);\n      |                          ^~\nOrange/classification/_tree_scorers.c: In function ‘__pyx_pf_6Orange_14classification_13_tree_scorers_8find_binarization_MSE’:\nOrange/classification/_tree_scorers.c:22657:43: warning: comparison of integer expressions of different signedness: ‘unsigned int’ and ‘int’ [-Wsign-compare]\n22657 |           for (__pyx_t_19 = 0; __pyx_t_19 < __pyx_t_18; __pyx_t_19+=1) {\n      |                                           ^\nOrange/classification/_tree_scorers.c:22778:38: warning: comparison of integer expressions of different signedness: ‘unsigned int’ and ‘int’ [-Wsign-compare]\n22778 |           __pyx_t_20 = (__pyx_v_left >= __pyx_v_min_leaf);\n      |                                      ^~\nOrange/classification/_tree_scorers.c:22784:52: warning: comparison of integer expressions of different signedness: ‘unsigned int’ and ‘int’ [-Wsign-compare]\n22784 |           __pyx_t_20 = ((__pyx_v_N - __pyx_v_left) >= __pyx_v_min_leaf);\n      |                                                    ^~\nOrange/classification/_tree_scorers.c: In function ‘__pyx_pf_6Orange_14classification_13_tree_scorers_14compute_predictions_csr’:\nOrange/classification/_tree_scorers.c:24434:41: warning: comparison of integer expressions of different signedness: ‘unsigned int’ and ‘int’ [-Wsign-compare]\n24434 |         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:25156:41: warning: comparison of integer expressions of different signedness: ‘unsigned int’ and ‘int’ [-Wsign-compare]\n25156 |         for (__pyx_t_12 = 0; __pyx_t_12 < __pyx_t_11; __pyx_t_12+=1) {\n      |                                         ^\nOrange/classification/_tree_scorers.c:25216:149: warning: comparison of integer expressions of different signedness: ‘__pyx_t_5numpy_int32_t’ {aka ‘int’} and ‘unsigned int’ [-Wsign-compare]\n25216 |               __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);\n      |                                                                                                                                                     ^~\nIn file included from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.10/site-packages/numpy/core/include/numpy/ndarraytypes.h:1929,\n                 from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.10/site-packages/numpy/core/include/numpy/ndarrayobject.h:12,\n                 from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.10/site-packages/numpy/core/include/numpy/arrayobject.h:5,\n                 from Orange/data/_contingency.c:1253:\n/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.10/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:5091:43: warning: ‘__pyx_v_decs’ may be used uninitialized in this function [-Wmaybe-uninitialized]\n 5091 |           __pyx_v_value = ((__pyx_v_value * __pyx_v_decs) + (__pyx_v_c & 0xf));\n      |                            ~~~~~~~~~~~~~~~^~~~~~~~~~~~~~~\nOrange/data/_io.c:5261:23: warning: ‘__pyx_v_value’ may be used uninitialized in this function [-Wmaybe-uninitialized]\n 5261 |           __pyx_t_5 = PyFloat_FromDouble(__pyx_v_value); if (unlikely(!__pyx_t_5)) __PYX_ERR(0, 223, __pyx_L5_error)\n      |                       ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\nOrange/data/_io.c:1148:40: warning: ‘__pyx_v_attr_index’ may be used uninitialized in this function [-Wmaybe-uninitialized]\n 1148 |   #define PyInt_FromLong               PyLong_FromLong\n      |                                        ^~~~~~~~~~~~~~~\nOrange/data/_io.c:2863:7: note: ‘__pyx_v_attr_index’ was declared here\n 2863 |   int __pyx_v_attr_index;\n      |       ^~~~~~~~~~~~~~~~~~\nOrange/data/_io.c:4691:30: warning: ‘__pyx_v_atomp’ may be used uninitialized in this function [-Wmaybe-uninitialized]\n 4691 |           (__pyx_v_atomp[0]) = 0;\n      |           ~~~~~~~~~~~~~~~~~~~^~~\nIn file included from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.10/site-packages/numpy/core/include/numpy/ndarraytypes.h:1929,\n                 from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.10/site-packages/numpy/core/include/numpy/ndarrayobject.h:12,\n                 from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.10/site-packages/numpy/core/include/numpy/arrayobject.h:5,\n                 from Orange/data/_valuecount.c:1253:\n/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.10/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_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.10/site-packages/numpy/core/include/numpy/ndarraytypes.h:1929,\n                 from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.10/site-packages/numpy/core/include/numpy/ndarrayobject.h:12,\n                 from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.10/site-packages/numpy/core/include/numpy/arrayobject.h:5,\n                 from Orange/distance/_distance.c:1253:\n/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.10/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_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.10/site-packages/numpy/core/include/numpy/ndarraytypes.h:1929,\n                 from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.10/site-packages/numpy/core/include/numpy/ndarrayobject.h:12,\n                 from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.10/site-packages/numpy/core/include/numpy/arrayobject.h:5,\n                 from Orange/preprocess/_discretize.c:1252:\n/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.10/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_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.10/site-packages/numpy/core/include/numpy/ndarraytypes.h:1929,\n                 from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.10/site-packages/numpy/core/include/numpy/ndarrayobject.h:12,\n                 from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.10/site-packages/numpy/core/include/numpy/arrayobject.h:5,\n                 from Orange/preprocess/_relieff.cpp:1266:\n/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.10/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_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.10/site-packages/numpy/core/include/numpy/ndarraytypes.h:1929,\n                 from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.10/site-packages/numpy/core/include/numpy/ndarrayobject.h:12,\n                 from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.10/site-packages/numpy/core/include/numpy/arrayobject.h:5,\n                 from Orange/projection/_som.c:1253:\n/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.10/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:20732:7: warning: ‘__pyx_v_win_y’ may be used uninitialized in this function [-Wmaybe-uninitialized]\n20732 |   int __pyx_v_win_y;\n      |       ^~~~~~~~~~~~~\nOrange/projection/_som.c:20731:7: warning: ‘__pyx_v_win_x’ may be used uninitialized in this function [-Wmaybe-uninitialized]\n20731 |   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:22819:7: warning: ‘__pyx_v_win_y’ may be used uninitialized in this function [-Wmaybe-uninitialized]\n22819 |   int __pyx_v_win_y;\n      |       ^~~~~~~~~~~~~\nOrange/projection/_som.c:22818:7: warning: ‘__pyx_v_win_x’ may be used uninitialized in this function [-Wmaybe-uninitialized]\n22818 |   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:21996:7: warning: ‘__pyx_v_win_y’ may be used uninitialized in this function [-Wmaybe-uninitialized]\n21996 |   int __pyx_v_win_y;\n      |       ^~~~~~~~~~~~~\nOrange/projection/_som.c:21995:7: warning: ‘__pyx_v_win_x’ may be used uninitialized in this function [-Wmaybe-uninitialized]\n21995 |   int __pyx_v_win_x;\n      |       ^~~~~~~~~~~~~\nOrange/projection/_som.c: In function ‘__pyx_pw_6Orange_10projection_4_som_1get_winners’:\nOrange/projection/_som.c:19911:223: warning: ‘__pyx_v_win_y’ may be used uninitialized in this function [-Wmaybe-uninitialized]\n19911 |           *__Pyx_BufPtrStrided2d(__pyx_t_5numpy_int16_t *, __pyx_pybuffernd_winners.rcbuffer->pybuffer.buf, __pyx_t_23, __pyx_pybuffernd_winners.diminfo[0].strides, __pyx_t_24, __pyx_pybuffernd_winners.diminfo[1].strides) = __pyx_v_win_y;\n      |           ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^~~~~~~~~~~~~~~\nOrange/projection/_som.c:19558:7: note: ‘__pyx_v_win_y’ was declared here\n19558 |   int __pyx_v_win_y;\n      |       ^~~~~~~~~~~~~\nOrange/projection/_som.c:19900:223: warning: ‘__pyx_v_win_x’ may be used uninitialized in this function [-Wmaybe-uninitialized]\n19900 |           *__Pyx_BufPtrStrided2d(__pyx_t_5numpy_int16_t *, __pyx_pybuffernd_winners.rcbuffer->pybuffer.buf, __pyx_t_24, __pyx_pybuffernd_winners.diminfo[0].strides, __pyx_t_23, __pyx_pybuffernd_winners.diminfo[1].strides) = __pyx_v_win_x;\n      |           ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^~~~~~~~~~~~~~~\nOrange/projection/_som.c:19557:7: note: ‘__pyx_v_win_x’ was declared here\n19557 |   int __pyx_v_win_x;\n      |       ^~~~~~~~~~~~~\nOrange/projection/_som.c: In function ‘__pyx_pw_6Orange_10projection_4_som_3update’:\nOrange/projection/_som.c:20161:7: warning: ‘__pyx_v_win_y’ may be used uninitialized in this function [-Wmaybe-uninitialized]\n20161 |   int __pyx_v_win_y;\n      |       ^~~~~~~~~~~~~\nOrange/projection/_som.c: In function ‘__pyx_pw_6Orange_10projection_4_som_7get_winners_sparse’:\nOrange/projection/_som.c:21717:223: warning: ‘__pyx_v_win_y’ may be used uninitialized in this function [-Wmaybe-uninitialized]\n21717 |           *__Pyx_BufPtrStrided2d(__pyx_t_5numpy_int16_t *, __pyx_pybuffernd_winners.rcbuffer->pybuffer.buf, __pyx_t_27, __pyx_pybuffernd_winners.diminfo[0].strides, __pyx_t_28, __pyx_pybuffernd_winners.diminfo[1].strides) = __pyx_v_win_y;\n      |           ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^~~~~~~~~~~~~~~\nOrange/projection/_som.c:21234:7: note: ‘__pyx_v_win_y’ was declared here\n21234 |   int __pyx_v_win_y;\n      |       ^~~~~~~~~~~~~\nOrange/projection/_som.c:21706:223: warning: ‘__pyx_v_win_x’ may be used uninitialized in this function [-Wmaybe-uninitialized]\n21706 |           *__Pyx_BufPtrStrided2d(__pyx_t_5numpy_int16_t *, __pyx_pybuffernd_winners.rcbuffer->pybuffer.buf, __pyx_t_28, __pyx_pybuffernd_winners.diminfo[0].strides, __pyx_t_27, __pyx_pybuffernd_winners.diminfo[1].strides) = __pyx_v_win_x;\n      |           ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^~~~~~~~~~~~~~~\nOrange/projection/_som.c:21233:7: note: ‘__pyx_v_win_x’ was declared here\n21233 |   int __pyx_v_win_x;\n      |       ^~~~~~~~~~~~~\n/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.10/site-packages/setuptools/_distutils/dist.py:270: UserWarning: Unknown distribution option: 'test_suite'\n  warnings.warn(msg)\n/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.10/site-packages/setuptools/command/develop.py:41: EasyInstallDeprecationWarning: easy_install command is deprecated.\n!!\n\n        ********************************************************************************\n        Please avoid running ``setup.py`` and ``easy_install``.\n        Instead, use pypa/build, pypa/installer or other\n        standards-based tools.\n\n        See https://github.com/pypa/setuptools/issues/917 for details.\n        ********************************************************************************\n\n!!\n  easy_install.initialize_options(self)\n/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074/.venv/lib/python3.10/site-packages/setuptools/_distutils/cmd.py:79: SetuptoolsDeprecationWarning: setup.py install is deprecated.\n!!\n\n        ********************************************************************************\n        Please avoid running ``setup.py`` directly.\n        Instead, use pypa/build, pypa/installer or other\n        standards-based tools.\n\n        See https://blog.ganssle.io/articles/2021/10/setup-py-deprecated.html for details.\n        ********************************************************************************\n\n!!\n  self.initialize_options()\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'\nResolved 102 packages in 2.42s\nPrepared 1 package in 46.03s\nUninstalled 1 package in 0.32ms\nInstalled 1 package in 1ms\n - orange3==3.38.0.dev0+2a6f878\n + orange3==3.38.0.dev0+2a6f878 (from file:///home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074)\n",
    "new_commit_res_code": 0,
    "new_commit_res_stdout": "============================= test session starts ==============================\nplatform linux -- Python 3.10.16, pytest-8.3.4, pluggy-1.5.0\nrootdir: /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074\nconfigfile: pyproject.toml\nplugins: anyio-4.8.0, typeguard-4.3.0\ncollected 19 items\n\nr2e_tests/test_1.py sss................                                  [100%]\n\n==================================== PASSES ====================================\n______________ TestOWHierarchicalClustering.test_all_zero_inputs _______________\n----------------------------- Captured stderr call -----------------------------\nQGraphicsScene::addItem: item has already been added to this scene\n_______ TestOWHierarchicalClustering.test_annotation_settings_retrieval ________\n----------------------------- Captured stderr call -----------------------------\nQGraphicsScene::addItem: item has already been added to this scene\n______________ TestOWHierarchicalClustering.test_column_distances ______________\n----------------------------- Captured stderr call -----------------------------\nQGraphicsScene::addItem: item has already been added to this scene\n_____________ TestOWHierarchicalClustering.test_domain_loses_class _____________\n----------------------------- Captured stderr call -----------------------------\nQGraphicsScene::addItem: item has already been added to this scene\n________________ TestOWHierarchicalClustering.test_empty_matrix ________________\n----------------------------- Captured stderr call -----------------------------\nQGraphicsScene::addItem: item has already been added to this scene\n________________ TestOWHierarchicalClustering.test_image_export ________________\n----------------------------- Captured stderr call -----------------------------\nQGraphicsScene::addItem: item has already been added to this scene\n_____________ TestOWHierarchicalClustering.test_infinite_distances _____________\n----------------------------- Captured stderr call -----------------------------\nQGraphicsScene::addItem: item has already been added to this scene\n____________ TestOWHierarchicalClustering.test_many_values_warning _____________\n----------------------------- Captured stderr call -----------------------------\nQGraphicsScene::addItem: item has already been added to this scene\n________________ TestOWHierarchicalClustering.test_minimum_size ________________\n----------------------------- Captured stderr call -----------------------------\nQGraphicsScene::addItem: item has already been added to this scene\n_______________ TestOWHierarchicalClustering.test_msg_base_class _______________\n----------------------------- Captured stderr call -----------------------------\nQGraphicsScene::addItem: item has already been added to this scene\n_______________ TestOWHierarchicalClustering.test_not_symmetric ________________\n----------------------------- Captured stderr call -----------------------------\nQGraphicsScene::addItem: item has already been added to this scene\n______________ TestOWHierarchicalClustering.test_output_cut_ratio ______________\n----------------------------- Captured stderr call -----------------------------\nQGraphicsScene::addItem: item has already been added to this scene\n__________________ TestOWHierarchicalClustering.test_outputs ___________________\n----------------------------- Captured stderr call -----------------------------\nQGraphicsScene::addItem: item has already been added to this scene\n_______________ TestOWHierarchicalClustering.test_restore_state ________________\n----------------------------- Captured stderr call -----------------------------\nQGraphicsScene::addItem: item has already been added to this scene\nQGraphicsScene::addItem: item has already been added to this scene\n______________ TestOWHierarchicalClustering.test_retain_selection ______________\n----------------------------- Captured stderr call -----------------------------\nQGraphicsScene::addItem: item has already been added to this scene\n____________ TestOWHierarchicalClustering.test_selection_box_output ____________\n----------------------------- Captured stderr call -----------------------------\nQGraphicsScene::addItem: item has already been added to this scene\n=========================== short test summary info ============================\nPASSED r2e_tests/test_1.py::TestOWHierarchicalClustering::test_all_zero_inputs\nPASSED r2e_tests/test_1.py::TestOWHierarchicalClustering::test_annotation_settings_retrieval\nPASSED r2e_tests/test_1.py::TestOWHierarchicalClustering::test_column_distances\nPASSED r2e_tests/test_1.py::TestOWHierarchicalClustering::test_domain_loses_class\nPASSED r2e_tests/test_1.py::TestOWHierarchicalClustering::test_empty_matrix\nPASSED r2e_tests/test_1.py::TestOWHierarchicalClustering::test_image_export\nPASSED r2e_tests/test_1.py::TestOWHierarchicalClustering::test_infinite_distances\nPASSED r2e_tests/test_1.py::TestOWHierarchicalClustering::test_many_values_warning\nPASSED r2e_tests/test_1.py::TestOWHierarchicalClustering::test_minimum_size\nPASSED r2e_tests/test_1.py::TestOWHierarchicalClustering::test_msg_base_class\nPASSED r2e_tests/test_1.py::TestOWHierarchicalClustering::test_not_symmetric\nPASSED r2e_tests/test_1.py::TestOWHierarchicalClustering::test_output_cut_ratio\nPASSED r2e_tests/test_1.py::TestOWHierarchicalClustering::test_outputs\nPASSED r2e_tests/test_1.py::TestOWHierarchicalClustering::test_restore_state\nPASSED r2e_tests/test_1.py::TestOWHierarchicalClustering::test_retain_selection\nPASSED r2e_tests/test_1.py::TestOWHierarchicalClustering::test_selection_box_output\nSKIPPED [1] .venv/lib/python3.10/site-packages/orangewidget/tests/base.py:248: image exporting not tested as .widget was not set\nSKIPPED [1] .venv/lib/python3.10/site-packages/orangewidget/tests/base.py:242: minimum size not tested as .widget was not set\nSKIPPED [1] .venv/lib/python3.10/site-packages/orangewidget/tests/base.py:254: msg base class not tested as .widget was not set\n======================== 16 passed, 3 skipped in 6.33s =========================\nWidgets left: 0    Max widgets: 1939 \n\n",
    "new_commit_res_stderr": "",
    "old_commit_res_code": 1,
    "old_commit_res_stdout": "============================= test session starts ==============================\nplatform linux -- Python 3.10.16, pytest-8.3.4, pluggy-1.5.0\nrootdir: /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_2a6f878a820b39fb58a57d023999d8246a982074\nconfigfile: pyproject.toml\nplugins: anyio-4.8.0, typeguard-4.3.0\ncollected 19 items\n\nr2e_tests/test_1.py sss.......F........                                  [100%]\n\n=================================== FAILURES ===================================\n____________ TestOWHierarchicalClustering.test_many_values_warning _____________\n\nself = <r2e_tests.test_1.TestOWHierarchicalClustering testMethod=test_many_values_warning>\n\n    def test_many_values_warning(self):\n        w = self.widget\n    \n        self.send_signal(self.widget.Inputs.distances, self.distances)\n        w.top_n = 21\n        w.selection_box.buttons[2].click()\n>       self.assertTrue(w.Warning.many_clusters.is_shown())\nE       AttributeError: 'Warning' object has no attribute 'many_clusters'\n\nr2e_tests/test_1.py:217: AttributeError\n----------------------------- Captured stderr call -----------------------------\nQGraphicsScene::addItem: item has already been added to this scene\n==================================== PASSES ====================================\n______________ TestOWHierarchicalClustering.test_all_zero_inputs _______________\n----------------------------- Captured stderr call -----------------------------\nQGraphicsScene::addItem: item has already been added to this scene\n_______ TestOWHierarchicalClustering.test_annotation_settings_retrieval ________\n----------------------------- Captured stderr call -----------------------------\nQGraphicsScene::addItem: item has already been added to this scene\n______________ TestOWHierarchicalClustering.test_column_distances ______________\n----------------------------- Captured stderr call -----------------------------\nQGraphicsScene::addItem: item has already been added to this scene\n_____________ TestOWHierarchicalClustering.test_domain_loses_class _____________\n----------------------------- Captured stderr call -----------------------------\nQGraphicsScene::addItem: item has already been added to this scene\n________________ TestOWHierarchicalClustering.test_empty_matrix ________________\n----------------------------- Captured stderr call -----------------------------\nQGraphicsScene::addItem: item has already been added to this scene\n________________ TestOWHierarchicalClustering.test_image_export ________________\n----------------------------- Captured stderr call -----------------------------\nQGraphicsScene::addItem: item has already been added to this scene\n_____________ TestOWHierarchicalClustering.test_infinite_distances _____________\n----------------------------- Captured stderr call -----------------------------\nQGraphicsScene::addItem: item has already been added to this scene\n________________ TestOWHierarchicalClustering.test_minimum_size ________________\n----------------------------- Captured stderr call -----------------------------\nQGraphicsScene::addItem: item has already been added to this scene\n_______________ TestOWHierarchicalClustering.test_msg_base_class _______________\n----------------------------- Captured stderr call -----------------------------\nQGraphicsScene::addItem: item has already been added to this scene\n_______________ TestOWHierarchicalClustering.test_not_symmetric ________________\n----------------------------- Captured stderr call -----------------------------\nQGraphicsScene::addItem: item has already been added to this scene\n______________ TestOWHierarchicalClustering.test_output_cut_ratio ______________\n----------------------------- Captured stderr call -----------------------------\nQGraphicsScene::addItem: item has already been added to this scene\n__________________ TestOWHierarchicalClustering.test_outputs ___________________\n----------------------------- Captured stderr call -----------------------------\nQGraphicsScene::addItem: item has already been added to this scene\n_______________ TestOWHierarchicalClustering.test_restore_state ________________\n----------------------------- Captured stderr call -----------------------------\nQGraphicsScene::addItem: item has already been added to this scene\nQGraphicsScene::addItem: item has already been added to this scene\n______________ TestOWHierarchicalClustering.test_retain_selection ______________\n----------------------------- Captured stderr call -----------------------------\nQGraphicsScene::addItem: item has already been added to this scene\n____________ TestOWHierarchicalClustering.test_selection_box_output ____________\n----------------------------- Captured stderr call -----------------------------\nQGraphicsScene::addItem: item has already been added to this scene\n=========================== short test summary info ============================\nPASSED r2e_tests/test_1.py::TestOWHierarchicalClustering::test_all_zero_inputs\nPASSED r2e_tests/test_1.py::TestOWHierarchicalClustering::test_annotation_settings_retrieval\nPASSED r2e_tests/test_1.py::TestOWHierarchicalClustering::test_column_distances\nPASSED r2e_tests/test_1.py::TestOWHierarchicalClustering::test_domain_loses_class\nPASSED r2e_tests/test_1.py::TestOWHierarchicalClustering::test_empty_matrix\nPASSED r2e_tests/test_1.py::TestOWHierarchicalClustering::test_image_export\nPASSED r2e_tests/test_1.py::TestOWHierarchicalClustering::test_infinite_distances\nPASSED r2e_tests/test_1.py::TestOWHierarchicalClustering::test_minimum_size\nPASSED r2e_tests/test_1.py::TestOWHierarchicalClustering::test_msg_base_class\nPASSED r2e_tests/test_1.py::TestOWHierarchicalClustering::test_not_symmetric\nPASSED r2e_tests/test_1.py::TestOWHierarchicalClustering::test_output_cut_ratio\nPASSED r2e_tests/test_1.py::TestOWHierarchicalClustering::test_outputs\nPASSED r2e_tests/test_1.py::TestOWHierarchicalClustering::test_restore_state\nPASSED r2e_tests/test_1.py::TestOWHierarchicalClustering::test_retain_selection\nPASSED r2e_tests/test_1.py::TestOWHierarchicalClustering::test_selection_box_output\nSKIPPED [1] .venv/lib/python3.10/site-packages/orangewidget/tests/base.py:248: image exporting not tested as .widget was not set\nSKIPPED [1] .venv/lib/python3.10/site-packages/orangewidget/tests/base.py:242: minimum size not tested as .widget was not set\nSKIPPED [1] .venv/lib/python3.10/site-packages/orangewidget/tests/base.py:254: msg base class not tested as .widget was not set\nFAILED r2e_tests/test_1.py::TestOWHierarchicalClustering::test_many_values_warning\n=================== 1 failed, 15 passed, 3 skipped in 4.56s ====================\nWidgets left: 0    Max widgets: 1939 \n\n",
    "old_commit_res_stderr": ""
}
Orange/widgets/unsupervised/owhierarchicalclustering.py Orange/widgets/unsupervised/tests/test_owhierarchicalclustering.py
We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
[ISSUE]
**Title:** AttributeError when selecting more than 20 clusters in Hierarchical Clustering widget

**Description:**
When configuring the Hierarchical Clustering widget to select more than 20 clusters, an AttributeError occurs because the `Warning` object does not have a `many_clusters` attribute. This issue prevents the widget from displaying the intended warning message about potential performance degradation when handling a large number of clusters.

**Example Code:**
```python
widget = OWHierarchicalClustering()
widget.top_n = 21
widget.selection_box.buttons[2].click()
# Expected: Warning.many_clusters is shown
# Actual: AttributeError: 'Warning' object has no attribute 'many_clusters'
```

**Expected Behavior:**
Setting the `top_n` parameter to a value greater than 20 should display the `many_clusters` warning, informing users that selecting a large number of clusters may degrade the performance of downstream widgets.

**Actual Behavior:**
An AttributeError is raised stating that the `'Warning' object has no attribute 'many_clusters'`, which prevents the warning from being displayed when more than 20 clusters are selected.
[/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/unsupervised/owhierarchicalclustering.py
from itertools import chain
from contextlib import contextmanager

import typing
from typing import Any, List, Tuple, Dict, Optional, Set, Union

import numpy as np

from AnyQt.QtWidgets import (
    QGraphicsWidget, QGraphicsScene, QGridLayout, QSizePolicy,
    QAction, QComboBox, QGraphicsGridLayout, QGraphicsSceneMouseEvent, QLabel
)
from AnyQt.QtGui import (QPen, QFont, QKeySequence, QPainterPath, QColor,
    QFontMetrics)
from AnyQt.QtCore import (
    Qt, QObject, QSize, QPointF, QRectF, QLineF, QEvent, QModelIndex
)
from AnyQt.QtCore import pyqtSignal as Signal, pyqtSlot as Slot

from Orange.widgets.utils.localization import pl
from orangewidget.utils.itemmodels import PyListModel
from orangewidget.utils.signals import LazyValue

import Orange.data
from Orange.data.domain import filter_visible
from Orange.data import Domain, DiscreteVariable, ContinuousVariable, \
    StringVariable, Table
import Orange.misc
from Orange.clustering.hierarchical import \
    postorder, preorder, Tree, tree_from_linkage, dist_matrix_linkage, \
    leaves, prune, top_clusters
from Orange.data.util import get_unique_names

from Orange.widgets import widget, gui, settings
from Orange.widgets.utils import itemmodels, combobox
from Orange.widgets.utils.annotated_data import (lazy_annotated_table,
                                                 ANNOTATED_DATA_SIGNAL_NAME,
                                                 domain_with_annotation_column,
                                                 add_columns,
                                                 create_annotated_table)
from Orange.widgets.utils.widgetpreview import WidgetPreview
from Orange.widgets.visualize.utils.plotutils import AxisItem
from Orange.widgets.widget import Input, Output, Msg

from Orange.widgets.utils.stickygraphicsview import StickyGraphicsView
from Orange.widgets.utils.graphicsview import GraphicsWidgetView
from Orange.widgets.utils.graphicstextlist import TextListView
from Orange.widgets.utils.dendrogram import DendrogramWidget

__all__ = ["OWHierarchicalClustering"]


LINKAGE = ["Single", "Average", "Weighted", "Complete", "Ward"]
LINKAGE_ARGS = ["single", "average", "weighted", "complete", "ward"]
DEFAULT_LINKAGE = "Ward"


def make_pen(brush=Qt.black, width=1, style=Qt.SolidLine,
             cap_style=Qt.SquareCap, join_style=Qt.BevelJoin,
             cosmetic=False):
    pen = QPen(brush)
    pen.setWidth(width)
    pen.setStyle(style)
    pen.setCapStyle(cap_style)
    pen.setJoinStyle(join_style)
    pen.setCosmetic(cosmetic)
    return pen


@contextmanager
def blocked(obj):
    old = obj.signalsBlocked()
    obj.blockSignals(True)
    try:
        yield obj
    finally:
        obj.blockSignals(old)


class SaveStateSettingsHandler(settings.SettingsHandler):
    """
    A settings handler that delegates session data store/restore to the
    OWWidget instance.

    The OWWidget subclass must implement `save_state() -> Dict[str, Any]` and
    `set_restore_state(state: Dict[str, Any])` methods.
    """
    def initialize(self, instance, data=None):
        super().initialize(instance, data)
        if data is not None and "__session_state_data" in data:
            session_data = data["__session_state_data"]
            instance.set_restore_state(session_data)

    def pack_data(self, widget):
        # type: (widget.OWWidget) -> dict
        res = super().pack_data(widget)
        state = widget.save_state()
        if state:
            assert "__session_state_data" not in res
            res["__session_state_data"] = state
        return res


class _DomainContextHandler(settings.DomainContextHandler,
                            SaveStateSettingsHandler):
    pass


if typing.TYPE_CHECKING:
    #: Encoded selection state for persistent storage.
    #: This is a list of tuples of leaf indices in the selection and
    #: a (N, 3) linkage matrix for validation (the 4-th column from scipy
    #: is omitted).
    SelectionState = Tuple[List[Tuple[int]], List[Tuple[int, int, float]]]


class SelectedLabelsModel(PyListModel):
    def __init__(self):
        super().__init__([])
        self.subset = set()
        self.__font = QFont()
        self.__colors = None

    def rowCount(self, parent=QModelIndex()):
        count = super().rowCount()
        if self.__colors is not None:
            count = max(count, len(self.__colors))
        return count

    def _emit_data_changed(self):
        self.dataChanged.emit(self.index(0, 0), self.index(len(self) - 1, 0))

    def set_subset(self, subset):
        self.subset = set(subset)
        self._emit_data_changed()

    def set_colors(self, colors):
        self.__colors = colors
        self._emit_data_changed()

    def setFont(self, font):
        self.__font = font
        self._emit_data_changed()

    def data(self, index, role=Qt.DisplayRole):
        if role == Qt.FontRole:
            font = QFont(self.__font)
            font.setBold(index.row() in self.subset)
            return font
        if role == Qt.BackgroundRole:
            if self.__colors is not None:
                return self.__colors[index.row()]
            elif not any(self) and self.subset:  # no labels, no color, but subset
                return QColor(0, 0, 0)
        if role == Qt.UserRole and self.subset:
            return index.row() in self.subset

        return super().data(index, role)


class GraphicsView(GraphicsWidgetView, StickyGraphicsView):
    def minimumSizeHint(self) -> QSize:
        msh = super().minimumSizeHint()
        w = self.centralWidget()
        if w is not None:
            width = w.minimumWidth() + 4 + self.verticalScrollBar().width()
            msh.setWidth(max(int(width), msh.width()))
        return msh

    def eventFilter(self, recv: QObject, event: QEvent) -> bool:
        ret = super().eventFilter(recv, event)
        if event.type() == QEvent.LayoutRequest and recv is self.centralWidget():
            self.updateGeometry()
        return ret


class OWHierarchicalClustering(widget.OWWidget):
    name = "Hierarchical Clustering"
    description = "Display a dendrogram of a hierarchical clustering " \
                  "constructed from the input distance matrix."
    icon = "icons/HierarchicalClustering.svg"
    priority = 2100
    keywords = "hierarchical clustering"

    class Inputs:
        distances = Input("Distances", Orange.misc.DistMatrix)
        subset = Input("Data Subset", Orange.data.Table, explicit=True)

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

    settings_version = 2
    settingsHandler = _DomainContextHandler()

    #: Selected linkage
    linkage = settings.Setting(LINKAGE.index(DEFAULT_LINKAGE))
    #: Index of the selected annotation item (variable, ...)
    annotation = settings.ContextSetting("Enumeration")
    #: Out-of-context setting for the case when the "Name" option is available
    annotation_if_names = settings.Setting("Name")
    #: Out-of-context setting for the case with just "Enumeration" and "None"
    annotation_if_enumerate = settings.Setting("Enumeration")
    #: Selected tree pruning (none/max depth)
    pruning = settings.Setting(0)
    #: Maximum depth when max depth pruning is selected
    max_depth = settings.Setting(10)

    #: Selected cluster selection method (none, cut distance, top n)
    selection_method = settings.Setting(0)
    #: Cut height ratio wrt root height
    cut_ratio = settings.Setting(75.0)
    #: Number of top clusters to select
    top_n = settings.Setting(3)
    #: Dendrogram zoom factor
    zoom_factor = settings.Setting(0)
    #: Show labels only for subset (if present)
    label_only_subset = settings.Setting(False)
    #: Color for label decoration
    color_by: Union[DiscreteVariable, ContinuousVariable, None] = \
        settings.ContextSetting(None)

    autocommit = settings.Setting(True)

    graph_name = "scene"  # QGraphicsScene

    basic_annotations = [None, "Enumeration"]

    class Error(widget.OWWidget.Error):
        empty_matrix = Msg("Distance matrix is empty.")
        not_finite_distances = Msg("Some distances are infinite")
        not_symmetric = widget.Msg("Distance matrix is not symmetric.")

    class Warning(widget.OWWidget.Warning):
        subset_on_no_table = \
            Msg("Unused data subset: distances do not refer to data instances")
        subset_not_subset = \
            Msg("Some data from the subset does not appear in distance matrix")
        subset_wrong = \
            Msg("Subset data refers to a different table")
        pruning_disables_colors = \
            Msg("Pruned cluster doesn't show colors and indicate subset")

    #: Stored (manual) selection state (from a saved workflow) to restore.
    __pending_selection_restore = None  # type: Optional[SelectionState]

    def __init__(self):
        super().__init__()

        self.matrix = None
        self.items = None
        self.subset = None
        self.subset_rows = set()
        self.linkmatrix = None
        self.root = None
        self._displayed_root = None
        self.cutoff_height = 0.0

        spin_width = QFontMetrics(self.font()).horizontalAdvance("M" * 7)
        gui.comboBox(
            self.controlArea, self, "linkage", items=LINKAGE, box="Linkage",
            callback=self._invalidate_clustering)

        model = itemmodels.VariableListModel(placeholder="None")
        model[:] = self.basic_annotations

        grid = QGridLayout()
        gui.widgetBox(self.controlArea, "Annotations", orientation=grid)
        self.label_cb = cb = combobox.ComboBoxSearch(
            minimumContentsLength=14,
            sizeAdjustPolicy=QComboBox.AdjustToMinimumContentsLengthWithIcon
        )
        cb.setModel(model)
        cb.setCurrentIndex(cb.findData(self.annotation, Qt.EditRole))

        def on_annotation_activated():
            self.annotation = self.label_cb.currentData(Qt.EditRole)
            self._update_labels()
        cb.activated.connect(on_annotation_activated)

        def on_annotation_changed(value):
            self.label_cb.setCurrentIndex(
                self.label_cb.findData(value, Qt.EditRole))
        self.connect_control("annotation", on_annotation_changed)

        grid.addWidget(self.label_cb, 0, 0, 1, 2)

        cb = gui.checkBox(
            None, self, "label_only_subset", "Show labels only for subset",
            disabled=True,
            callback=self._update_labels, stateWhenDisabled=False)
        grid.addWidget(cb, 1, 0, 1, 2)

        model = itemmodels.DomainModel(
            valid_types=(DiscreteVariable, ContinuousVariable),
            placeholder="None")
        cb = gui.comboBox(
            None, self, "color_by", orientation=Qt.Horizontal,
            model=model, callback=self._update_labels,
            sizePolicy=QSizePolicy(QSizePolicy.MinimumExpanding,
                                   QSizePolicy.Fixed))
        self.color_by_label = QLabel("Color by:")
        grid.addWidget(self.color_by_label, 2, 0)
        grid.addWidget(cb, 2, 1)

        box = gui.radioButtons(
            self.controlArea, self, "pruning", box="Pruning",
            callback=self._invalidate_pruning)
        grid = QGridLayout()
        box.layout().addLayout(grid)
        grid.addWidget(
            gui.appendRadioButton(box, "None", addToLayout=False),
            0, 0
        )
        self.max_depth_spin = gui.spin(
            box, self, "max_depth", minv=1, maxv=100,
            controlWidth=spin_width, alignment=Qt.AlignRight,
            callback=self._max_depth_changed,
            keyboardTracking=False, addToLayout=False
        )
        self.max_depth_spin.lineEdit().returnPressed.connect(
            self._max_depth_return)

        grid.addWidget(
            gui.appendRadioButton(box, "Max depth:", addToLayout=False),
            1, 0)
        grid.addWidget(self.max_depth_spin, 1, 1)

        self.selection_box = gui.radioButtons(
            self.controlArea, self, "selection_method",
            box="Selection",
            callback=self._selection_method_changed)

        grid = QGridLayout()
        self.selection_box.layout().addLayout(grid)
        grid.addWidget(
            gui.appendRadioButton(
                self.selection_box, "Manual", addToLayout=False),
            0, 0
        )
        grid.addWidget(
            gui.appendRadioButton(
                self.selection_box, "Height ratio:", addToLayout=False),
            1, 0
        )
        self.cut_ratio_spin = gui.spin(
            self.selection_box, self, "cut_ratio", 0, 100, step=1e-1,
            controlWidth=spin_width, alignment = Qt.AlignRight,
            spinType=float, callback=self._cut_ratio_changed,
            addToLayout=False
        )
        self.cut_ratio_spin.setSuffix(" %")
        self.cut_ratio_spin.lineEdit().returnPressed.connect(
            self._cut_ratio_return)

        grid.addWidget(self.cut_ratio_spin, 1, 1)

        grid.addWidget(
            gui.appendRadioButton(
                self.selection_box, "Top N:", addToLayout=False),
            2, 0
        )
        self.top_n_spin = gui.spin(
            self.selection_box, self, "top_n", 1, 20,
            controlWidth=spin_width, alignment=Qt.AlignRight,
            callback=self._top_n_changed, addToLayout=False)
        self.top_n_spin.lineEdit().returnPressed.connect(self._top_n_return)
        grid.addWidget(self.top_n_spin, 2, 1)

        self.zoom_slider = gui.hSlider(
            self.controlArea, self, "zoom_factor", box="Zoom",
            minValue=-6, maxValue=3, step=1, ticks=True, createLabel=False,
            callback=self.__update_font_scale)

        zoom_in = QAction(
            "Zoom in", self, shortcut=QKeySequence.ZoomIn,
            triggered=self.__zoom_in
        )
        zoom_out = QAction(
            "Zoom out", self, shortcut=QKeySequence.ZoomOut,
            triggered=self.__zoom_out
        )
        zoom_reset = QAction(
            "Reset zoom", self,
            shortcut=QKeySequence(Qt.ControlModifier | Qt.Key_0),
            triggered=self.__zoom_reset
        )
        self.addActions([zoom_in, zoom_out, zoom_reset])

        self.controlArea.layout().addStretch()

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

        self.scene = QGraphicsScene(self)
        self.view = GraphicsView(
            self.scene,
            horizontalScrollBarPolicy=Qt.ScrollBarAlwaysOff,
            verticalScrollBarPolicy=Qt.ScrollBarAlwaysOn,
            alignment=Qt.AlignLeft | Qt.AlignVCenter,
            widgetResizable=True,
        )
        # Disable conflicting action shortcuts. We define our own.
        for a in self.view.viewActions():
            a.setEnabled(False)
        self.mainArea.layout().setSpacing(1)
        self.mainArea.layout().addWidget(self.view)

        def axis_view(orientation):
            ax = AxisItem(orientation=orientation, maxTickLength=7)
            ax.mousePressed.connect(self._activate_cut_line)
            ax.mouseMoved.connect(self._activate_cut_line)
            ax.mouseReleased.connect(self._activate_cut_line)
            ax.setRange(1.0, 0.0)
            return ax

        self.top_axis = axis_view("top")
        self.bottom_axis = axis_view("bottom")

        self._main_graphics = QGraphicsWidget(
            sizePolicy=QSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.Preferred)
        )
        scenelayout = QGraphicsGridLayout()
        scenelayout.setHorizontalSpacing(10)
        scenelayout.setVerticalSpacing(10)

        self._main_graphics.setLayout(scenelayout)
        self.scene.addItem(self._main_graphics)
        self.view.setCentralWidget(self._main_graphics)
        self.scene.addItem(self._main_graphics)

        self.dendrogram = DendrogramWidget(pen_width=2)
        self.dendrogram.setSizePolicy(QSizePolicy.MinimumExpanding,
                                      QSizePolicy.MinimumExpanding)
        self.dendrogram.selectionChanged.connect(self._invalidate_output)
        self.dendrogram.selectionEdited.connect(self._selection_edited)

        self.labels = TextListView()
        self.label_model = SelectedLabelsModel()
        self.labels.setModel(self.label_model)
        self.labels.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Preferred)
        self.labels.setAlignment(Qt.AlignLeft)
        self.labels.setMaximumWidth(200)

        scenelayout.addItem(self.top_axis, 0, 0,
                            alignment=Qt.AlignLeft | Qt.AlignVCenter)
        scenelayout.addItem(self.dendrogram, 1, 0,
                            alignment=Qt.AlignLeft | Qt.AlignVCenter)
        scenelayout.addItem(self.labels, 1, 1,
                            alignment=Qt.AlignLeft | Qt.AlignVCenter)
        scenelayout.addItem(self.bottom_axis, 2, 0,
                            alignment=Qt.AlignLeft | Qt.AlignVCenter)
        self.top_axis.setZValue(self.dendrogram.zValue() + 10)
        self.bottom_axis.setZValue(self.dendrogram.zValue() + 10)
        self.cut_line = SliderLine(self.top_axis,
                                   orientation=Qt.Horizontal)
        self.cut_line.valueChanged.connect(self._dendrogram_slider_changed)
        self.dendrogram.geometryChanged.connect(self._dendrogram_geom_changed)
        self._set_cut_line_visible(self.selection_method == 1)
        self.__update_font_scale()

    @Inputs.distances
    def set_distances(self, matrix):
        self.error()
        self.Error.clear()

        self.matrix = None
        self.Error.clear()
        if matrix is not None:
            if len(matrix) < 2:
                self.Error.empty_matrix()
            elif not matrix.is_symmetric():
                self.Error.not_symmetric()
            elif not np.all(np.isfinite(matrix)):
                self.Error.not_finite_distances()
            else:
                self.matrix = matrix

    @Inputs.subset
    def set_subset(self, subset):
        self.subset = subset
        self.controls.label_only_subset.setDisabled(subset is None)

    def handleNewSignals(self):
        if self.__pending_selection_restore is not None:
            selection_state = self.__pending_selection_restore
        else:
            # save the current selection to (possibly) restore later
            selection_state = self._save_selection()

        matrix = self.matrix
        if matrix is not None:
            self._set_items(matrix.row_items, matrix.axis)
        else:
            self._set_items(None)
        self._update()

        # Can now attempt to restore session state from a saved workflow.
        if self.root and selection_state is not None:
            self._restore_selection(selection_state)
            self.__pending_selection_restore = None

        self.Warning.clear()
        rows = set()
        if self.subset:
            subsetids = set(self.subset.ids)
            if not isinstance(self.items, Orange.data.Table) \
                    or not self.matrix.axis:
                self.Warning.subset_on_no_table()
            elif (dataids := set(self.items.ids)) and not subsetids & dataids:
                self.Warning.subset_wrong()
            elif not subsetids <= dataids:
                self.Warning.subset_not_subset()
            else:
                indices = [leaf.value.index for leaf in leaves(self.root)]
                rows = {
                    row for row, rowid in enumerate(self.items.ids[indices])
                    if rowid in subsetids
                }

        self.subset_rows = rows
        self._update_labels()
        self.commit.now()

    def _set_items(self, items, axis=1):
        self.closeContext()
        self.items = items
        model = self.label_cb.model()
        color_model = self.controls.color_by.model()
        color_model.set_domain(None)
        self.color_by = None
        if len(model) == 2 and model[0] is None:
            if model[1] == "Name":
                self.annotation_if_names = self.annotation
            if model[1] == self.basic_annotations[1]:
                self.annotation_if_enumerate = self.annotation
        if isinstance(items, Orange.data.Table) and axis:
            metas_class = tuple(
                filter_visible(chain(items.domain.metas,
                                     items.domain.class_vars)))
            visible_attrs = tuple(filter_visible(items.domain.attributes))
            if not (metas_class or visible_attrs):
                model[:] = self.basic_annotations
            else:
                model[:] = (
                    (None, )
                    + metas_class
                    + (model.Separator, ) * bool(metas_class and visible_attrs)
                    + visible_attrs)
            for meta in items.domain.metas:
                if isinstance(meta, StringVariable):
                    self.annotation = meta
                    break
            else:
                if items.domain.class_vars:
                    # No string metas: show class
                    self.annotation = items.domain.class_vars[0]
                else:
                    # No string metas and no class: show the first option
                    # which is not None (in the worst case, Enumeration)
                    self.annotation = model[1]

            color_model.set_domain(items.domain)
            if items.domain.class_vars:
                self.color_by = items.domain.class_vars[0]
            self.openContext(items.domain)
        elif isinstance(items, Orange.data.Table) and not axis \
                or (isinstance(items, list) and \
                    all(isinstance(var, Orange.data.Variable)
                        for var in items)):
            model[:] = (None, "Name")
            self.annotation = self.annotation_if_names
        else:
            model[:] = self.basic_annotations
            self.annotation = self.annotation_if_enumerate

        no_colors = len(color_model) == 1
        self.controls.color_by.setDisabled(no_colors)
        self.color_by_label.setDisabled(no_colors)

    def _clear_plot(self):
        self.dendrogram.set_root(None)
        self.label_model.clear()

    def _set_displayed_root(self, root):
        self._clear_plot()
        self._displayed_root = root
        self.dendrogram.set_root(root)
        self._update_labels()

    def _update(self):
        self._clear_plot()

        distances = self.matrix

        if distances is not None:
            method = LINKAGE_ARGS[self.linkage]
            Z = dist_matrix_linkage(distances, linkage=method)

            tree = tree_from_linkage(Z)
            self.linkmatrix = Z
            self.root = tree

            self.top_axis.setRange(tree.value.height, 0.0)
            self.bottom_axis.setRange(tree.value.height, 0.0)

            if self.pruning:
                self._set_displayed_root(prune(tree, level=self.max_depth))
            else:
                self._set_displayed_root(tree)
        else:
            self.linkmatrix = None
            self.root = None
            self._set_displayed_root(None)

        self._apply_selection()

    def _update_labels(self):
        if not hasattr(self, "label_model"):
            # This method can be called during widget initialization when
            # creating check box for label_only_subset, if it's value is
            # initially True.
            # See https://github.com/biolab/orange-widget-base/pull/213;
            # if it's merged, this check can be removed.
            return

        self.Warning.pruning_disables_colors(
            shown=self.pruning
                  and (self.subset_rows or self.color_by is not None))
        labels = []
        if self.root and self._displayed_root:
            indices = [leaf.value.index for leaf in leaves(self.root)]

            if self.annotation is None:
                if not self.pruning \
                        and self.subset_rows and self.color_by is None:
                    # Model fails if number of labels and of colors mismatch
                    labels = [""] * len(indices)
                else:
                    labels = []
            elif self.annotation == "Enumeration":
                labels = [str(i+1) for i in indices]
            elif self.annotation == "Name":
                attr = self.matrix.row_items.domain.attributes
                labels = [str(attr[i]) for i in indices]
            elif isinstance(self.annotation, Orange.data.Variable):
                col_data = self.items.get_column(self.annotation)
                labels = [self.annotation.str_val(val) for val in col_data]
                labels = [labels[idx] for idx in indices]
            else:
                labels = []
            if not self.pruning and \
                    labels and self.label_only_subset and self.subset_rows:
                labels = [label if row in self.subset_rows else ""
                          for row, label in enumerate(labels)]

            if labels and self._displayed_root is not self.root:
                joined = leaves(self._displayed_root)
                labels = [", ".join(labels[leaf.value.first: leaf.value.last])
                          for leaf in joined]

        self.label_model[:] = labels
        self.label_model.set_subset(set() if self.pruning else self.subset_rows)
        self.labels.setMinimumWidth(1 if labels else -1)

        if not self.pruning and self.color_by is not None:
            col = self.items.get_column(self.color_by)
            self.label_model.set_colors(
                self.color_by.palette.values_to_qcolors(col[indices]))
        else:
            self.label_model.set_colors(None)

    def _restore_selection(self, state):
        # type: (SelectionState) -> bool
        """
        Restore the (manual) node selection state.

        Return True if successful; False otherwise.
        """
        linkmatrix = self.linkmatrix
        if self.selection_method == 0 and self.root:
            selected, linksaved = state
            linkstruct = np.array(linksaved, dtype=float)
            selected = set(selected)  # type: Set[Tuple[int]]
            if not selected:
                return False
            if linkmatrix.shape[0] != linkstruct.shape[0]:
                return False
            # check that the linkage matrix structure matches. Use isclose for
            # the height column to account for inexact floating point math
            # (e.g. summation order in different ?gemm implementations for
            # euclidean distances, ...)
            if np.any(linkstruct[:, :2] != linkmatrix[:, :2]) or \
                    not np.all(np.isclose(linkstruct[:, 2], linkstruct[:, 2])):
                return False
            selection = []
            indices = np.array([n.value.index for n in leaves(self.root)],
                               dtype=int)
            # mapping from ranges to display (pruned) nodes
            mapping = {node.value.range: node
                       for node in postorder(self._displayed_root)}
            for node in postorder(self.root):  # type: Tree
                r = tuple(indices[node.value.first: node.value.last])
                if r in selected:
                    if node.value.range not in mapping:
                        # the node was pruned from display and cannot be
                        # selected
                        break
                    selection.append(mapping[node.value.range])
                    selected.remove(r)
                if not selected:
                    break  # found all, nothing more to do
            if selection and selected:
                # Could not restore all selected nodes (only partial match)
                return False

            self._set_selected_nodes(selection)
            return True
        return False

    def _set_selected_nodes(self, selection):
        # type: (List[Tree]) -> None
        """
        Set the nodes in `selection` to be the current selected nodes.

        The selection nodes must be subtrees of the current `_displayed_root`.
        """
        self.dendrogram.selectionChanged.disconnect(self._invalidate_output)
        try:
            self.dendrogram.set_selected_clusters(selection)
        finally:
            self.dendrogram.selectionChanged.connect(self._invalidate_output)

    def _max_depth_return(self):
        if self.pruning != 1:
            self.pruning = 1
            self._invalidate_pruning()

    def _max_depth_changed(self):
        self.pruning = 1
        self._invalidate_pruning()

    def _invalidate_clustering(self):
        self._update()
        self._update_labels()
        self._invalidate_output()

    def _invalidate_output(self):
        self.commit.deferred()

    def _invalidate_pruning(self):
        if self.root:
            selection = self.dendrogram.selected_nodes()
            ranges = [node.value.range for node in selection]
            if self.pruning:
                self._set_displayed_root(
                    prune(self.root, level=self.max_depth))
            else:
                self._set_displayed_root(self.root)
            selected = [node for node in preorder(self._displayed_root)
                        if node.value.range in ranges]

            self.dendrogram.set_selected_clusters(selected)

        self._apply_selection()

    @gui.deferred
    def commit(self):
        items = getattr(self.matrix, "items", self.items)
        if not items:
            self.Outputs.selected_data.send(None)
            self.Outputs.annotated_data.send(None)
            return

        selection = self.dendrogram.selected_nodes()
        selection = sorted(selection, key=lambda c: c.value.first)

        indices = [leaf.value.index for leaf in leaves(self.root)]

        maps = [indices[node.value.first:node.value.last]
                for node in selection]

        selected_indices = list(chain(*maps))

        if not selected_indices:
            self.Outputs.selected_data.send(None)
            annotated_data = lazy_annotated_table(items, []) \
                if self.selection_method == 0 and self.matrix.axis else None
            self.Outputs.annotated_data.send(annotated_data)
            return

        selected_data = annotated_data = None

        if isinstance(items, Orange.data.Table) and self.matrix.axis == 1:
            # Select rows
            data, domain = items, items.domain

            c = np.full(self.matrix.shape[0], len(maps))
            for i, indices in enumerate(maps):
                c[indices] = i

            clust_name = get_unique_names(domain, "Cluster")
            values = [f"C{i + 1}" for i in range(len(maps))]

            sel_clust_var = Orange.data.DiscreteVariable(
                name=clust_name, values=values)
            sel_domain = add_columns(domain, metas=(sel_clust_var,))
            selected_data = LazyValue[Table](
                lambda: items.add_column(
                    sel_clust_var, c, to_metas=True)[c != len(maps)],
                domain=sel_domain, length=len(selected_indices))

            ann_clust_var = Orange.data.DiscreteVariable(
                name=clust_name, values=values + ["Other"]
            )
            ann_domain = add_columns(
                domain_with_annotation_column(data)[0], metas=(ann_clust_var, ))
            annotated_data = LazyValue[Table](
                lambda: create_annotated_table(
                    data=items.add_column(ann_clust_var, c, to_metas=True),
                    selected_indices=selected_indices),
                domain=ann_domain, length=len(items)
            )

        elif isinstance(items, Orange.data.Table) and self.matrix.axis == 0:
            # Select columns
            attrs = []
            unselected_indices = sorted(set(range(self.root.value.last)) -
                                        set(selected_indices))
            for clust, indices in chain(enumerate(maps, start=1),
                                        [(0, unselected_indices)]):
                for i in indices:
                    attr = items.domain[i].copy()
                    attr.attributes["cluster"] = clust
                    attrs.append(attr)
            all_domain = Orange.data.Domain(
                # len(unselected_indices) can be 0
                attrs[:len(attrs) - len(unselected_indices)],
                items.domain.class_vars, items.domain.metas)

            selected_data = LazyValue[Table](
                lambda: items.from_table(all_domain, items),
                domain=all_domain, length=len(items))

            sel_domain = Orange.data.Domain(
                attrs,
                items.domain.class_vars, items.domain.metas)
            annotated_data = LazyValue[Table](
                lambda: items.from_table(sel_domain, items),
                domain=sel_domain, length=len(items))

        self.Outputs.selected_data.send(selected_data)
        self.Outputs.annotated_data.send(annotated_data)

    @Slot(QPointF)
    def _activate_cut_line(self, pos: QPointF):
        """Activate cut line selection an set cut value to `pos.x()`."""
        self.selection_method = 1
        self.cut_line.setValue(pos.x())
        self._selection_method_changed()

    def onDeleteWidget(self):
        super().onDeleteWidget()
        self._clear_plot()
        self.dendrogram.clear()
        self.dendrogram.deleteLater()

    def _dendrogram_geom_changed(self):
        pos = self.dendrogram.pos_at_height(self.cutoff_height)
        geom = self.dendrogram.geometry()
        self._set_slider_value(pos.x(), geom.width())

        self.cut_line.setLength(
            self.bottom_axis.geometry().bottom()
            - self.top_axis.geometry().top()
        )

        geom = self._main_graphics.geometry()
        assert geom.topLeft() == QPointF(0, 0)

        def adjustLeft(rect):
            rect = QRectF(rect)
            rect.setLeft(geom.left())
            return rect
        margin = 3
        self.scene.setSceneRect(geom)
        self.view.setSceneRect(geom)
        self.view.setHeaderSceneRect(
            adjustLeft(self.top_axis.geometry()).adjusted(0, 0, 0, margin)
        )
        self.view.setFooterSceneRect(
            adjustLeft(self.bottom_axis.geometry()).adjusted(0, -margin, 0, 0)
        )

    def _dendrogram_slider_changed(self, value):
        p = QPointF(value, 0)
        cl_height = self.dendrogram.height_at(p)

        self.set_cutoff_height(cl_height)

    def _set_slider_value(self, value, span):
        with blocked(self.cut_line):
            self.cut_line.setRange(0, span)
            self.cut_line.setValue(value)

    def set_cutoff_height(self, height):
        self.cutoff_height = height
        if self.root:
            self.cut_ratio = 100 * height / self.root.value.height
        self.select_max_height(height)

    def _set_cut_line_visible(self, visible):
        self.cut_line.setVisible(visible)

    def select_top_n(self, n):
        root = self._displayed_root
        if root:
            clusters = top_clusters(root, n)
            self.dendrogram.set_selected_clusters(clusters)

    def select_max_height(self, height):
        root = self._displayed_root
        if root:
            clusters = clusters_at_height(root, height)
            self.dendrogram.set_selected_clusters(clusters)

    def _cut_ratio_changed(self):
        self.selection_method = 1
        self._selection_method_changed()

    def _cut_ratio_return(self):
        if self.selection_method != 1:
            self.selection_method = 1
            self._selection_method_changed()

    def _top_n_changed(self):
        self.selection_method = 2
        self._selection_method_changed()

    def _top_n_return(self):
        if self.selection_method != 2:
            self.selection_method = 2
            self._selection_method_changed()

    def _selection_method_changed(self):
        self._set_cut_line_visible(self.selection_method == 1)
        if self.root:
            self._apply_selection()

    def _apply_selection(self):
        if not self.root:
            return

        if self.selection_method == 0:
            pass
        elif self.selection_method == 1:
            height = self.cut_ratio * self.root.value.height / 100
            self.set_cutoff_height(height)
            pos = self.dendrogram.pos_at_height(height)
            self._set_slider_value(pos.x(), self.dendrogram.size().width())
        elif self.selection_method == 2:
            self.select_top_n(self.top_n)

    def _selection_edited(self):
        # Selection was edited by clicking on a cluster in the
        # dendrogram view.
        self.selection_method = 0
        self._selection_method_changed()
        self._invalidate_output()

    def _save_selection(self):
        # Save the current manual node selection state
        selection_state = None
        if self.selection_method == 0 and self.root:
            assert self.linkmatrix is not None
            linkmat = [(int(_0), int(_1), _2)
                       for _0, _1, _2 in self.linkmatrix[:, :3].tolist()]
            nodes_ = self.dendrogram.selected_nodes()
            # match the display (pruned) nodes back (by ranges)
            mapping = {node.value.range: node for node in postorder(self.root)}
            nodes = [mapping[node.value.range] for node in nodes_]
            indices = [tuple(node.value.index for node in leaves(node))
                       for node in nodes]
            if nodes:
                selection_state = (indices, linkmat)
        return selection_state

    def save_state(self):
        # type: () -> Dict[str, Any]
        """
        Save state for `set_restore_state`
        """
        selection = self._save_selection()
        res = {"version": (0, 0, 0)}
        if selection is not None:
            res["selection_state"] = selection
        return res

    def set_restore_state(self, state):
        # type: (Dict[str, Any]) -> bool
        """
        Restore session data from a saved state.

        Parameters
        ----------
        state : Dict[str, Any]

        NOTE
        ----
        This is method called while the instance (self) is being constructed,
        even before its `__init__` is called. Consider `self` to be only a
        `QObject` at this stage.
        """
        if "selection_state" in state:
            selection = state["selection_state"]
            self.__pending_selection_restore = selection
        return True

    def __zoom_in(self):
        def clip(minval, maxval, val):
            return min(max(val, minval), maxval)
        self.zoom_factor = clip(self.zoom_slider.minimum(),
                                self.zoom_slider.maximum(),
                                self.zoom_factor + 1)
        self.__update_font_scale()

    def __zoom_out(self):
        def clip(minval, maxval, val):
            return min(max(val, minval), maxval)
        self.zoom_factor = clip(self.zoom_slider.minimum(),
                                self.zoom_slider.maximum(),
                                self.zoom_factor - 1)
        self.__update_font_scale()

    def __zoom_reset(self):
        self.zoom_factor = 0
        self.__update_font_scale()

    def __update_font_scale(self):
        font = self.scene.font()
        factor = (1.25 ** self.zoom_factor)
        font = qfont_scaled(font, factor)
        self._main_graphics.setFont(font)
        self.label_model.setFont(font)

    def send_report(self):
        annot = self.label_cb.currentText()
        if isinstance(self.annotation, str):
            annot = annot.lower()
        if self.selection_method == 0:
            sel = "manual"
        elif self.selection_method == 1:
            sel = "at {:.1f} of height".format(self.cut_ratio)
        else:
            sel = f"top {self.top_n} {pl(self.top_n, 'cluster')}"
        self.report_items((
            ("Linkage", LINKAGE[self.linkage]),
            ("Annotation", annot),
            ("Pruning",
             self.pruning != 0 and "{} levels".format(self.max_depth)),
            ("Selection", sel),
        ))
        self.report_plot()

    @classmethod
    def migrate_context(cls, context, version):
        if version < 2:
            if context.values["annotation"] == "None":
                context.values["annotation"] = None


def qfont_scaled(font, factor):
    scaled = QFont(font)
    if font.pointSizeF() != -1:
        scaled.setPointSizeF(font.pointSizeF() * factor)
    elif font.pixelSize() != -1:
        scaled.setPixelSize(int(font.pixelSize() * factor))
    return scaled


class AxisItem(AxisItem):
    mousePressed = Signal(QPointF, Qt.MouseButton)
    mouseMoved = Signal(QPointF, Qt.MouseButtons)
    mouseReleased = Signal(QPointF, Qt.MouseButton)

    #: \reimp
    def wheelEvent(self, event):
        event.ignore()  # ignore event to propagate to the view -> scroll

    def mousePressEvent(self, event: QGraphicsSceneMouseEvent) -> None:
        self.mousePressed.emit(event.pos(), event.button())
        super().mousePressEvent(event)
        event.accept()

    def mouseMoveEvent(self, event):
        self.mouseMoved.emit(event.pos(), event.buttons())
        super().mouseMoveEvent(event)
        event.accept()

    def mouseReleaseEvent(self, event):
        self.mouseReleased.emit(event.pos(), event.button())
        super().mouseReleaseEvent(event)
        event.accept()


class SliderLine(QGraphicsWidget):
    """A movable slider line."""
    valueChanged = Signal(float)

    linePressed = Signal()
    lineMoved = Signal()
    lineReleased = Signal()
    rangeChanged = Signal(float, float)

    def __init__(self, parent=None, orientation=Qt.Vertical, value=0.0,
                 length=10.0, **kwargs):
        self._orientation = orientation
        self._value = value
        self._length = length
        self._min = 0.0
        self._max = 1.0
        self._line: Optional[QLineF] = QLineF()
        self._pen: Optional[QPen] = None
        super().__init__(parent, **kwargs)
        self.setAcceptedMouseButtons(Qt.LeftButton)
        if self._orientation == Qt.Vertical:
            self.setCursor(Qt.SizeVerCursor)
        else:
            self.setCursor(Qt.SizeHorCursor)

    def setPen(self, pen: Union[QPen, Qt.GlobalColor, Qt.PenStyle]) -> None:
        pen = QPen(pen)
        if self._pen != pen:
            self.prepareGeometryChange()
            self._pen = pen
            self._line = None
            self.update()

    def pen(self) -> QPen:
        if self._pen is None:
            return QPen(self.palette().text(), 1.0, Qt.DashLine)
        else:
            return QPen(self._pen)

    def setValue(self, value: float):
        value = min(max(value, self._min), self._max)

        if self._value != value:
            self.prepareGeometryChange()
            self._value = value
            self._line = None
            self.valueChanged.emit(value)

    def value(self) -> float:
        return self._value

    def setRange(self, minval: float, maxval: float) -> None:
        maxval = max(minval, maxval)
        if minval != self._min or maxval != self._max:
            self._min = minval
            self._max = maxval
            self.rangeChanged.emit(minval, maxval)
            self.setValue(self._value)

    def setLength(self, length: float):
        if self._length != length:
            self.prepareGeometryChange()
            self._length = length
            self._line = None

    def length(self) -> float:
        return self._length

    def setOrientation(self, orientation: Qt.Orientation):
        if self._orientation != orientation:
            self.prepareGeometryChange()
            self._orientation = orientation
            self._line = None
            if self._orientation == Qt.Vertical:
                self.setCursor(Qt.SizeVerCursor)
            else:
                self.setCursor(Qt.SizeHorCursor)

    def mousePressEvent(self, event: QGraphicsSceneMouseEvent) -> None:
        event.accept()
        self.linePressed.emit()

    def mouseMoveEvent(self, event: QGraphicsSceneMouseEvent) -> None:
        pos = event.pos()
        if self._orientation == Qt.Vertical:
            self.setValue(pos.y())
        else:
            self.setValue(pos.x())
        self.lineMoved.emit()
        event.accept()

    def mouseReleaseEvent(self, event: QGraphicsSceneMouseEvent) -> None:
        if self._orientation == Qt.Vertical:
            self.setValue(event.pos().y())
        else:
            self.setValue(event.pos().x())
        self.lineReleased.emit()
        event.accept()

    def shape(self) -> QPainterPath:
        path = QPainterPath()
        path.addRect(self.boundingRect())
        return path

    def boundingRect(self) -> QRectF:
        if self._line is None:
            if self._orientation == Qt.Vertical:
                self._line = QLineF(0, self._value, self._length, self._value)
            else:
                self._line = QLineF(self._value, 0, self._value, self._length)
        r = QRectF(self._line.p1(), self._line.p2())
        penw = self.pen().width()
        return r.adjusted(-penw, -penw, penw, penw)

    def paint(self, painter, *args):
        if self._line is None:
            self.boundingRect()

        painter.save()
        painter.setPen(self.pen())
        painter.drawLine(self._line)
        painter.restore()


def clusters_at_height(root, height):
    """Return a list of clusters by cutting the clustering at `height`.
    """
    lower = set()
    cluster_list = []
    for cl in preorder(root):
        if cl in lower:
            continue
        if cl.value.height < height:
            cluster_list.append(cl)
            lower.update(preorder(cl))
    return cluster_list


def main():
    # pragma: no cover
    from Orange import distance  # pylint: disable=import-outside-toplevel
    data = Orange.data.Table("iris")
    matrix = distance.Euclidean(distance._preprocess(data))
    subset = data[10:30]
    WidgetPreview(OWHierarchicalClustering).run(matrix, set_subset=subset)


if __name__ == "__main__":  # pragma: no cover
    main()

```
--- 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:** AttributeError when selecting more than 20 clusters in Hierarchical Clustering widget

**Description:**
When configuring the Hierarchical Clustering widget to select more than 20 clusters, an AttributeError occurs because the `Warning` object does not have a `many_clusters` attribute. This issue prevents the widget from displaying the intended warning message about potential performance degradation when handling a large number of clusters.

**Example Code:**
```python
widget = OWHierarchicalClustering()
widget.top_n = 21
widget.selection_box.buttons[2].click()
# Expected: Warning.many_clusters is shown
# Actual: AttributeError: 'Warning' object has no attribute 'many_clusters'
```

**Expected Behavior:**
Setting the `top_n` parameter to a value greater than 20 should display the `many_clusters` warning, informing users that selecting a large number of clusters may degrade the performance of downstream widgets.

**Actual Behavior:**
An AttributeError is raised stating that the `'Warning' object has no attribute 'many_clusters'`, which prevents the warning from being displayed when more than 20 clusters are selected.
[/ISSUE]
{
    "TestOWHierarchicalClustering.test_all_zero_inputs": "PASSED",
    "TestOWHierarchicalClustering.test_annotation_settings_retrieval": "PASSED",
    "TestOWHierarchicalClustering.test_column_distances": "PASSED",
    "TestOWHierarchicalClustering.test_domain_loses_class": "PASSED",
    "TestOWHierarchicalClustering.test_empty_matrix": "PASSED",
    "TestOWHierarchicalClustering.test_image_export": "PASSED",
    "TestOWHierarchicalClustering.test_infinite_distances": "PASSED",
    "TestOWHierarchicalClustering.test_many_values_warning": "PASSED",
    "TestOWHierarchicalClustering.test_minimum_size": "PASSED",
    "TestOWHierarchicalClustering.test_msg_base_class": "PASSED",
    "TestOWHierarchicalClustering.test_not_symmetric": "PASSED",
    "TestOWHierarchicalClustering.test_output_cut_ratio": "PASSED",
    "TestOWHierarchicalClustering.test_outputs": "PASSED",
    "TestOWHierarchicalClustering.test_restore_state": "PASSED",
    "TestOWHierarchicalClustering.test_retain_selection": "PASSED",
    "TestOWHierarchicalClustering.test_selection_box_output": "PASSED"
}
diff --git a/Orange/widgets/unsupervised/owhierarchicalclustering.py b/Orange/widgets/unsupervised/owhierarchicalclustering.py
index f71cc12ba..dd73e5a70 100644
--- a/Orange/widgets/unsupervised/owhierarchicalclustering.py
+++ b/Orange/widgets/unsupervised/owhierarchicalclustering.py
@@ -240,6 +240,9 @@ class OWHierarchicalClustering(widget.OWWidget):
             Msg("Subset data refers to a different table")
         pruning_disables_colors = \
             Msg("Pruned cluster doesn't show colors and indicate subset")
+        many_clusters = \
+            Msg("Variables with too many values may "
+                "degrade the performance of downstream widgets.")
 
     #: Stored (manual) selection state (from a saved workflow) to restore.
     __pending_selection_restore = None  # type: Optional[SelectionState]
@@ -361,7 +364,7 @@ class OWHierarchicalClustering(widget.OWWidget):
             2, 0
         )
         self.top_n_spin = gui.spin(
-            self.selection_box, self, "top_n", 1, 20,
+            self.selection_box, self, "top_n", 1, 1000,
             controlWidth=spin_width, alignment=Qt.AlignRight,
             callback=self._top_n_changed, addToLayout=False)
         self.top_n_spin.lineEdit().returnPressed.connect(self._top_n_return)
@@ -766,6 +769,7 @@ class OWHierarchicalClustering(widget.OWWidget):
     @gui.deferred
     def commit(self):
         items = getattr(self.matrix, "items", self.items)
+        self.Warning.many_clusters.clear()
         if not items:
             self.Outputs.selected_data.send(None)
             self.Outputs.annotated_data.send(None)
@@ -778,6 +782,8 @@ class OWHierarchicalClustering(widget.OWWidget):
 
         maps = [indices[node.value.first:node.value.last]
                 for node in selection]
+        if len(maps) > 20:
+            self.Warning.many_clusters()
 
         selected_indices = list(chain(*maps))
 

{"Orange/widgets/unsupervised/owhierarchicalclustering.py": "from itertools import chain\nfrom contextlib import contextmanager\n\nimport typing\nfrom typing import Any, List, Tuple, Dict, Optional, Set, Union\n\nimport numpy as np\n\nfrom AnyQt.QtWidgets import (\n    QGraphicsWidget, QGraphicsScene, QGridLayout, QSizePolicy,\n    QAction, QComboBox, QGraphicsGridLayout, QGraphicsSceneMouseEvent, QLabel\n)\nfrom AnyQt.QtGui import (QPen, QFont, QKeySequence, QPainterPath, QColor,\n    QFontMetrics)\nfrom AnyQt.QtCore import (\n    Qt, QObject, QSize, QPointF, QRectF, QLineF, QEvent, QModelIndex\n)\nfrom AnyQt.QtCore import pyqtSignal as Signal, pyqtSlot as Slot\n\nfrom Orange.widgets.utils.localization import pl\nfrom orangewidget.utils.itemmodels import PyListModel\nfrom orangewidget.utils.signals import LazyValue\n\nimport Orange.data\nfrom Orange.data.domain import filter_visible\nfrom Orange.data import Domain, DiscreteVariable, ContinuousVariable, \\\n    StringVariable, Table\nimport Orange.misc\nfrom Orange.clustering.hierarchical import \\\n    postorder, preorder, Tree, tree_from_linkage, dist_matrix_linkage, \\\n    leaves, prune, top_clusters\nfrom Orange.data.util import get_unique_names\n\nfrom Orange.widgets import widget, gui, settings\nfrom Orange.widgets.utils import itemmodels, combobox\nfrom Orange.widgets.utils.annotated_data import (lazy_annotated_table,\n                                                 ANNOTATED_DATA_SIGNAL_NAME,\n                                                 domain_with_annotation_column,\n                                                 add_columns,\n                                                 create_annotated_table)\nfrom Orange.widgets.utils.widgetpreview import WidgetPreview\nfrom Orange.widgets.visualize.utils.plotutils import AxisItem\nfrom Orange.widgets.widget import Input, Output, Msg\n\nfrom Orange.widgets.utils.stickygraphicsview import StickyGraphicsView\nfrom Orange.widgets.utils.graphicsview import GraphicsWidgetView\nfrom Orange.widgets.utils.graphicstextlist import TextListView\nfrom Orange.widgets.utils.dendrogram import DendrogramWidget\n\n__all__ = [\"OWHierarchicalClustering\"]\n\n\nLINKAGE = [\"Single\", \"Average\", \"Weighted\", \"Complete\", \"Ward\"]\nLINKAGE_ARGS = [\"single\", \"average\", \"weighted\", \"complete\", \"ward\"]\nDEFAULT_LINKAGE = \"Ward\"\n\n\ndef make_pen(brush=Qt.black, width=1, style=Qt.SolidLine,\n             cap_style=Qt.SquareCap, join_style=Qt.BevelJoin,\n             cosmetic=False):\n    pen = QPen(brush)\n    pen.setWidth(width)\n    pen.setStyle(style)\n    pen.setCapStyle(cap_style)\n    pen.setJoinStyle(join_style)\n    pen.setCosmetic(cosmetic)\n    return pen\n\n\n@contextmanager\ndef blocked(obj):\n    old = obj.signalsBlocked()\n    obj.blockSignals(True)\n    try:\n        yield obj\n    finally:\n        obj.blockSignals(old)\n\n\nclass SaveStateSettingsHandler(settings.SettingsHandler):\n    \"\"\"\n    A settings handler that delegates session data store/restore to the\n    OWWidget instance.\n\n    The OWWidget subclass must implement `save_state() -> Dict[str, Any]` and\n    `set_restore_state(state: Dict[str, Any])` methods.\n    \"\"\"\n    def initialize(self, instance, data=None):\n        super().initialize(instance, data)\n        if data is not None and \"__session_state_data\" in data:\n            session_data = data[\"__session_state_data\"]\n            instance.set_restore_state(session_data)\n\n    def pack_data(self, widget):\n        # type: (widget.OWWidget) -> dict\n        res = super().pack_data(widget)\n        state = widget.save_state()\n        if state:\n            assert \"__session_state_data\" not in res\n            res[\"__session_state_data\"] = state\n        return res\n\n\nclass _DomainContextHandler(settings.DomainContextHandler,\n                            SaveStateSettingsHandler):\n    pass\n\n\nif typing.TYPE_CHECKING:\n    #: Encoded selection state for persistent storage.\n    #: This is a list of tuples of leaf indices in the selection and\n    #: a (N, 3) linkage matrix for validation (the 4-th column from scipy\n    #: is omitted).\n    SelectionState = Tuple[List[Tuple[int]], List[Tuple[int, int, float]]]\n\n\nclass SelectedLabelsModel(PyListModel):\n    def __init__(self):\n        super().__init__([])\n        self.subset = set()\n        self.__font = QFont()\n        self.__colors = None\n\n    def rowCount(self, parent=QModelIndex()):\n        count = super().rowCount()\n        if self.__colors is not None:\n            count = max(count, len(self.__colors))\n        return count\n\n    def _emit_data_changed(self):\n        self.dataChanged.emit(self.index(0, 0), self.index(len(self) - 1, 0))\n\n    def set_subset(self, subset):\n        self.subset = set(subset)\n        self._emit_data_changed()\n\n    def set_colors(self, colors):\n        self.__colors = colors\n        self._emit_data_changed()\n\n    def setFont(self, font):\n        self.__font = font\n        self._emit_data_changed()\n\n    def data(self, index, role=Qt.DisplayRole):\n        if role == Qt.FontRole:\n            font = QFont(self.__font)\n            font.setBold(index.row() in self.subset)\n            return font\n        if role == Qt.BackgroundRole:\n            if self.__colors is not None:\n                return self.__colors[index.row()]\n            elif not any(self) and self.subset:  # no labels, no color, but subset\n                return QColor(0, 0, 0)\n        if role == Qt.UserRole and self.subset:\n            return index.row() in self.subset\n\n        return super().data(index, role)\n\n\nclass GraphicsView(GraphicsWidgetView, StickyGraphicsView):\n    def minimumSizeHint(self) -> QSize:\n        msh = super().minimumSizeHint()\n        w = self.centralWidget()\n        if w is not None:\n            width = w.minimumWidth() + 4 + self.verticalScrollBar().width()\n            msh.setWidth(max(int(width), msh.width()))\n        return msh\n\n    def eventFilter(self, recv: QObject, event: QEvent) -> bool:\n        ret = super().eventFilter(recv, event)\n        if event.type() == QEvent.LayoutRequest and recv is self.centralWidget():\n            self.updateGeometry()\n        return ret\n\n\nclass OWHierarchicalClustering(widget.OWWidget):\n    name = \"Hierarchical Clustering\"\n    description = \"Display a dendrogram of a hierarchical clustering \" \\\n                  \"constructed from the input distance matrix.\"\n    icon = \"icons/HierarchicalClustering.svg\"\n    priority = 2100\n    keywords = \"hierarchical clustering\"\n\n    class Inputs:\n        distances = Input(\"Distances\", Orange.misc.DistMatrix)\n        subset = Input(\"Data Subset\", Orange.data.Table, explicit=True)\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    settings_version = 2\n    settingsHandler = _DomainContextHandler()\n\n    #: Selected linkage\n    linkage = settings.Setting(LINKAGE.index(DEFAULT_LINKAGE))\n    #: Index of the selected annotation item (variable, ...)\n    annotation = settings.ContextSetting(\"Enumeration\")\n    #: Out-of-context setting for the case when the \"Name\" option is available\n    annotation_if_names = settings.Setting(\"Name\")\n    #: Out-of-context setting for the case with just \"Enumeration\" and \"None\"\n    annotation_if_enumerate = settings.Setting(\"Enumeration\")\n    #: Selected tree pruning (none/max depth)\n    pruning = settings.Setting(0)\n    #: Maximum depth when max depth pruning is selected\n    max_depth = settings.Setting(10)\n\n    #: Selected cluster selection method (none, cut distance, top n)\n    selection_method = settings.Setting(0)\n    #: Cut height ratio wrt root height\n    cut_ratio = settings.Setting(75.0)\n    #: Number of top clusters to select\n    top_n = settings.Setting(3)\n    #: Dendrogram zoom factor\n    zoom_factor = settings.Setting(0)\n    #: Show labels only for subset (if present)\n    label_only_subset = settings.Setting(False)\n    #: Color for label decoration\n    color_by: Union[DiscreteVariable, ContinuousVariable, None] = \\\n        settings.ContextSetting(None)\n\n    autocommit = settings.Setting(True)\n\n    graph_name = \"scene\"  # QGraphicsScene\n\n    basic_annotations = [None, \"Enumeration\"]\n\n    class Error(widget.OWWidget.Error):\n        empty_matrix = Msg(\"Distance matrix is empty.\")\n        not_finite_distances = Msg(\"Some distances are infinite\")\n        not_symmetric = widget.Msg(\"Distance matrix is not symmetric.\")\n\n    class Warning(widget.OWWidget.Warning):\n        subset_on_no_table = \\\n            Msg(\"Unused data subset: distances do not refer to data instances\")\n        subset_not_subset = \\\n            Msg(\"Some data from the subset does not appear in distance matrix\")\n        subset_wrong = \\\n            Msg(\"Subset data refers to a different table\")\n        pruning_disables_colors = \\\n            Msg(\"Pruned cluster doesn't show colors and indicate subset\")\n\n    #: Stored (manual) selection state (from a saved workflow) to restore.\n    __pending_selection_restore = None  # type: Optional[SelectionState]\n\n    def __init__(self):\n        super().__init__()\n\n        self.matrix = None\n        self.items = None\n        self.subset = None\n        self.subset_rows = set()\n        self.linkmatrix = None\n        self.root = None\n        self._displayed_root = None\n        self.cutoff_height = 0.0\n\n        spin_width = QFontMetrics(self.font()).horizontalAdvance(\"M\" * 7)\n        gui.comboBox(\n            self.controlArea, self, \"linkage\", items=LINKAGE, box=\"Linkage\",\n            callback=self._invalidate_clustering)\n\n        model = itemmodels.VariableListModel(placeholder=\"None\")\n        model[:] = self.basic_annotations\n\n        grid = QGridLayout()\n        gui.widgetBox(self.controlArea, \"Annotations\", orientation=grid)\n        self.label_cb = cb = combobox.ComboBoxSearch(\n            minimumContentsLength=14,\n            sizeAdjustPolicy=QComboBox.AdjustToMinimumContentsLengthWithIcon\n        )\n        cb.setModel(model)\n        cb.setCurrentIndex(cb.findData(self.annotation, Qt.EditRole))\n\n        def on_annotation_activated():\n            self.annotation = self.label_cb.currentData(Qt.EditRole)\n            self._update_labels()\n        cb.activated.connect(on_annotation_activated)\n\n        def on_annotation_changed(value):\n            self.label_cb.setCurrentIndex(\n                self.label_cb.findData(value, Qt.EditRole))\n        self.connect_control(\"annotation\", on_annotation_changed)\n\n        grid.addWidget(self.label_cb, 0, 0, 1, 2)\n\n        cb = gui.checkBox(\n            None, self, \"label_only_subset\", \"Show labels only for subset\",\n            disabled=True,\n            callback=self._update_labels, stateWhenDisabled=False)\n        grid.addWidget(cb, 1, 0, 1, 2)\n\n        model = itemmodels.DomainModel(\n            valid_types=(DiscreteVariable, ContinuousVariable),\n            placeholder=\"None\")\n        cb = gui.comboBox(\n            None, self, \"color_by\", orientation=Qt.Horizontal,\n            model=model, callback=self._update_labels,\n            sizePolicy=QSizePolicy(QSizePolicy.MinimumExpanding,\n                                   QSizePolicy.Fixed))\n        self.color_by_label = QLabel(\"Color by:\")\n        grid.addWidget(self.color_by_label, 2, 0)\n        grid.addWidget(cb, 2, 1)\n\n        box = gui.radioButtons(\n            self.controlArea, self, \"pruning\", box=\"Pruning\",\n            callback=self._invalidate_pruning)\n        grid = QGridLayout()\n        box.layout().addLayout(grid)\n        grid.addWidget(\n            gui.appendRadioButton(box, \"None\", addToLayout=False),\n            0, 0\n        )\n        self.max_depth_spin = gui.spin(\n            box, self, \"max_depth\", minv=1, maxv=100,\n            controlWidth=spin_width, alignment=Qt.AlignRight,\n            callback=self._max_depth_changed,\n            keyboardTracking=False, addToLayout=False\n        )\n        self.max_depth_spin.lineEdit().returnPressed.connect(\n            self._max_depth_return)\n\n        grid.addWidget(\n            gui.appendRadioButton(box, \"Max depth:\", addToLayout=False),\n            1, 0)\n        grid.addWidget(self.max_depth_spin, 1, 1)\n\n        self.selection_box = gui.radioButtons(\n            self.controlArea, self, \"selection_method\",\n            box=\"Selection\",\n            callback=self._selection_method_changed)\n\n        grid = QGridLayout()\n        self.selection_box.layout().addLayout(grid)\n        grid.addWidget(\n            gui.appendRadioButton(\n                self.selection_box, \"Manual\", addToLayout=False),\n            0, 0\n        )\n        grid.addWidget(\n            gui.appendRadioButton(\n                self.selection_box, \"Height ratio:\", addToLayout=False),\n            1, 0\n        )\n        self.cut_ratio_spin = gui.spin(\n            self.selection_box, self, \"cut_ratio\", 0, 100, step=1e-1,\n            controlWidth=spin_width, alignment = Qt.AlignRight,\n            spinType=float, callback=self._cut_ratio_changed,\n            addToLayout=False\n        )\n        self.cut_ratio_spin.setSuffix(\" %\")\n        self.cut_ratio_spin.lineEdit().returnPressed.connect(\n            self._cut_ratio_return)\n\n        grid.addWidget(self.cut_ratio_spin, 1, 1)\n\n        grid.addWidget(\n            gui.appendRadioButton(\n                self.selection_box, \"Top N:\", addToLayout=False),\n            2, 0\n        )\n        self.top_n_spin = gui.spin(\n            self.selection_box, self, \"top_n\", 1, 20,\n            controlWidth=spin_width, alignment=Qt.AlignRight,\n            callback=self._top_n_changed, addToLayout=False)\n        self.top_n_spin.lineEdit().returnPressed.connect(self._top_n_return)\n        grid.addWidget(self.top_n_spin, 2, 1)\n\n        self.zoom_slider = gui.hSlider(\n            self.controlArea, self, \"zoom_factor\", box=\"Zoom\",\n            minValue=-6, maxValue=3, step=1, ticks=True, createLabel=False,\n            callback=self.__update_font_scale)\n\n        zoom_in = QAction(\n            \"Zoom in\", self, shortcut=QKeySequence.ZoomIn,\n            triggered=self.__zoom_in\n        )\n        zoom_out = QAction(\n            \"Zoom out\", self, shortcut=QKeySequence.ZoomOut,\n            triggered=self.__zoom_out\n        )\n        zoom_reset = QAction(\n            \"Reset zoom\", self,\n            shortcut=QKeySequence(Qt.ControlModifier | Qt.Key_0),\n            triggered=self.__zoom_reset\n        )\n        self.addActions([zoom_in, zoom_out, zoom_reset])\n\n        self.controlArea.layout().addStretch()\n\n        gui.auto_send(self.buttonsArea, self, \"autocommit\")\n\n        self.scene = QGraphicsScene(self)\n        self.view = GraphicsView(\n            self.scene,\n            horizontalScrollBarPolicy=Qt.ScrollBarAlwaysOff,\n            verticalScrollBarPolicy=Qt.ScrollBarAlwaysOn,\n            alignment=Qt.AlignLeft | Qt.AlignVCenter,\n            widgetResizable=True,\n        )\n        # Disable conflicting action shortcuts. We define our own.\n        for a in self.view.viewActions():\n            a.setEnabled(False)\n        self.mainArea.layout().setSpacing(1)\n        self.mainArea.layout().addWidget(self.view)\n\n        def axis_view(orientation):\n            ax = AxisItem(orientation=orientation, maxTickLength=7)\n            ax.mousePressed.connect(self._activate_cut_line)\n            ax.mouseMoved.connect(self._activate_cut_line)\n            ax.mouseReleased.connect(self._activate_cut_line)\n            ax.setRange(1.0, 0.0)\n            return ax\n\n        self.top_axis = axis_view(\"top\")\n        self.bottom_axis = axis_view(\"bottom\")\n\n        self._main_graphics = QGraphicsWidget(\n            sizePolicy=QSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.Preferred)\n        )\n        scenelayout = QGraphicsGridLayout()\n        scenelayout.setHorizontalSpacing(10)\n        scenelayout.setVerticalSpacing(10)\n\n        self._main_graphics.setLayout(scenelayout)\n        self.scene.addItem(self._main_graphics)\n        self.view.setCentralWidget(self._main_graphics)\n        self.scene.addItem(self._main_graphics)\n\n        self.dendrogram = DendrogramWidget(pen_width=2)\n        self.dendrogram.setSizePolicy(QSizePolicy.MinimumExpanding,\n                                      QSizePolicy.MinimumExpanding)\n        self.dendrogram.selectionChanged.connect(self._invalidate_output)\n        self.dendrogram.selectionEdited.connect(self._selection_edited)\n\n        self.labels = TextListView()\n        self.label_model = SelectedLabelsModel()\n        self.labels.setModel(self.label_model)\n        self.labels.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Preferred)\n        self.labels.setAlignment(Qt.AlignLeft)\n        self.labels.setMaximumWidth(200)\n\n        scenelayout.addItem(self.top_axis, 0, 0,\n                            alignment=Qt.AlignLeft | Qt.AlignVCenter)\n        scenelayout.addItem(self.dendrogram, 1, 0,\n                            alignment=Qt.AlignLeft | Qt.AlignVCenter)\n        scenelayout.addItem(self.labels, 1, 1,\n                            alignment=Qt.AlignLeft | Qt.AlignVCenter)\n        scenelayout.addItem(self.bottom_axis, 2, 0,\n                            alignment=Qt.AlignLeft | Qt.AlignVCenter)\n        self.top_axis.setZValue(self.dendrogram.zValue() + 10)\n        self.bottom_axis.setZValue(self.dendrogram.zValue() + 10)\n        self.cut_line = SliderLine(self.top_axis,\n                                   orientation=Qt.Horizontal)\n        self.cut_line.valueChanged.connect(self._dendrogram_slider_changed)\n        self.dendrogram.geometryChanged.connect(self._dendrogram_geom_changed)\n        self._set_cut_line_visible(self.selection_method == 1)\n        self.__update_font_scale()\n\n    @Inputs.distances\n    def set_distances(self, matrix):\n        self.error()\n        self.Error.clear()\n\n        self.matrix = None\n        self.Error.clear()\n        if matrix is not None:\n            if len(matrix) < 2:\n                self.Error.empty_matrix()\n            elif not matrix.is_symmetric():\n                self.Error.not_symmetric()\n            elif not np.all(np.isfinite(matrix)):\n                self.Error.not_finite_distances()\n            else:\n                self.matrix = matrix\n\n    @Inputs.subset\n    def set_subset(self, subset):\n        self.subset = subset\n        self.controls.label_only_subset.setDisabled(subset is None)\n\n    def handleNewSignals(self):\n        if self.__pending_selection_restore is not None:\n            selection_state = self.__pending_selection_restore\n        else:\n            # save the current selection to (possibly) restore later\n            selection_state = self._save_selection()\n\n        matrix = self.matrix\n        if matrix is not None:\n            self._set_items(matrix.row_items, matrix.axis)\n        else:\n            self._set_items(None)\n        self._update()\n\n        # Can now attempt to restore session state from a saved workflow.\n        if self.root and selection_state is not None:\n            self._restore_selection(selection_state)\n            self.__pending_selection_restore = None\n\n        self.Warning.clear()\n        rows = set()\n        if self.subset:\n            subsetids = set(self.subset.ids)\n            if not isinstance(self.items, Orange.data.Table) \\\n                    or not self.matrix.axis:\n                self.Warning.subset_on_no_table()\n            elif (dataids := set(self.items.ids)) and not subsetids & dataids:\n                self.Warning.subset_wrong()\n            elif not subsetids <= dataids:\n                self.Warning.subset_not_subset()\n            else:\n                indices = [leaf.value.index for leaf in leaves(self.root)]\n                rows = {\n                    row for row, rowid in enumerate(self.items.ids[indices])\n                    if rowid in subsetids\n                }\n\n        self.subset_rows = rows\n        self._update_labels()\n        self.commit.now()\n\n    def _set_items(self, items, axis=1):\n        self.closeContext()\n        self.items = items\n        model = self.label_cb.model()\n        color_model = self.controls.color_by.model()\n        color_model.set_domain(None)\n        self.color_by = None\n        if len(model) == 2 and model[0] is None:\n            if model[1] == \"Name\":\n                self.annotation_if_names = self.annotation\n            if model[1] == self.basic_annotations[1]:\n                self.annotation_if_enumerate = self.annotation\n        if isinstance(items, Orange.data.Table) and axis:\n            metas_class = tuple(\n                filter_visible(chain(items.domain.metas,\n                                     items.domain.class_vars)))\n            visible_attrs = tuple(filter_visible(items.domain.attributes))\n            if not (metas_class or visible_attrs):\n                model[:] = self.basic_annotations\n            else:\n                model[:] = (\n                    (None, )\n                    + metas_class\n                    + (model.Separator, ) * bool(metas_class and visible_attrs)\n                    + visible_attrs)\n            for meta in items.domain.metas:\n                if isinstance(meta, StringVariable):\n                    self.annotation = meta\n                    break\n            else:\n                if items.domain.class_vars:\n                    # No string metas: show class\n                    self.annotation = items.domain.class_vars[0]\n                else:\n                    # No string metas and no class: show the first option\n                    # which is not None (in the worst case, Enumeration)\n                    self.annotation = model[1]\n\n            color_model.set_domain(items.domain)\n            if items.domain.class_vars:\n                self.color_by = items.domain.class_vars[0]\n            self.openContext(items.domain)\n        elif isinstance(items, Orange.data.Table) and not axis \\\n                or (isinstance(items, list) and \\\n                    all(isinstance(var, Orange.data.Variable)\n                        for var in items)):\n            model[:] = (None, \"Name\")\n            self.annotation = self.annotation_if_names\n        else:\n            model[:] = self.basic_annotations\n            self.annotation = self.annotation_if_enumerate\n\n        no_colors = len(color_model) == 1\n        self.controls.color_by.setDisabled(no_colors)\n        self.color_by_label.setDisabled(no_colors)\n\n    def _clear_plot(self):\n        self.dendrogram.set_root(None)\n        self.label_model.clear()\n\n    def _set_displayed_root(self, root):\n        self._clear_plot()\n        self._displayed_root = root\n        self.dendrogram.set_root(root)\n        self._update_labels()\n\n    def _update(self):\n        self._clear_plot()\n\n        distances = self.matrix\n\n        if distances is not None:\n            method = LINKAGE_ARGS[self.linkage]\n            Z = dist_matrix_linkage(distances, linkage=method)\n\n            tree = tree_from_linkage(Z)\n            self.linkmatrix = Z\n            self.root = tree\n\n            self.top_axis.setRange(tree.value.height, 0.0)\n            self.bottom_axis.setRange(tree.value.height, 0.0)\n\n            if self.pruning:\n                self._set_displayed_root(prune(tree, level=self.max_depth))\n            else:\n                self._set_displayed_root(tree)\n        else:\n            self.linkmatrix = None\n            self.root = None\n            self._set_displayed_root(None)\n\n        self._apply_selection()\n\n    def _update_labels(self):\n        if not hasattr(self, \"label_model\"):\n            # This method can be called during widget initialization when\n            # creating check box for label_only_subset, if it's value is\n            # initially True.\n            # See https://github.com/biolab/orange-widget-base/pull/213;\n            # if it's merged, this check can be removed.\n            return\n\n        self.Warning.pruning_disables_colors(\n            shown=self.pruning\n                  and (self.subset_rows or self.color_by is not None))\n        labels = []\n        if self.root and self._displayed_root:\n            indices = [leaf.value.index for leaf in leaves(self.root)]\n\n            if self.annotation is None:\n                if not self.pruning \\\n                        and self.subset_rows and self.color_by is None:\n                    # Model fails if number of labels and of colors mismatch\n                    labels = [\"\"] * len(indices)\n                else:\n                    labels = []\n            elif self.annotation == \"Enumeration\":\n                labels = [str(i+1) for i in indices]\n            elif self.annotation == \"Name\":\n                attr = self.matrix.row_items.domain.attributes\n                labels = [str(attr[i]) for i in indices]\n            elif isinstance(self.annotation, Orange.data.Variable):\n                col_data = self.items.get_column(self.annotation)\n                labels = [self.annotation.str_val(val) for val in col_data]\n                labels = [labels[idx] for idx in indices]\n            else:\n                labels = []\n            if not self.pruning and \\\n                    labels and self.label_only_subset and self.subset_rows:\n                labels = [label if row in self.subset_rows else \"\"\n                          for row, label in enumerate(labels)]\n\n            if labels and self._displayed_root is not self.root:\n                joined = leaves(self._displayed_root)\n                labels = [\", \".join(labels[leaf.value.first: leaf.value.last])\n                          for leaf in joined]\n\n        self.label_model[:] = labels\n        self.label_model.set_subset(set() if self.pruning else self.subset_rows)\n        self.labels.setMinimumWidth(1 if labels else -1)\n\n        if not self.pruning and self.color_by is not None:\n            col = self.items.get_column(self.color_by)\n            self.label_model.set_colors(\n                self.color_by.palette.values_to_qcolors(col[indices]))\n        else:\n            self.label_model.set_colors(None)\n\n    def _restore_selection(self, state):\n        # type: (SelectionState) -> bool\n        \"\"\"\n        Restore the (manual) node selection state.\n\n        Return True if successful; False otherwise.\n        \"\"\"\n        linkmatrix = self.linkmatrix\n        if self.selection_method == 0 and self.root:\n            selected, linksaved = state\n            linkstruct = np.array(linksaved, dtype=float)\n            selected = set(selected)  # type: Set[Tuple[int]]\n            if not selected:\n                return False\n            if linkmatrix.shape[0] != linkstruct.shape[0]:\n                return False\n            # check that the linkage matrix structure matches. Use isclose for\n            # the height column to account for inexact floating point math\n            # (e.g. summation order in different ?gemm implementations for\n            # euclidean distances, ...)\n            if np.any(linkstruct[:, :2] != linkmatrix[:, :2]) or \\\n                    not np.all(np.isclose(linkstruct[:, 2], linkstruct[:, 2])):\n                return False\n            selection = []\n            indices = np.array([n.value.index for n in leaves(self.root)],\n                               dtype=int)\n            # mapping from ranges to display (pruned) nodes\n            mapping = {node.value.range: node\n                       for node in postorder(self._displayed_root)}\n            for node in postorder(self.root):  # type: Tree\n                r = tuple(indices[node.value.first: node.value.last])\n                if r in selected:\n                    if node.value.range not in mapping:\n                        # the node was pruned from display and cannot be\n                        # selected\n                        break\n                    selection.append(mapping[node.value.range])\n                    selected.remove(r)\n                if not selected:\n                    break  # found all, nothing more to do\n            if selection and selected:\n                # Could not restore all selected nodes (only partial match)\n                return False\n\n            self._set_selected_nodes(selection)\n            return True\n        return False\n\n    def _set_selected_nodes(self, selection):\n        # type: (List[Tree]) -> None\n        \"\"\"\n        Set the nodes in `selection` to be the current selected nodes.\n\n        The selection nodes must be subtrees of the current `_displayed_root`.\n        \"\"\"\n        self.dendrogram.selectionChanged.disconnect(self._invalidate_output)\n        try:\n            self.dendrogram.set_selected_clusters(selection)\n        finally:\n            self.dendrogram.selectionChanged.connect(self._invalidate_output)\n\n    def _max_depth_return(self):\n        if self.pruning != 1:\n            self.pruning = 1\n            self._invalidate_pruning()\n\n    def _max_depth_changed(self):\n        self.pruning = 1\n        self._invalidate_pruning()\n\n    def _invalidate_clustering(self):\n        self._update()\n        self._update_labels()\n        self._invalidate_output()\n\n    def _invalidate_output(self):\n        self.commit.deferred()\n\n    def _invalidate_pruning(self):\n        if self.root:\n            selection = self.dendrogram.selected_nodes()\n            ranges = [node.value.range for node in selection]\n            if self.pruning:\n                self._set_displayed_root(\n                    prune(self.root, level=self.max_depth))\n            else:\n                self._set_displayed_root(self.root)\n            selected = [node for node in preorder(self._displayed_root)\n                        if node.value.range in ranges]\n\n            self.dendrogram.set_selected_clusters(selected)\n\n        self._apply_selection()\n\n    @gui.deferred\n    def commit(self):\n        items = getattr(self.matrix, \"items\", self.items)\n        if not items:\n            self.Outputs.selected_data.send(None)\n            self.Outputs.annotated_data.send(None)\n            return\n\n        selection = self.dendrogram.selected_nodes()\n        selection = sorted(selection, key=lambda c: c.value.first)\n\n        indices = [leaf.value.index for leaf in leaves(self.root)]\n\n        maps = [indices[node.value.first:node.value.last]\n                for node in selection]\n\n        selected_indices = list(chain(*maps))\n\n        if not selected_indices:\n            self.Outputs.selected_data.send(None)\n            annotated_data = lazy_annotated_table(items, []) \\\n                if self.selection_method == 0 and self.matrix.axis else None\n            self.Outputs.annotated_data.send(annotated_data)\n            return\n\n        selected_data = annotated_data = None\n\n        if isinstance(items, Orange.data.Table) and self.matrix.axis == 1:\n            # Select rows\n            data, domain = items, items.domain\n\n            c = np.full(self.matrix.shape[0], len(maps))\n            for i, indices in enumerate(maps):\n                c[indices] = i\n\n            clust_name = get_unique_names(domain, \"Cluster\")\n            values = [f\"C{i + 1}\" for i in range(len(maps))]\n\n            sel_clust_var = Orange.data.DiscreteVariable(\n                name=clust_name, values=values)\n            sel_domain = add_columns(domain, metas=(sel_clust_var,))\n            selected_data = LazyValue[Table](\n                lambda: items.add_column(\n                    sel_clust_var, c, to_metas=True)[c != len(maps)],\n                domain=sel_domain, length=len(selected_indices))\n\n            ann_clust_var = Orange.data.DiscreteVariable(\n                name=clust_name, values=values + [\"Other\"]\n            )\n            ann_domain = add_columns(\n                domain_with_annotation_column(data)[0], metas=(ann_clust_var, ))\n            annotated_data = LazyValue[Table](\n                lambda: create_annotated_table(\n                    data=items.add_column(ann_clust_var, c, to_metas=True),\n                    selected_indices=selected_indices),\n                domain=ann_domain, length=len(items)\n            )\n\n        elif isinstance(items, Orange.data.Table) and self.matrix.axis == 0:\n            # Select columns\n            attrs = []\n            unselected_indices = sorted(set(range(self.root.value.last)) -\n                                        set(selected_indices))\n            for clust, indices in chain(enumerate(maps, start=1),\n                                        [(0, unselected_indices)]):\n                for i in indices:\n                    attr = items.domain[i].copy()\n                    attr.attributes[\"cluster\"] = clust\n                    attrs.append(attr)\n            all_domain = Orange.data.Domain(\n                # len(unselected_indices) can be 0\n                attrs[:len(attrs) - len(unselected_indices)],\n                items.domain.class_vars, items.domain.metas)\n\n            selected_data = LazyValue[Table](\n                lambda: items.from_table(all_domain, items),\n                domain=all_domain, length=len(items))\n\n            sel_domain = Orange.data.Domain(\n                attrs,\n                items.domain.class_vars, items.domain.metas)\n            annotated_data = LazyValue[Table](\n                lambda: items.from_table(sel_domain, items),\n                domain=sel_domain, length=len(items))\n\n        self.Outputs.selected_data.send(selected_data)\n        self.Outputs.annotated_data.send(annotated_data)\n\n    @Slot(QPointF)\n    def _activate_cut_line(self, pos: QPointF):\n        \"\"\"Activate cut line selection an set cut value to `pos.x()`.\"\"\"\n        self.selection_method = 1\n        self.cut_line.setValue(pos.x())\n        self._selection_method_changed()\n\n    def onDeleteWidget(self):\n        super().onDeleteWidget()\n        self._clear_plot()\n        self.dendrogram.clear()\n        self.dendrogram.deleteLater()\n\n    def _dendrogram_geom_changed(self):\n        pos = self.dendrogram.pos_at_height(self.cutoff_height)\n        geom = self.dendrogram.geometry()\n        self._set_slider_value(pos.x(), geom.width())\n\n        self.cut_line.setLength(\n            self.bottom_axis.geometry().bottom()\n            - self.top_axis.geometry().top()\n        )\n\n        geom = self._main_graphics.geometry()\n        assert geom.topLeft() == QPointF(0, 0)\n\n        def adjustLeft(rect):\n            rect = QRectF(rect)\n            rect.setLeft(geom.left())\n            return rect\n        margin = 3\n        self.scene.setSceneRect(geom)\n        self.view.setSceneRect(geom)\n        self.view.setHeaderSceneRect(\n            adjustLeft(self.top_axis.geometry()).adjusted(0, 0, 0, margin)\n        )\n        self.view.setFooterSceneRect(\n            adjustLeft(self.bottom_axis.geometry()).adjusted(0, -margin, 0, 0)\n        )\n\n    def _dendrogram_slider_changed(self, value):\n        p = QPointF(value, 0)\n        cl_height = self.dendrogram.height_at(p)\n\n        self.set_cutoff_height(cl_height)\n\n    def _set_slider_value(self, value, span):\n        with blocked(self.cut_line):\n            self.cut_line.setRange(0, span)\n            self.cut_line.setValue(value)\n\n    def set_cutoff_height(self, height):\n        self.cutoff_height = height\n        if self.root:\n            self.cut_ratio = 100 * height / self.root.value.height\n        self.select_max_height(height)\n\n    def _set_cut_line_visible(self, visible):\n        self.cut_line.setVisible(visible)\n\n    def select_top_n(self, n):\n        root = self._displayed_root\n        if root:\n            clusters = top_clusters(root, n)\n            self.dendrogram.set_selected_clusters(clusters)\n\n    def select_max_height(self, height):\n        root = self._displayed_root\n        if root:\n            clusters = clusters_at_height(root, height)\n            self.dendrogram.set_selected_clusters(clusters)\n\n    def _cut_ratio_changed(self):\n        self.selection_method = 1\n        self._selection_method_changed()\n\n    def _cut_ratio_return(self):\n        if self.selection_method != 1:\n            self.selection_method = 1\n            self._selection_method_changed()\n\n    def _top_n_changed(self):\n        self.selection_method = 2\n        self._selection_method_changed()\n\n    def _top_n_return(self):\n        if self.selection_method != 2:\n            self.selection_method = 2\n            self._selection_method_changed()\n\n    def _selection_method_changed(self):\n        self._set_cut_line_visible(self.selection_method == 1)\n        if self.root:\n            self._apply_selection()\n\n    def _apply_selection(self):\n        if not self.root:\n            return\n\n        if self.selection_method == 0:\n            pass\n        elif self.selection_method == 1:\n            height = self.cut_ratio * self.root.value.height / 100\n            self.set_cutoff_height(height)\n            pos = self.dendrogram.pos_at_height(height)\n            self._set_slider_value(pos.x(), self.dendrogram.size().width())\n        elif self.selection_method == 2:\n            self.select_top_n(self.top_n)\n\n    def _selection_edited(self):\n        # Selection was edited by clicking on a cluster in the\n        # dendrogram view.\n        self.selection_method = 0\n        self._selection_method_changed()\n        self._invalidate_output()\n\n    def _save_selection(self):\n        # Save the current manual node selection state\n        selection_state = None\n        if self.selection_method == 0 and self.root:\n            assert self.linkmatrix is not None\n            linkmat = [(int(_0), int(_1), _2)\n                       for _0, _1, _2 in self.linkmatrix[:, :3].tolist()]\n            nodes_ = self.dendrogram.selected_nodes()\n            # match the display (pruned) nodes back (by ranges)\n            mapping = {node.value.range: node for node in postorder(self.root)}\n            nodes = [mapping[node.value.range] for node in nodes_]\n            indices = [tuple(node.value.index for node in leaves(node))\n                       for node in nodes]\n            if nodes:\n                selection_state = (indices, linkmat)\n        return selection_state\n\n    def save_state(self):\n        # type: () -> Dict[str, Any]\n        \"\"\"\n        Save state for `set_restore_state`\n        \"\"\"\n        selection = self._save_selection()\n        res = {\"version\": (0, 0, 0)}\n        if selection is not None:\n            res[\"selection_state\"] = selection\n        return res\n\n    def set_restore_state(self, state):\n        # type: (Dict[str, Any]) -> bool\n        \"\"\"\n        Restore session data from a saved state.\n\n        Parameters\n        ----------\n        state : Dict[str, Any]\n\n        NOTE\n        ----\n        This is method called while the instance (self) is being constructed,\n        even before its `__init__` is called. Consider `self` to be only a\n        `QObject` at this stage.\n        \"\"\"\n        if \"selection_state\" in state:\n            selection = state[\"selection_state\"]\n            self.__pending_selection_restore = selection\n        return True\n\n    def __zoom_in(self):\n        def clip(minval, maxval, val):\n            return min(max(val, minval), maxval)\n        self.zoom_factor = clip(self.zoom_slider.minimum(),\n                                self.zoom_slider.maximum(),\n                                self.zoom_factor + 1)\n        self.__update_font_scale()\n\n    def __zoom_out(self):\n        def clip(minval, maxval, val):\n            return min(max(val, minval), maxval)\n        self.zoom_factor = clip(self.zoom_slider.minimum(),\n                                self.zoom_slider.maximum(),\n                                self.zoom_factor - 1)\n        self.__update_font_scale()\n\n    def __zoom_reset(self):\n        self.zoom_factor = 0\n        self.__update_font_scale()\n\n    def __update_font_scale(self):\n        font = self.scene.font()\n        factor = (1.25 ** self.zoom_factor)\n        font = qfont_scaled(font, factor)\n        self._main_graphics.setFont(font)\n        self.label_model.setFont(font)\n\n    def send_report(self):\n        annot = self.label_cb.currentText()\n        if isinstance(self.annotation, str):\n            annot = annot.lower()\n        if self.selection_method == 0:\n            sel = \"manual\"\n        elif self.selection_method == 1:\n            sel = \"at {:.1f} of height\".format(self.cut_ratio)\n        else:\n            sel = f\"top {self.top_n} {pl(self.top_n, 'cluster')}\"\n        self.report_items((\n            (\"Linkage\", LINKAGE[self.linkage]),\n            (\"Annotation\", annot),\n            (\"Pruning\",\n             self.pruning != 0 and \"{} levels\".format(self.max_depth)),\n            (\"Selection\", sel),\n        ))\n        self.report_plot()\n\n    @classmethod\n    def migrate_context(cls, context, version):\n        if version < 2:\n            if context.values[\"annotation\"] == \"None\":\n                context.values[\"annotation\"] = None\n\n\ndef qfont_scaled(font, factor):\n    scaled = QFont(font)\n    if font.pointSizeF() != -1:\n        scaled.setPointSizeF(font.pointSizeF() * factor)\n    elif font.pixelSize() != -1:\n        scaled.setPixelSize(int(font.pixelSize() * factor))\n    return scaled\n\n\nclass AxisItem(AxisItem):\n    mousePressed = Signal(QPointF, Qt.MouseButton)\n    mouseMoved = Signal(QPointF, Qt.MouseButtons)\n    mouseReleased = Signal(QPointF, Qt.MouseButton)\n\n    #: \\reimp\n    def wheelEvent(self, event):\n        event.ignore()  # ignore event to propagate to the view -> scroll\n\n    def mousePressEvent(self, event: QGraphicsSceneMouseEvent) -> None:\n        self.mousePressed.emit(event.pos(), event.button())\n        super().mousePressEvent(event)\n        event.accept()\n\n    def mouseMoveEvent(self, event):\n        self.mouseMoved.emit(event.pos(), event.buttons())\n        super().mouseMoveEvent(event)\n        event.accept()\n\n    def mouseReleaseEvent(self, event):\n        self.mouseReleased.emit(event.pos(), event.button())\n        super().mouseReleaseEvent(event)\n        event.accept()\n\n\nclass SliderLine(QGraphicsWidget):\n    \"\"\"A movable slider line.\"\"\"\n    valueChanged = Signal(float)\n\n    linePressed = Signal()\n    lineMoved = Signal()\n    lineReleased = Signal()\n    rangeChanged = Signal(float, float)\n\n    def __init__(self, parent=None, orientation=Qt.Vertical, value=0.0,\n                 length=10.0, **kwargs):\n        self._orientation = orientation\n        self._value = value\n        self._length = length\n        self._min = 0.0\n        self._max = 1.0\n        self._line: Optional[QLineF] = QLineF()\n        self._pen: Optional[QPen] = None\n        super().__init__(parent, **kwargs)\n        self.setAcceptedMouseButtons(Qt.LeftButton)\n        if self._orientation == Qt.Vertical:\n            self.setCursor(Qt.SizeVerCursor)\n        else:\n            self.setCursor(Qt.SizeHorCursor)\n\n    def setPen(self, pen: Union[QPen, Qt.GlobalColor, Qt.PenStyle]) -> None:\n        pen = QPen(pen)\n        if self._pen != pen:\n            self.prepareGeometryChange()\n            self._pen = pen\n            self._line = None\n            self.update()\n\n    def pen(self) -> QPen:\n        if self._pen is None:\n            return QPen(self.palette().text(), 1.0, Qt.DashLine)\n        else:\n            return QPen(self._pen)\n\n    def setValue(self, value: float):\n        value = min(max(value, self._min), self._max)\n\n        if self._value != value:\n            self.prepareGeometryChange()\n            self._value = value\n            self._line = None\n            self.valueChanged.emit(value)\n\n    def value(self) -> float:\n        return self._value\n\n    def setRange(self, minval: float, maxval: float) -> None:\n        maxval = max(minval, maxval)\n        if minval != self._min or maxval != self._max:\n            self._min = minval\n            self._max = maxval\n            self.rangeChanged.emit(minval, maxval)\n            self.setValue(self._value)\n\n    def setLength(self, length: float):\n        if self._length != length:\n            self.prepareGeometryChange()\n            self._length = length\n            self._line = None\n\n    def length(self) -> float:\n        return self._length\n\n    def setOrientation(self, orientation: Qt.Orientation):\n        if self._orientation != orientation:\n            self.prepareGeometryChange()\n            self._orientation = orientation\n            self._line = None\n            if self._orientation == Qt.Vertical:\n                self.setCursor(Qt.SizeVerCursor)\n            else:\n                self.setCursor(Qt.SizeHorCursor)\n\n    def mousePressEvent(self, event: QGraphicsSceneMouseEvent) -> None:\n        event.accept()\n        self.linePressed.emit()\n\n    def mouseMoveEvent(self, event: QGraphicsSceneMouseEvent) -> None:\n        pos = event.pos()\n        if self._orientation == Qt.Vertical:\n            self.setValue(pos.y())\n        else:\n            self.setValue(pos.x())\n        self.lineMoved.emit()\n        event.accept()\n\n    def mouseReleaseEvent(self, event: QGraphicsSceneMouseEvent) -> None:\n        if self._orientation == Qt.Vertical:\n            self.setValue(event.pos().y())\n        else:\n            self.setValue(event.pos().x())\n        self.lineReleased.emit()\n        event.accept()\n\n    def shape(self) -> QPainterPath:\n        path = QPainterPath()\n        path.addRect(self.boundingRect())\n        return path\n\n    def boundingRect(self) -> QRectF:\n        if self._line is None:\n            if self._orientation == Qt.Vertical:\n                self._line = QLineF(0, self._value, self._length, self._value)\n            else:\n                self._line = QLineF(self._value, 0, self._value, self._length)\n        r = QRectF(self._line.p1(), self._line.p2())\n        penw = self.pen().width()\n        return r.adjusted(-penw, -penw, penw, penw)\n\n    def paint(self, painter, *args):\n        if self._line is None:\n            self.boundingRect()\n\n        painter.save()\n        painter.setPen(self.pen())\n        painter.drawLine(self._line)\n        painter.restore()\n\n\ndef clusters_at_height(root, height):\n    \"\"\"Return a list of clusters by cutting the clustering at `height`.\n    \"\"\"\n    lower = set()\n    cluster_list = []\n    for cl in preorder(root):\n        if cl in lower:\n            continue\n        if cl.value.height < height:\n            cluster_list.append(cl)\n            lower.update(preorder(cl))\n    return cluster_list\n\n\ndef main():\n    # pragma: no cover\n    from Orange import distance  # pylint: disable=import-outside-toplevel\n    data = Orange.data.Table(\"iris\")\n    matrix = distance.Euclidean(distance._preprocess(data))\n    subset = data[10:30]\n    WidgetPreview(OWHierarchicalClustering).run(matrix, set_subset=subset)\n\n\nif __name__ == \"__main__\":  # pragma: no cover\n    main()\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:** AttributeError when selecting more than 20 clusters in Hierarchical Clustering widget

**Description:**
When configuring the Hierarchical Clustering widget to select more than 20 clusters, an AttributeError occurs because the `Warning` object does not have a `many_clusters` attribute. This issue prevents the widget from displaying the intended warning message about potential performance degradation when handling a large number of clusters.

**Example Code:**
```python
widget = OWHierarchicalClustering()
widget.top_n = 21
widget.selection_box.buttons[2].click()
# Expected: Warning.many_clusters is shown
# Actual: AttributeError: 'Warning' object has no attribute 'many_clusters'
```

**Expected Behavior:**
Setting the `top_n` parameter to a value greater than 20 should display the `many_clusters` warning, informing users that selecting a large number of clusters may degrade the performance of downstream widgets.

**Actual Behavior:**
An AttributeError is raised stating that the `'Warning' object has no attribute 'many_clusters'`, which prevents the warning from being displayed when more than 20 clusters are selected.
[/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/unsupervised/owhierarchicalclustering.py
from itertools import chain
from contextlib import contextmanager

import typing
from typing import Any, List, Tuple, Dict, Optional, Set, Union

import numpy as np

from AnyQt.QtWidgets import (
    QGraphicsWidget, QGraphicsScene, QGridLayout, QSizePolicy,
    QAction, QComboBox, QGraphicsGridLayout, QGraphicsSceneMouseEvent, QLabel
)
from AnyQt.QtGui import (QPen, QFont, QKeySequence, QPainterPath, QColor,
    QFontMetrics)
from AnyQt.QtCore import (
    Qt, QObject, QSize, QPointF, QRectF, QLineF, QEvent, QModelIndex
)
from AnyQt.QtCore import pyqtSignal as Signal, pyqtSlot as Slot

from Orange.widgets.utils.localization import pl
from orangewidget.utils.itemmodels import PyListModel
from orangewidget.utils.signals import LazyValue

import Orange.data
from Orange.data.domain import filter_visible
from Orange.data import Domain, DiscreteVariable, ContinuousVariable, \
    StringVariable, Table
import Orange.misc
from Orange.clustering.hierarchical import \
    postorder, preorder, Tree, tree_from_linkage, dist_matrix_linkage, \
    leaves, prune, top_clusters
from Orange.data.util import get_unique_names

from Orange.widgets import widget, gui, settings
from Orange.widgets.utils import itemmodels, combobox
from Orange.widgets.utils.annotated_data import (lazy_annotated_table,
                                                 ANNOTATED_DATA_SIGNAL_NAME,
                                                 domain_with_annotation_column,
                                                 add_columns,
                                                 create_annotated_table)
from Orange.widgets.utils.widgetpreview import WidgetPreview
from Orange.widgets.visualize.utils.plotutils import AxisItem
from Orange.widgets.widget import Input, Output, Msg

from Orange.widgets.utils.stickygraphicsview import StickyGraphicsView
from Orange.widgets.utils.graphicsview import GraphicsWidgetView
from Orange.widgets.utils.graphicstextlist import TextListView
from Orange.widgets.utils.dendrogram import DendrogramWidget

__all__ = ["OWHierarchicalClustering"]


LINKAGE = ["Single", "Average", "Weighted", "Complete", "Ward"]
LINKAGE_ARGS = ["single", "average", "weighted", "complete", "ward"]
DEFAULT_LINKAGE = "Ward"


def make_pen(brush=Qt.black, width=1, style=Qt.SolidLine,
             cap_style=Qt.SquareCap, join_style=Qt.BevelJoin,
             cosmetic=False):
    pen = QPen(brush)
    pen.setWidth(width)
    pen.setStyle(style)
    pen.setCapStyle(cap_style)
    pen.setJoinStyle(join_style)
    pen.setCosmetic(cosmetic)
    return pen


@contextmanager
def blocked(obj):
    old = obj.signalsBlocked()
    obj.blockSignals(True)
    try:
        yield obj
    finally:
        obj.blockSignals(old)


class SaveStateSettingsHandler(settings.SettingsHandler):
    """
    A settings handler that delegates session data store/restore to the
    OWWidget instance.

    The OWWidget subclass must implement `save_state() -> Dict[str, Any]` and
    `set_restore_state(state: Dict[str, Any])` methods.
    """
    def initialize(self, instance, data=None):
        super().initialize(instance, data)
        if data is not None and "__session_state_data" in data:
            session_data = data["__session_state_data"]
            instance.set_restore_state(session_data)

    def pack_data(self, widget):
        # type: (widget.OWWidget) -> dict
        res = super().pack_data(widget)
        state = widget.save_state()
        if state:
            assert "__session_state_data" not in res
            res["__session_state_data"] = state
        return res


class _DomainContextHandler(settings.DomainContextHandler,
                            SaveStateSettingsHandler):
    pass


if typing.TYPE_CHECKING:
    #: Encoded selection state for persistent storage.
    #: This is a list of tuples of leaf indices in the selection and
    #: a (N, 3) linkage matrix for validation (the 4-th column from scipy
    #: is omitted).
    SelectionState = Tuple[List[Tuple[int]], List[Tuple[int, int, float]]]


class SelectedLabelsModel(PyListModel):
    def __init__(self):
        super().__init__([])
        self.subset = set()
        self.__font = QFont()
        self.__colors = None

    def rowCount(self, parent=QModelIndex()):
        count = super().rowCount()
        if self.__colors is not None:
            count = max(count, len(self.__colors))
        return count

    def _emit_data_changed(self):
        self.dataChanged.emit(self.index(0, 0), self.index(len(self) - 1, 0))

    def set_subset(self, subset):
        self.subset = set(subset)
        self._emit_data_changed()

    def set_colors(self, colors):
        self.__colors = colors
        self._emit_data_changed()

    def setFont(self, font):
        self.__font = font
        self._emit_data_changed()

    def data(self, index, role=Qt.DisplayRole):
        if role == Qt.FontRole:
            font = QFont(self.__font)
            font.setBold(index.row() in self.subset)
            return font
        if role == Qt.BackgroundRole:
            if self.__colors is not None:
                return self.__colors[index.row()]
            elif not any(self) and self.subset:  # no labels, no color, but subset
                return QColor(0, 0, 0)
        if role == Qt.UserRole and self.subset:
            return index.row() in self.subset

        return super().data(index, role)


class GraphicsView(GraphicsWidgetView, StickyGraphicsView):
    def minimumSizeHint(self) -> QSize:
        msh = super().minimumSizeHint()
        w = self.centralWidget()
        if w is not None:
            width = w.minimumWidth() + 4 + self.verticalScrollBar().width()
            msh.setWidth(max(int(width), msh.width()))
        return msh

    def eventFilter(self, recv: QObject, event: QEvent) -> bool:
        ret = super().eventFilter(recv, event)
        if event.type() == QEvent.LayoutRequest and recv is self.centralWidget():
            self.updateGeometry()
        return ret


class OWHierarchicalClustering(widget.OWWidget):
    name = "Hierarchical Clustering"
    description = "Display a dendrogram of a hierarchical clustering " \
                  "constructed from the input distance matrix."
    icon = "icons/HierarchicalClustering.svg"
    priority = 2100
    keywords = "hierarchical clustering"

    class Inputs:
        distances = Input("Distances", Orange.misc.DistMatrix)
        subset = Input("Data Subset", Orange.data.Table, explicit=True)

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

    settings_version = 2
    settingsHandler = _DomainContextHandler()

    #: Selected linkage
    linkage = settings.Setting(LINKAGE.index(DEFAULT_LINKAGE))
    #: Index of the selected annotation item (variable, ...)
    annotation = settings.ContextSetting("Enumeration")
    #: Out-of-context setting for the case when the "Name" option is available
    annotation_if_names = settings.Setting("Name")
    #: Out-of-context setting for the case with just "Enumeration" and "None"
    annotation_if_enumerate = settings.Setting("Enumeration")
    #: Selected tree pruning (none/max depth)
    pruning = settings.Setting(0)
    #: Maximum depth when max depth pruning is selected
    max_depth = settings.Setting(10)

    #: Selected cluster selection method (none, cut distance, top n)
    selection_method = settings.Setting(0)
    #: Cut height ratio wrt root height
    cut_ratio = settings.Setting(75.0)
    #: Number of top clusters to select
    top_n = settings.Setting(3)
    #: Dendrogram zoom factor
    zoom_factor = settings.Setting(0)
    #: Show labels only for subset (if present)
    label_only_subset = settings.Setting(False)
    #: Color for label decoration
    color_by: Union[DiscreteVariable, ContinuousVariable, None] = \
        settings.ContextSetting(None)

    autocommit = settings.Setting(True)

    graph_name = "scene"  # QGraphicsScene

    basic_annotations = [None, "Enumeration"]

    class Error(widget.OWWidget.Error):
        empty_matrix = Msg("Distance matrix is empty.")
        not_finite_distances = Msg("Some distances are infinite")
        not_symmetric = widget.Msg("Distance matrix is not symmetric.")

    class Warning(widget.OWWidget.Warning):
        subset_on_no_table = \
            Msg("Unused data subset: distances do not refer to data instances")
        subset_not_subset = \
            Msg("Some data from the subset does not appear in distance matrix")
        subset_wrong = \
            Msg("Subset data refers to a different table")
        pruning_disables_colors = \
            Msg("Pruned cluster doesn't show colors and indicate subset")

    #: Stored (manual) selection state (from a saved workflow) to restore.
    __pending_selection_restore = None  # type: Optional[SelectionState]

    def __init__(self):
        super().__init__()

        self.matrix = None
        self.items = None
        self.subset = None
        self.subset_rows = set()
        self.linkmatrix = None
        self.root = None
        self._displayed_root = None
        self.cutoff_height = 0.0

        spin_width = QFontMetrics(self.font()).horizontalAdvance("M" * 7)
        gui.comboBox(
            self.controlArea, self, "linkage", items=LINKAGE, box="Linkage",
            callback=self._invalidate_clustering)

        model = itemmodels.VariableListModel(placeholder="None")
        model[:] = self.basic_annotations

        grid = QGridLayout()
        gui.widgetBox(self.controlArea, "Annotations", orientation=grid)
        self.label_cb = cb = combobox.ComboBoxSearch(
            minimumContentsLength=14,
            sizeAdjustPolicy=QComboBox.AdjustToMinimumContentsLengthWithIcon
        )
        cb.setModel(model)
        cb.setCurrentIndex(cb.findData(self.annotation, Qt.EditRole))

        def on_annotation_activated():
            self.annotation = self.label_cb.currentData(Qt.EditRole)
            self._update_labels()
        cb.activated.connect(on_annotation_activated)

        def on_annotation_changed(value):
            self.label_cb.setCurrentIndex(
                self.label_cb.findData(value, Qt.EditRole))
        self.connect_control("annotation", on_annotation_changed)

        grid.addWidget(self.label_cb, 0, 0, 1, 2)

        cb = gui.checkBox(
            None, self, "label_only_subset", "Show labels only for subset",
            disabled=True,
            callback=self._update_labels, stateWhenDisabled=False)
        grid.addWidget(cb, 1, 0, 1, 2)

        model = itemmodels.DomainModel(
            valid_types=(DiscreteVariable, ContinuousVariable),
            placeholder="None")
        cb = gui.comboBox(
            None, self, "color_by", orientation=Qt.Horizontal,
            model=model, callback=self._update_labels,
            sizePolicy=QSizePolicy(QSizePolicy.MinimumExpanding,
                                   QSizePolicy.Fixed))
        self.color_by_label = QLabel("Color by:")
        grid.addWidget(self.color_by_label, 2, 0)
        grid.addWidget(cb, 2, 1)

        box = gui.radioButtons(
            self.controlArea, self, "pruning", box="Pruning",
            callback=self._invalidate_pruning)
        grid = QGridLayout()
        box.layout().addLayout(grid)
        grid.addWidget(
            gui.appendRadioButton(box, "None", addToLayout=False),
            0, 0
        )
        self.max_depth_spin = gui.spin(
            box, self, "max_depth", minv=1, maxv=100,
            controlWidth=spin_width, alignment=Qt.AlignRight,
            callback=self._max_depth_changed,
            keyboardTracking=False, addToLayout=False
        )
        self.max_depth_spin.lineEdit().returnPressed.connect(
            self._max_depth_return)

        grid.addWidget(
            gui.appendRadioButton(box, "Max depth:", addToLayout=False),
            1, 0)
        grid.addWidget(self.max_depth_spin, 1, 1)

        self.selection_box = gui.radioButtons(
            self.controlArea, self, "selection_method",
            box="Selection",
            callback=self._selection_method_changed)

        grid = QGridLayout()
        self.selection_box.layout().addLayout(grid)
        grid.addWidget(
            gui.appendRadioButton(
                self.selection_box, "Manual", addToLayout=False),
            0, 0
        )
        grid.addWidget(
            gui.appendRadioButton(
                self.selection_box, "Height ratio:", addToLayout=False),
            1, 0
        )
        self.cut_ratio_spin = gui.spin(
            self.selection_box, self, "cut_ratio", 0, 100, step=1e-1,
            controlWidth=spin_width, alignment = Qt.AlignRight,
            spinType=float, callback=self._cut_ratio_changed,
            addToLayout=False
        )
        self.cut_ratio_spin.setSuffix(" %")
        self.cut_ratio_spin.lineEdit().returnPressed.connect(
            self._cut_ratio_return)

        grid.addWidget(self.cut_ratio_spin, 1, 1)

        grid.addWidget(
            gui.appendRadioButton(
                self.selection_box, "Top N:", addToLayout=False),
            2, 0
        )
        self.top_n_spin = gui.spin(
            self.selection_box, self, "top_n", 1, 20,
            controlWidth=spin_width, alignment=Qt.AlignRight,
            callback=self._top_n_changed, addToLayout=False)
        self.top_n_spin.lineEdit().returnPressed.connect(self._top_n_return)
        grid.addWidget(self.top_n_spin, 2, 1)

        self.zoom_slider = gui.hSlider(
            self.controlArea, self, "zoom_factor", box="Zoom",
            minValue=-6, maxValue=3, step=1, ticks=True, createLabel=False,
            callback=self.__update_font_scale)

        zoom_in = QAction(
            "Zoom in", self, shortcut=QKeySequence.ZoomIn,
            triggered=self.__zoom_in
        )
        zoom_out = QAction(
            "Zoom out", self, shortcut=QKeySequence.ZoomOut,
            triggered=self.__zoom_out
        )
        zoom_reset = QAction(
            "Reset zoom", self,
            shortcut=QKeySequence(Qt.ControlModifier | Qt.Key_0),
            triggered=self.__zoom_reset
        )
        self.addActions([zoom_in, zoom_out, zoom_reset])

        self.controlArea.layout().addStretch()

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

        self.scene = QGraphicsScene(self)
        self.view = GraphicsView(
            self.scene,
            horizontalScrollBarPolicy=Qt.ScrollBarAlwaysOff,
            verticalScrollBarPolicy=Qt.ScrollBarAlwaysOn,
            alignment=Qt.AlignLeft | Qt.AlignVCenter,
            widgetResizable=True,
        )
        # Disable conflicting action shortcuts. We define our own.
        for a in self.view.viewActions():
            a.setEnabled(False)
        self.mainArea.layout().setSpacing(1)
        self.mainArea.layout().addWidget(self.view)

        def axis_view(orientation):
            ax = AxisItem(orientation=orientation, maxTickLength=7)
            ax.mousePressed.connect(self._activate_cut_line)
            ax.mouseMoved.connect(self._activate_cut_line)
            ax.mouseReleased.connect(self._activate_cut_line)
            ax.setRange(1.0, 0.0)
            return ax

        self.top_axis = axis_view("top")
        self.bottom_axis = axis_view("bottom")

        self._main_graphics = QGraphicsWidget(
            sizePolicy=QSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.Preferred)
        )
        scenelayout = QGraphicsGridLayout()
        scenelayout.setHorizontalSpacing(10)
        scenelayout.setVerticalSpacing(10)

        self._main_graphics.setLayout(scenelayout)
        self.scene.addItem(self._main_graphics)
        self.view.setCentralWidget(self._main_graphics)
        self.scene.addItem(self._main_graphics)

        self.dendrogram = DendrogramWidget(pen_width=2)
        self.dendrogram.setSizePolicy(QSizePolicy.MinimumExpanding,
                                      QSizePolicy.MinimumExpanding)
        self.dendrogram.selectionChanged.connect(self._invalidate_output)
        self.dendrogram.selectionEdited.connect(self._selection_edited)

        self.labels = TextListView()
        self.label_model = SelectedLabelsModel()
        self.labels.setModel(self.label_model)
        self.labels.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Preferred)
        self.labels.setAlignment(Qt.AlignLeft)
        self.labels.setMaximumWidth(200)

        scenelayout.addItem(self.top_axis, 0, 0,
                            alignment=Qt.AlignLeft | Qt.AlignVCenter)
        scenelayout.addItem(self.dendrogram, 1, 0,
                            alignment=Qt.AlignLeft | Qt.AlignVCenter)
        scenelayout.addItem(self.labels, 1, 1,
                            alignment=Qt.AlignLeft | Qt.AlignVCenter)
        scenelayout.addItem(self.bottom_axis, 2, 0,
                            alignment=Qt.AlignLeft | Qt.AlignVCenter)
        self.top_axis.setZValue(self.dendrogram.zValue() + 10)
        self.bottom_axis.setZValue(self.dendrogram.zValue() + 10)
        self.cut_line = SliderLine(self.top_axis,
                                   orientation=Qt.Horizontal)
        self.cut_line.valueChanged.connect(self._dendrogram_slider_changed)
        self.dendrogram.geometryChanged.connect(self._dendrogram_geom_changed)
        self._set_cut_line_visible(self.selection_method == 1)
        self.__update_font_scale()

    @Inputs.distances
    def set_distances(self, matrix):
        self.error()
        self.Error.clear()

        self.matrix = None
        self.Error.clear()
        if matrix is not None:
            if len(matrix) < 2:
                self.Error.empty_matrix()
            elif not matrix.is_symmetric():
                self.Error.not_symmetric()
            elif not np.all(np.isfinite(matrix)):
                self.Error.not_finite_distances()
            else:
                self.matrix = matrix

    @Inputs.subset
    def set_subset(self, subset):
        self.subset = subset
        self.controls.label_only_subset.setDisabled(subset is None)

    def handleNewSignals(self):
        if self.__pending_selection_restore is not None:
            selection_state = self.__pending_selection_restore
        else:
            # save the current selection to (possibly) restore later
            selection_state = self._save_selection()

        matrix = self.matrix
        if matrix is not None:
            self._set_items(matrix.row_items, matrix.axis)
        else:
            self._set_items(None)
        self._update()

        # Can now attempt to restore session state from a saved workflow.
        if self.root and selection_state is not None:
            self._restore_selection(selection_state)
            self.__pending_selection_restore = None

        self.Warning.clear()
        rows = set()
        if self.subset:
            subsetids = set(self.subset.ids)
            if not isinstance(self.items, Orange.data.Table) \
                    or not self.matrix.axis:
                self.Warning.subset_on_no_table()
            elif (dataids := set(self.items.ids)) and not subsetids & dataids:
                self.Warning.subset_wrong()
            elif not subsetids <= dataids:
                self.Warning.subset_not_subset()
            else:
                indices = [leaf.value.index for leaf in leaves(self.root)]
                rows = {
                    row for row, rowid in enumerate(self.items.ids[indices])
                    if rowid in subsetids
                }

        self.subset_rows = rows
        self._update_labels()
        self.commit.now()

    def _set_items(self, items, axis=1):
        self.closeContext()
        self.items = items
        model = self.label_cb.model()
        color_model = self.controls.color_by.model()
        color_model.set_domain(None)
        self.color_by = None
        if len(model) == 2 and model[0] is None:
            if model[1] == "Name":
                self.annotation_if_names = self.annotation
            if model[1] == self.basic_annotations[1]:
                self.annotation_if_enumerate = self.annotation
        if isinstance(items, Orange.data.Table) and axis:
            metas_class = tuple(
                filter_visible(chain(items.domain.metas,
                                     items.domain.class_vars)))
            visible_attrs = tuple(filter_visible(items.domain.attributes))
            if not (metas_class or visible_attrs):
                model[:] = self.basic_annotations
            else:
                model[:] = (
                    (None, )
                    + metas_class
                    + (model.Separator, ) * bool(metas_class and visible_attrs)
                    + visible_attrs)
            for meta in items.domain.metas:
                if isinstance(meta, StringVariable):
                    self.annotation = meta
                    break
            else:
                if items.domain.class_vars:
                    # No string metas: show class
                    self.annotation = items.domain.class_vars[0]
                else:
                    # No string metas and no class: show the first option
                    # which is not None (in the worst case, Enumeration)
                    self.annotation = model[1]

            color_model.set_domain(items.domain)
            if items.domain.class_vars:
                self.color_by = items.domain.class_vars[0]
            self.openContext(items.domain)
        elif isinstance(items, Orange.data.Table) and not axis \
                or (isinstance(items, list) and \
                    all(isinstance(var, Orange.data.Variable)
                        for var in items)):
            model[:] = (None, "Name")
            self.annotation = self.annotation_if_names
        else:
            model[:] = self.basic_annotations
            self.annotation = self.annotation_if_enumerate

        no_colors = len(color_model) == 1
        self.controls.color_by.setDisabled(no_colors)
        self.color_by_label.setDisabled(no_colors)

    def _clear_plot(self):
        self.dendrogram.set_root(None)
        self.label_model.clear()

    def _set_displayed_root(self, root):
        self._clear_plot()
        self._displayed_root = root
        self.dendrogram.set_root(root)
        self._update_labels()

    def _update(self):
        self._clear_plot()

        distances = self.matrix

        if distances is not None:
            method = LINKAGE_ARGS[self.linkage]
            Z = dist_matrix_linkage(distances, linkage=method)

            tree = tree_from_linkage(Z)
            self.linkmatrix = Z
            self.root = tree

            self.top_axis.setRange(tree.value.height, 0.0)
            self.bottom_axis.setRange(tree.value.height, 0.0)

            if self.pruning:
                self._set_displayed_root(prune(tree, level=self.max_depth))
            else:
                self._set_displayed_root(tree)
        else:
            self.linkmatrix = None
            self.root = None
            self._set_displayed_root(None)

        self._apply_selection()

    def _update_labels(self):
        if not hasattr(self, "label_model"):
            # This method can be called during widget initialization when
            # creating check box for label_only_subset, if it's value is
            # initially True.
            # See https://github.com/biolab/orange-widget-base/pull/213;
            # if it's merged, this check can be removed.
            return

        self.Warning.pruning_disables_colors(
            shown=self.pruning
                  and (self.subset_rows or self.color_by is not None))
        labels = []
        if self.root and self._displayed_root:
            indices = [leaf.value.index for leaf in leaves(self.root)]

            if self.annotation is None:
                if not self.pruning \
                        and self.subset_rows and self.color_by is None:
                    # Model fails if number of labels and of colors mismatch
                    labels = [""] * len(indices)
                else:
                    labels = []
            elif self.annotation == "Enumeration":
                labels = [str(i+1) for i in indices]
            elif self.annotation == "Name":
                attr = self.matrix.row_items.domain.attributes
                labels = [str(attr[i]) for i in indices]
            elif isinstance(self.annotation, Orange.data.Variable):
                col_data = self.items.get_column(self.annotation)
                labels = [self.annotation.str_val(val) for val in col_data]
                labels = [labels[idx] for idx in indices]
            else:
                labels = []
            if not self.pruning and \
                    labels and self.label_only_subset and self.subset_rows:
                labels = [label if row in self.subset_rows else ""
                          for row, label in enumerate(labels)]

            if labels and self._displayed_root is not self.root:
                joined = leaves(self._displayed_root)
                labels = [", ".join(labels[leaf.value.first: leaf.value.last])
                          for leaf in joined]

        self.label_model[:] = labels
        self.label_model.set_subset(set() if self.pruning else self.subset_rows)
        self.labels.setMinimumWidth(1 if labels else -1)

        if not self.pruning and self.color_by is not None:
            col = self.items.get_column(self.color_by)
            self.label_model.set_colors(
                self.color_by.palette.values_to_qcolors(col[indices]))
        else:
            self.label_model.set_colors(None)

    def _restore_selection(self, state):
        # type: (SelectionState) -> bool
        """
        Restore the (manual) node selection state.

        Return True if successful; False otherwise.
        """
        linkmatrix = self.linkmatrix
        if self.selection_method == 0 and self.root:
            selected, linksaved = state
            linkstruct = np.array(linksaved, dtype=float)
            selected = set(selected)  # type: Set[Tuple[int]]
            if not selected:
                return False
            if linkmatrix.shape[0] != linkstruct.shape[0]:
                return False
            # check that the linkage matrix structure matches. Use isclose for
            # the height column to account for inexact floating point math
            # (e.g. summation order in different ?gemm implementations for
            # euclidean distances, ...)
            if np.any(linkstruct[:, :2] != linkmatrix[:, :2]) or \
                    not np.all(np.isclose(linkstruct[:, 2], linkstruct[:, 2])):
                return False
            selection = []
            indices = np.array([n.value.index for n in leaves(self.root)],
                               dtype=int)
            # mapping from ranges to display (pruned) nodes
            mapping = {node.value.range: node
                       for node in postorder(self._displayed_root)}
            for node in postorder(self.root):  # type: Tree
                r = tuple(indices[node.value.first: node.value.last])
                if r in selected:
                    if node.value.range not in mapping:
                        # the node was pruned from display and cannot be
                        # selected
                        break
                    selection.append(mapping[node.value.range])
                    selected.remove(r)
                if not selected:
                    break  # found all, nothing more to do
            if selection and selected:
                # Could not restore all selected nodes (only partial match)
                return False

            self._set_selected_nodes(selection)
            return True
        return False

    def _set_selected_nodes(self, selection):
        # type: (List[Tree]) -> None
        """
        Set the nodes in `selection` to be the current selected nodes.

        The selection nodes must be subtrees of the current `_displayed_root`.
        """
        self.dendrogram.selectionChanged.disconnect(self._invalidate_output)
        try:
            self.dendrogram.set_selected_clusters(selection)
        finally:
            self.dendrogram.selectionChanged.connect(self._invalidate_output)

    def _max_depth_return(self):
        if self.pruning != 1:
            self.pruning = 1
            self._invalidate_pruning()

    def _max_depth_changed(self):
        self.pruning = 1
        self._invalidate_pruning()

    def _invalidate_clustering(self):
        self._update()
        self._update_labels()
        self._invalidate_output()

    def _invalidate_output(self):
        self.commit.deferred()

    def _invalidate_pruning(self):
        if self.root:
            selection = self.dendrogram.selected_nodes()
            ranges = [node.value.range for node in selection]
            if self.pruning:
                self._set_displayed_root(
                    prune(self.root, level=self.max_depth))
            else:
                self._set_displayed_root(self.root)
            selected = [node for node in preorder(self._displayed_root)
                        if node.value.range in ranges]

            self.dendrogram.set_selected_clusters(selected)

        self._apply_selection()

    @gui.deferred
    def commit(self):
        items = getattr(self.matrix, "items", self.items)
        if not items:
            self.Outputs.selected_data.send(None)
            self.Outputs.annotated_data.send(None)
            return

        selection = self.dendrogram.selected_nodes()
        selection = sorted(selection, key=lambda c: c.value.first)

        indices = [leaf.value.index for leaf in leaves(self.root)]

        maps = [indices[node.value.first:node.value.last]
                for node in selection]

        selected_indices = list(chain(*maps))

        if not selected_indices:
            self.Outputs.selected_data.send(None)
            annotated_data = lazy_annotated_table(items, []) \
                if self.selection_method == 0 and self.matrix.axis else None
            self.Outputs.annotated_data.send(annotated_data)
            return

        selected_data = annotated_data = None

        if isinstance(items, Orange.data.Table) and self.matrix.axis == 1:
            # Select rows
            data, domain = items, items.domain

            c = np.full(self.matrix.shape[0], len(maps))
            for i, indices in enumerate(maps):
                c[indices] = i

            clust_name = get_unique_names(domain, "Cluster")
            values = [f"C{i + 1}" for i in range(len(maps))]

            sel_clust_var = Orange.data.DiscreteVariable(
                name=clust_name, values=values)
            sel_domain = add_columns(domain, metas=(sel_clust_var,))
            selected_data = LazyValue[Table](
                lambda: items.add_column(
                    sel_clust_var, c, to_metas=True)[c != len(maps)],
                domain=sel_domain, length=len(selected_indices))

            ann_clust_var = Orange.data.DiscreteVariable(
                name=clust_name, values=values + ["Other"]
            )
            ann_domain = add_columns(
                domain_with_annotation_column(data)[0], metas=(ann_clust_var, ))
            annotated_data = LazyValue[Table](
                lambda: create_annotated_table(
                    data=items.add_column(ann_clust_var, c, to_metas=True),
                    selected_indices=selected_indices),
                domain=ann_domain, length=len(items)
            )

        elif isinstance(items, Orange.data.Table) and self.matrix.axis == 0:
            # Select columns
            attrs = []
            unselected_indices = sorted(set(range(self.root.value.last)) -
                                        set(selected_indices))
            for clust, indices in chain(enumerate(maps, start=1),
                                        [(0, unselected_indices)]):
                for i in indices:
                    attr = items.domain[i].copy()
                    attr.attributes["cluster"] = clust
                    attrs.append(attr)
            all_domain = Orange.data.Domain(
                # len(unselected_indices) can be 0
                attrs[:len(attrs) - len(unselected_indices)],
                items.domain.class_vars, items.domain.metas)

            selected_data = LazyValue[Table](
                lambda: items.from_table(all_domain, items),
                domain=all_domain, length=len(items))

            sel_domain = Orange.data.Domain(
                attrs,
                items.domain.class_vars, items.domain.metas)
            annotated_data = LazyValue[Table](
                lambda: items.from_table(sel_domain, items),
                domain=sel_domain, length=len(items))

        self.Outputs.selected_data.send(selected_data)
        self.Outputs.annotated_data.send(annotated_data)

    @Slot(QPointF)
    def _activate_cut_line(self, pos: QPointF):
        """Activate cut line selection an set cut value to `pos.x()`."""
        self.selection_method = 1
        self.cut_line.setValue(pos.x())
        self._selection_method_changed()

    def onDeleteWidget(self):
        super().onDeleteWidget()
        self._clear_plot()
        self.dendrogram.clear()
        self.dendrogram.deleteLater()

    def _dendrogram_geom_changed(self):
        pos = self.dendrogram.pos_at_height(self.cutoff_height)
        geom = self.dendrogram.geometry()
        self._set_slider_value(pos.x(), geom.width())

        self.cut_line.setLength(
            self.bottom_axis.geometry().bottom()
            - self.top_axis.geometry().top()
        )

        geom = self._main_graphics.geometry()
        assert geom.topLeft() == QPointF(0, 0)

        def adjustLeft(rect):
            rect = QRectF(rect)
            rect.setLeft(geom.left())
            return rect
        margin = 3
        self.scene.setSceneRect(geom)
        self.view.setSceneRect(geom)
        self.view.setHeaderSceneRect(
            adjustLeft(self.top_axis.geometry()).adjusted(0, 0, 0, margin)
        )
        self.view.setFooterSceneRect(
            adjustLeft(self.bottom_axis.geometry()).adjusted(0, -margin, 0, 0)
        )

    def _dendrogram_slider_changed(self, value):
        p = QPointF(value, 0)
        cl_height = self.dendrogram.height_at(p)

        self.set_cutoff_height(cl_height)

    def _set_slider_value(self, value, span):
        with blocked(self.cut_line):
            self.cut_line.setRange(0, span)
            self.cut_line.setValue(value)

    def set_cutoff_height(self, height):
        self.cutoff_height = height
        if self.root:
            self.cut_ratio = 100 * height / self.root.value.height
        self.select_max_height(height)

    def _set_cut_line_visible(self, visible):
        self.cut_line.setVisible(visible)

    def select_top_n(self, n):
        root = self._displayed_root
        if root:
            clusters = top_clusters(root, n)
            self.dendrogram.set_selected_clusters(clusters)

    def select_max_height(self, height):
        root = self._displayed_root
        if root:
            clusters = clusters_at_height(root, height)
            self.dendrogram.set_selected_clusters(clusters)

    def _cut_ratio_changed(self):
        self.selection_method = 1
        self._selection_method_changed()

    def _cut_ratio_return(self):
        if self.selection_method != 1:
            self.selection_method = 1
            self._selection_method_changed()

    def _top_n_changed(self):
        self.selection_method = 2
        self._selection_method_changed()

    def _top_n_return(self):
        if self.selection_method != 2:
            self.selection_method = 2
            self._selection_method_changed()

    def _selection_method_changed(self):
        self._set_cut_line_visible(self.selection_method == 1)
        if self.root:
            self._apply_selection()

    def _apply_selection(self):
        if not self.root:
            return

        if self.selection_method == 0:
            pass
        elif self.selection_method == 1:
            height = self.cut_ratio * self.root.value.height / 100
            self.set_cutoff_height(height)
            pos = self.dendrogram.pos_at_height(height)
            self._set_slider_value(pos.x(), self.dendrogram.size().width())
        elif self.selection_method == 2:
            self.select_top_n(self.top_n)

    def _selection_edited(self):
        # Selection was edited by clicking on a cluster in the
        # dendrogram view.
        self.selection_method = 0
        self._selection_method_changed()
        self._invalidate_output()

    def _save_selection(self):
        # Save the current manual node selection state
        selection_state = None
        if self.selection_method == 0 and self.root:
            assert self.linkmatrix is not None
            linkmat = [(int(_0), int(_1), _2)
                       for _0, _1, _2 in self.linkmatrix[:, :3].tolist()]
            nodes_ = self.dendrogram.selected_nodes()
            # match the display (pruned) nodes back (by ranges)
            mapping = {node.value.range: node for node in postorder(self.root)}
            nodes = [mapping[node.value.range] for node in nodes_]
            indices = [tuple(node.value.index for node in leaves(node))
                       for node in nodes]
            if nodes:
                selection_state = (indices, linkmat)
        return selection_state

    def save_state(self):
        # type: () -> Dict[str, Any]
        """
        Save state for `set_restore_state`
        """
        selection = self._save_selection()
        res = {"version": (0, 0, 0)}
        if selection is not None:
            res["selection_state"] = selection
        return res

    def set_restore_state(self, state):
        # type: (Dict[str, Any]) -> bool
        """
        Restore session data from a saved state.

        Parameters
        ----------
        state : Dict[str, Any]

        NOTE
        ----
        This is method called while the instance (self) is being constructed,
        even before its `__init__` is called. Consider `self` to be only a
        `QObject` at this stage.
        """
        if "selection_state" in state:
            selection = state["selection_state"]
            self.__pending_selection_restore = selection
        return True

    def __zoom_in(self):
        def clip(minval, maxval, val):
            return min(max(val, minval), maxval)
        self.zoom_factor = clip(self.zoom_slider.minimum(),
                                self.zoom_slider.maximum(),
                                self.zoom_factor + 1)
        self.__update_font_scale()

    def __zoom_out(self):
        def clip(minval, maxval, val):
            return min(max(val, minval), maxval)
        self.zoom_factor = clip(self.zoom_slider.minimum(),
                                self.zoom_slider.maximum(),
                                self.zoom_factor - 1)
        self.__update_font_scale()

    def __zoom_reset(self):
        self.zoom_factor = 0
        self.__update_font_scale()

    def __update_font_scale(self):
        font = self.scene.font()
        factor = (1.25 ** self.zoom_factor)
        font = qfont_scaled(font, factor)
        self._main_graphics.setFont(font)
        self.label_model.setFont(font)

    def send_report(self):
        annot = self.label_cb.currentText()
        if isinstance(self.annotation, str):
            annot = annot.lower()
        if self.selection_method == 0:
            sel = "manual"
        elif self.selection_method == 1:
            sel = "at {:.1f} of height".format(self.cut_ratio)
        else:
            sel = f"top {self.top_n} {pl(self.top_n, 'cluster')}"
        self.report_items((
            ("Linkage", LINKAGE[self.linkage]),
            ("Annotation", annot),
            ("Pruning",
             self.pruning != 0 and "{} levels".format(self.max_depth)),
            ("Selection", sel),
        ))
        self.report_plot()

    @classmethod
    def migrate_context(cls, context, version):
        if version < 2:
            if context.values["annotation"] == "None":
                context.values["annotation"] = None


def qfont_scaled(font, factor):
    scaled = QFont(font)
    if font.pointSizeF() != -1:
        scaled.setPointSizeF(font.pointSizeF() * factor)
    elif font.pixelSize() != -1:
        scaled.setPixelSize(int(font.pixelSize() * factor))
    return scaled


class AxisItem(AxisItem):
    mousePressed = Signal(QPointF, Qt.MouseButton)
    mouseMoved = Signal(QPointF, Qt.MouseButtons)
    mouseReleased = Signal(QPointF, Qt.MouseButton)

    #: \reimp
    def wheelEvent(self, event):
        event.ignore()  # ignore event to propagate to the view -> scroll

    def mousePressEvent(self, event: QGraphicsSceneMouseEvent) -> None:
        self.mousePressed.emit(event.pos(), event.button())
        super().mousePressEvent(event)
        event.accept()

    def mouseMoveEvent(self, event):
        self.mouseMoved.emit(event.pos(), event.buttons())
        super().mouseMoveEvent(event)
        event.accept()

    def mouseReleaseEvent(self, event):
        self.mouseReleased.emit(event.pos(), event.button())
        super().mouseReleaseEvent(event)
        event.accept()


class SliderLine(QGraphicsWidget):
    """A movable slider line."""
    valueChanged = Signal(float)

    linePressed = Signal()
    lineMoved = Signal()
    lineReleased = Signal()
    rangeChanged = Signal(float, float)

    def __init__(self, parent=None, orientation=Qt.Vertical, value=0.0,
                 length=10.0, **kwargs):
        self._orientation = orientation
        self._value = value
        self._length = length
        self._min = 0.0
        self._max = 1.0
        self._line: Optional[QLineF] = QLineF()
        self._pen: Optional[QPen] = None
        super().__init__(parent, **kwargs)
        self.setAcceptedMouseButtons(Qt.LeftButton)
        if self._orientation == Qt.Vertical:
            self.setCursor(Qt.SizeVerCursor)
        else:
            self.setCursor(Qt.SizeHorCursor)

    def setPen(self, pen: Union[QPen, Qt.GlobalColor, Qt.PenStyle]) -> None:
        pen = QPen(pen)
        if self._pen != pen:
            self.prepareGeometryChange()
            self._pen = pen
            self._line = None
            self.update()

    def pen(self) -> QPen:
        if self._pen is None:
            return QPen(self.palette().text(), 1.0, Qt.DashLine)
        else:
            return QPen(self._pen)

    def setValue(self, value: float):
        value = min(max(value, self._min), self._max)

        if self._value != value:
            self.prepareGeometryChange()
            self._value = value
            self._line = None
            self.valueChanged.emit(value)

    def value(self) -> float:
        return self._value

    def setRange(self, minval: float, maxval: float) -> None:
        maxval = max(minval, maxval)
        if minval != self._min or maxval != self._max:
            self._min = minval
            self._max = maxval
            self.rangeChanged.emit(minval, maxval)
            self.setValue(self._value)

    def setLength(self, length: float):
        if self._length != length:
            self.prepareGeometryChange()
            self._length = length
            self._line = None

    def length(self) -> float:
        return self._length

    def setOrientation(self, orientation: Qt.Orientation):
        if self._orientation != orientation:
            self.prepareGeometryChange()
            self._orientation = orientation
            self._line = None
            if self._orientation == Qt.Vertical:
                self.setCursor(Qt.SizeVerCursor)
            else:
                self.setCursor(Qt.SizeHorCursor)

    def mousePressEvent(self, event: QGraphicsSceneMouseEvent) -> None:
        event.accept()
        self.linePressed.emit()

    def mouseMoveEvent(self, event: QGraphicsSceneMouseEvent) -> None:
        pos = event.pos()
        if self._orientation == Qt.Vertical:
            self.setValue(pos.y())
        else:
            self.setValue(pos.x())
        self.lineMoved.emit()
        event.accept()

    def mouseReleaseEvent(self, event: QGraphicsSceneMouseEvent) -> None:
        if self._orientation == Qt.Vertical:
            self.setValue(event.pos().y())
        else:
            self.setValue(event.pos().x())
        self.lineReleased.emit()
        event.accept()

    def shape(self) -> QPainterPath:
        path = QPainterPath()
        path.addRect(self.boundingRect())
        return path

    def boundingRect(self) -> QRectF:
        if self._line is None:
            if self._orientation == Qt.Vertical:
                self._line = QLineF(0, self._value, self._length, self._value)
            else:
                self._line = QLineF(self._value, 0, self._value, self._length)
        r = QRectF(self._line.p1(), self._line.p2())
        penw = self.pen().width()
        return r.adjusted(-penw, -penw, penw, penw)

    def paint(self, painter, *args):
        if self._line is None:
            self.boundingRect()

        painter.save()
        painter.setPen(self.pen())
        painter.drawLine(self._line)
        painter.restore()


def clusters_at_height(root, height):
    """Return a list of clusters by cutting the clustering at `height`.
    """
    lower = set()
    cluster_list = []
    for cl in preorder(root):
        if cl in lower:
            continue
        if cl.value.height < height:
            cluster_list.append(cl)
            lower.update(preorder(cl))
    return cluster_list


def main():
    # pragma: no cover
    from Orange import distance  # pylint: disable=import-outside-toplevel
    data = Orange.data.Table("iris")
    matrix = distance.Euclidean(distance._preprocess(data))
    subset = data[10:30]
    WidgetPreview(OWHierarchicalClustering).run(matrix, set_subset=subset)


if __name__ == "__main__":  # pragma: no cover
    main()

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