{
    "file_diffs": [
        {
            "old_file_content": "import enum\nfrom itertools import islice\nfrom typing import (\n    Iterable, Mapping, Any, TypeVar, Type, NamedTuple, Sequence, Optional,\n    Union, Tuple, List, Callable\n)\n\nimport numpy as np\nimport scipy.sparse as sp\n\nfrom AnyQt.QtWidgets import (\n    QGraphicsScene, QGraphicsView, QFormLayout, QComboBox, QGroupBox,\n    QMenu, QAction, QSizePolicy\n)\nfrom AnyQt.QtGui import QStandardItemModel, QStandardItem, QFont, QKeySequence\nfrom AnyQt.QtCore import Qt, QSize, QRectF, QObject\n\nfrom orangewidget.utils.combobox import ComboBox, ComboBoxSearch\nfrom Orange.data import Domain, Table, Variable, DiscreteVariable\nfrom Orange.data.sql.table import SqlTable\nimport Orange.distance\n\nfrom Orange.clustering import hierarchical, kmeans\nfrom Orange.widgets.utils import colorpalettes, apply_all\nfrom Orange.widgets.utils.itemmodels import DomainModel\nfrom Orange.widgets.utils.stickygraphicsview import StickyGraphicsView\nfrom Orange.widgets.utils.graphicsview import GraphicsWidgetView\nfrom Orange.widgets.utils.colorpalettes import DiscretePalette, Palette\n\nfrom Orange.widgets.utils.annotated_data import (create_annotated_table,\n                                                 ANNOTATED_DATA_SIGNAL_NAME)\nfrom Orange.widgets import widget, gui, settings\nfrom Orange.widgets.widget import Msg, Input, Output\n\nfrom Orange.widgets.data.oweditdomain import table_column_data\nfrom Orange.widgets.visualize.utils.heatmap import HeatmapGridWidget, \\\n    ColorMap, CategoricalColorMap, GradientColorMap\nfrom Orange.widgets.utils.widgetpreview import WidgetPreview\nfrom Orange.widgets.utils.state_summary import format_summary_details\n\n\n__all__ = []\n\n\ndef kmeans_compress(X, k=50):\n    km = kmeans.KMeans(n_clusters=k, n_init=5, random_state=42)\n    return km.get_model(X)\n\n\ndef cbselect(cb: QComboBox, value, role: Qt.ItemDataRole = Qt.EditRole) -> None:\n    \"\"\"\n    Find and select the `value` in the `cb` QComboBox.\n\n    Parameters\n    ----------\n    cb: QComboBox\n    value: Any\n    role: Qt.ItemDataRole\n        The data role in the combo box model to match value against\n    \"\"\"\n    cb.setCurrentIndex(cb.findData(value, role))\n\n\nclass Clustering(enum.IntEnum):\n    #: No clustering\n    None_ = 0\n    #: Hierarchical clustering\n    Clustering = 1\n    #: Hierarchical clustering with optimal leaf ordering\n    OrderedClustering = 2\n\n\nClusteringRole = Qt.UserRole + 13\n#: Item data for clustering method selection models\nClusteringModelData = [\n    {\n        Qt.DisplayRole: \"None\",\n        Qt.ToolTipRole: \"No clustering\",\n        ClusteringRole: Clustering.None_,\n    }, {\n        Qt.DisplayRole: \"Clustering\",\n        Qt.ToolTipRole: \"Apply hierarchical clustering\",\n        ClusteringRole: Clustering.Clustering,\n    }, {\n        Qt.DisplayRole: \"Clustering (opt. ordering)\",\n        Qt.ToolTipRole: \"Apply hierarchical clustering with optimal leaf \"\n                        \"ordering.\",\n        ClusteringRole: Clustering.OrderedClustering,\n    }\n]\n\nColumnLabelsPosData = [\n    {Qt.DisplayRole: name, Qt.UserRole: value}\n    for name, value in [\n        (\"None\", HeatmapGridWidget.NoPosition),\n        (\"Top\", HeatmapGridWidget.PositionTop),\n        (\"Bottom\", HeatmapGridWidget.PositionBottom),\n        (\"Top and Bottom\", (HeatmapGridWidget.PositionTop |\n                            HeatmapGridWidget.PositionBottom)),\n    ]\n]\n\n\ndef create_list_model(\n        items: Iterable[Mapping[Qt.ItemDataRole, Any]],\n        parent: Optional[QObject] = None,\n) -> QStandardItemModel:\n    \"\"\"Create list model from an item date iterable.\"\"\"\n    model = QStandardItemModel(parent)\n    for item in items:\n        sitem = QStandardItem()\n        for role, value in item.items():\n            sitem.setData(value, role)\n        model.appendRow([sitem])\n    return model\n\n\nE = TypeVar(\"E\", bound=enum.Enum)  # pylint: disable=invalid-name\n\n\ndef enum_get(etype: Type[E], name: str, default: E) -> E:\n    \"\"\"\n    Return an Enum member by `name`. If no such member exists in `etype`\n    return `default`.\n    \"\"\"\n    try:\n        return etype[name]\n    except LookupError:\n        return default\n\n\nclass OWHeatMap(widget.OWWidget):\n    name = \"Heat Map\"\n    description = \"Plot a data matrix heatmap.\"\n    icon = \"icons/Heatmap.svg\"\n    priority = 260\n    keywords = []\n\n    class Inputs:\n        data = Input(\"Data\", Table)\n\n    class Outputs:\n        selected_data = Output(\"Selected Data\", Table, default=True)\n        annotated_data = Output(ANNOTATED_DATA_SIGNAL_NAME, Table)\n\n    settings_version = 3\n\n    settingsHandler = settings.DomainContextHandler()\n\n    # Disable clustering for inputs bigger than this\n    MaxClustering = 25000\n    # Disable cluster leaf ordering for inputs bigger than this\n    MaxOrderedClustering = 1000\n\n    threshold_low = settings.Setting(0.0)\n    threshold_high = settings.Setting(1.0)\n\n    merge_kmeans = settings.Setting(False)\n    merge_kmeans_k = settings.Setting(50)\n\n    # Display column with averages\n    averages: bool = settings.Setting(True)\n    # Display legend\n    legend: bool = settings.Setting(True)\n    # Annotations\n    #: text row annotation (row names)\n    annotation_var = settings.ContextSetting(None)\n    #: color row annotation\n    annotation_color_var = settings.ContextSetting(None)\n    # Discrete variable used to split that data/heatmaps (vertically)\n    split_by_var = settings.ContextSetting(None)\n    # Selected row/column clustering method (name)\n    col_clustering_method: str = settings.Setting(Clustering.None_.name)\n    row_clustering_method: str = settings.Setting(Clustering.None_.name)\n\n    palette_name = settings.Setting(colorpalettes.DefaultContinuousPaletteName)\n    column_label_pos: int = settings.Setting(1)\n    selected_rows: List[int] = settings.Setting(None, schema_only=True)\n\n    auto_commit = settings.Setting(True)\n\n    graph_name = \"scene\"\n\n    left_side_scrolling = True\n\n    class Information(widget.OWWidget.Information):\n        sampled = Msg(\"Data has been sampled\")\n        discrete_ignored = Msg(\"{} categorical feature{} ignored\")\n        row_clust = Msg(\"{}\")\n        col_clust = Msg(\"{}\")\n        sparse_densified = Msg(\"Showing this data may require a lot of memory\")\n\n    class Error(widget.OWWidget.Error):\n        no_continuous = Msg(\"No numeric features\")\n        not_enough_features = Msg(\"Not enough features for column clustering\")\n        not_enough_instances = Msg(\"Not enough instances for clustering\")\n        not_enough_instances_k_means = Msg(\n            \"Not enough instances for k-means merging\")\n        not_enough_memory = Msg(\"Not enough memory to show this data\")\n\n    class Warning(widget.OWWidget.Warning):\n        empty_clusters = Msg(\"Empty clusters were removed\")\n        missing_split_values = Msg(\n            \"Instances with undefined split variable values are not shown\")\n\n    def __init__(self):\n        super().__init__()\n        self.__pending_selection = self.selected_rows\n\n        # A kingdom for a save_state/restore_state\n        self.col_clustering = enum_get(\n            Clustering, self.col_clustering_method, Clustering.None_)\n        self.row_clustering = enum_get(\n            Clustering, self.row_clustering_method, Clustering.None_)\n\n        @self.settingsAboutToBePacked.connect\n        def _():\n            self.col_clustering_method = self.col_clustering.name\n            self.row_clustering_method = self.row_clustering.name\n\n        self.keep_aspect = False\n\n        #: The original data with all features (retained to\n        #: preserve the domain on the output)\n        self.input_data = None\n        #: The effective data striped of discrete features, and often\n        #: merged using k-means\n        self.data = None\n        self.effective_data = None\n        #: kmeans model used to merge rows of input_data\n        self.kmeans_model = None\n        #: merge indices derived from kmeans\n        #: a list (len==k) of int ndarray where the i-th item contains\n        #: the indices which merge the input_data into the heatmap row i\n        self.merge_indices = None\n        self.parts: Optional[Parts] = None\n        self.__rows_cache = {}\n        self.__columns_cache = {}\n\n        self.info.set_input_summary(self.info.NoInput)\n        self.info.set_output_summary(self.info.NoOutput)\n\n        # GUI definition\n        colorbox = gui.vBox(self.controlArea, \"Color\")\n        self.color_cb = gui.palette_combo_box(self.palette_name)\n        self.color_cb.currentIndexChanged.connect(self.update_color_schema)\n        colorbox.layout().addWidget(self.color_cb)\n\n        form = QFormLayout(\n            formAlignment=Qt.AlignLeft,\n            labelAlignment=Qt.AlignLeft,\n            fieldGrowthPolicy=QFormLayout.AllNonFixedFieldsGrow\n        )\n        lowslider = gui.hSlider(\n            colorbox, self, \"threshold_low\", minValue=0.0, maxValue=1.0,\n            step=0.05, ticks=True, intOnly=False,\n            createLabel=False, callback=self.update_lowslider)\n        highslider = gui.hSlider(\n            colorbox, self, \"threshold_high\", minValue=0.0, maxValue=1.0,\n            step=0.05, ticks=True, intOnly=False,\n            createLabel=False, callback=self.update_highslider)\n\n        form.addRow(\"Low:\", lowslider)\n        form.addRow(\"High:\", highslider)\n\n        colorbox.layout().addLayout(form)\n\n        mergebox = gui.vBox(self.controlArea, \"Merge\",)\n        gui.checkBox(mergebox, self, \"merge_kmeans\", \"Merge by k-means\",\n                     callback=self.__update_row_clustering)\n        ibox = gui.indentedBox(mergebox)\n        gui.spin(ibox, self, \"merge_kmeans_k\", minv=5, maxv=500,\n                 label=\"Clusters:\", keyboardTracking=False,\n                 callbackOnReturn=True, callback=self.update_merge)\n\n        cluster_box = gui.vBox(self.controlArea, \"Clustering\")\n        # Row clustering\n        self.row_cluster_cb = cb = ComboBox()\n        cb.setModel(create_list_model(ClusteringModelData, self))\n        cbselect(cb, self.row_clustering, ClusteringRole)\n        self.connect_control(\n            \"row_clustering\",\n            lambda value, cb=cb: cbselect(cb, value, ClusteringRole)\n        )\n        @cb.activated.connect\n        def _(idx, cb=cb):\n            self.set_row_clustering(cb.itemData(idx, ClusteringRole))\n\n        # Column clustering\n        self.col_cluster_cb = cb = ComboBox()\n        cb.setModel(create_list_model(ClusteringModelData, self))\n        cbselect(cb, self.col_clustering, ClusteringRole)\n        self.connect_control(\n            \"col_clustering\",\n            lambda value, cb=cb: cbselect(cb, value, ClusteringRole)\n        )\n        @cb.activated.connect\n        def _(idx, cb=cb):\n            self.set_col_clustering(cb.itemData(idx, ClusteringRole))\n\n        form = QFormLayout(\n            labelAlignment=Qt.AlignLeft, formAlignment=Qt.AlignLeft,\n            fieldGrowthPolicy=QFormLayout.AllNonFixedFieldsGrow,\n        )\n        form.addRow(\"Rows:\", self.row_cluster_cb)\n        form.addRow(\"Columns:\", self.col_cluster_cb)\n        cluster_box.layout().addLayout(form)\n        box = gui.vBox(self.controlArea, \"Split By\")\n\n        self.row_split_model = DomainModel(\n            placeholder=\"(None)\",\n            valid_types=(Orange.data.DiscreteVariable,),\n            parent=self,\n        )\n        self.row_split_cb = cb = ComboBoxSearch(\n            enabled=not self.merge_kmeans,\n            sizeAdjustPolicy=ComboBox.AdjustToMinimumContentsLengthWithIcon,\n            minimumContentsLength=14,\n            toolTip=\"Split the heatmap vertically by a categorical column\"\n        )\n        self.row_split_cb.setModel(self.row_split_model)\n        self.connect_control(\n            \"split_by_var\", lambda value, cb=cb: cbselect(cb, value)\n        )\n        self.connect_control(\n            \"merge_kmeans\", self.row_split_cb.setDisabled\n        )\n        self.split_by_var = None\n\n        self.row_split_cb.activated.connect(\n            self.__on_split_rows_activated\n        )\n        box.layout().addWidget(self.row_split_cb)\n\n        box = gui.vBox(self.controlArea, 'Annotation && Legends')\n\n        gui.checkBox(box, self, 'legend', 'Show legend',\n                     callback=self.update_legend)\n\n        gui.checkBox(box, self, 'averages', 'Stripes with averages',\n                     callback=self.update_averages_stripe)\n        annotbox = QGroupBox(\"Row Annotations\", flat=True)\n        form = QFormLayout(\n            annotbox,\n            formAlignment=Qt.AlignLeft,\n            labelAlignment=Qt.AlignLeft,\n            fieldGrowthPolicy=QFormLayout.AllNonFixedFieldsGrow\n        )\n        self.annotation_model = DomainModel(placeholder=\"(None)\")\n        self.annotation_text_cb = ComboBoxSearch(\n            minimumContentsLength=12,\n            sizeAdjustPolicy=QComboBox.AdjustToMinimumContentsLength\n        )\n        self.annotation_text_cb.setModel(self.annotation_model)\n        self.annotation_text_cb.activated.connect(self.set_annotation_var)\n        self.connect_control(\"annotation_var\", self.annotation_var_changed)\n\n        self.row_side_color_model = DomainModel(\n            order=(DomainModel.CLASSES, DomainModel.Separator,\n                   DomainModel.METAS),\n            placeholder=\"(None)\", valid_types=DomainModel.PRIMITIVE,\n            flags=Qt.ItemIsSelectable | Qt.ItemIsEnabled,\n            parent=self,\n        )\n        self.row_side_color_cb = ComboBoxSearch(\n            sizeAdjustPolicy=QComboBox.AdjustToMinimumContentsLength,\n            minimumContentsLength=12\n        )\n        self.row_side_color_cb.setModel(self.row_side_color_model)\n        self.row_side_color_cb.activated.connect(self.set_annotation_color_var)\n        self.connect_control(\"annotation_color_var\", self.annotation_color_var_changed)\n        form.addRow(\"Text\", self.annotation_text_cb)\n        form.addRow(\"Color\", self.row_side_color_cb)\n        box.layout().addWidget(annotbox)\n        posbox = gui.vBox(box, \"Column Labels Position\", addSpace=False)\n        posbox.setFlat(True)\n        cb = gui.comboBox(\n            posbox, self, \"column_label_pos\",\n            callback=self.update_column_annotations)\n        cb.setModel(create_list_model(ColumnLabelsPosData, parent=self))\n        cb.setCurrentIndex(self.column_label_pos)\n        gui.checkBox(self.controlArea, self, \"keep_aspect\",\n                     \"Keep aspect ratio\", box=\"Resize\",\n                     callback=self.__aspect_mode_changed)\n\n        gui.rubber(self.controlArea)\n        gui.auto_send(self.controlArea, self, \"auto_commit\")\n\n        # Scene with heatmap\n        class HeatmapScene(QGraphicsScene):\n            widget: Optional[HeatmapGridWidget] = None\n\n        self.scene = self.scene = HeatmapScene(parent=self)\n        self.view = GraphicsView(\n            self.scene,\n            verticalScrollBarPolicy=Qt.ScrollBarAlwaysOn,\n            horizontalScrollBarPolicy=Qt.ScrollBarAlwaysOn,\n            viewportUpdateMode=QGraphicsView.FullViewportUpdate,\n            widgetResizable=True,\n        )\n        self.view.setContextMenuPolicy(Qt.CustomContextMenu)\n        self.view.customContextMenuRequested.connect(\n            self._on_view_context_menu\n        )\n        self.mainArea.layout().addWidget(self.view)\n        self.selected_rows = []\n        self.__font_inc = QAction(\n            \"Increase Font\", self, shortcut=QKeySequence(\"ctrl+>\"))\n        self.__font_dec = QAction(\n            \"Decrease Font\", self, shortcut=QKeySequence(\"ctrl+<\"))\n        self.__font_inc.triggered.connect(lambda: self.__adjust_font_size(1))\n        self.__font_dec.triggered.connect(lambda: self.__adjust_font_size(-1))\n        if hasattr(QAction, \"setShortcutVisibleInContextMenu\"):\n            apply_all(\n                [self.__font_inc, self.__font_dec],\n                lambda a: a.setShortcutVisibleInContextMenu(True)\n            )\n        self.addActions([self.__font_inc, self.__font_dec])\n\n    @property\n    def center_palette(self):\n        palette = self.color_cb.currentData()\n        return bool(palette.flags & palette.Diverging)\n\n    @property\n    def _column_label_pos(self) -> HeatmapGridWidget.Position:\n        return ColumnLabelsPosData[self.column_label_pos][Qt.UserRole]\n\n    def annotation_color_var_changed(self, value):\n        cbselect(self.row_side_color_cb, value, Qt.EditRole)\n\n    def annotation_var_changed(self, value):\n        cbselect(self.annotation_text_cb, value, Qt.EditRole)\n\n    def set_row_clustering(self, method: Clustering) -> None:\n        assert isinstance(method, Clustering)\n        if self.row_clustering != method:\n            self.row_clustering = method\n            cbselect(self.row_cluster_cb, method, ClusteringRole)\n            self.__update_row_clustering()\n\n    def set_col_clustering(self, method: Clustering) -> None:\n        assert isinstance(method, Clustering)\n        if self.col_clustering != method:\n            self.col_clustering = method\n            cbselect(self.col_cluster_cb, method, ClusteringRole)\n            self.__update_column_clustering()\n\n    def sizeHint(self) -> QSize:\n        return super().sizeHint().expandedTo(QSize(900, 700))\n\n    def color_palette(self):\n        return self.color_cb.currentData().lookup_table()\n\n    def color_map(self) -> GradientColorMap:\n        return GradientColorMap(\n            self.color_palette(), (self.threshold_low, self.threshold_high),\n            0 if self.center_palette else None\n        )\n\n    def clear(self):\n        self.data = None\n        self.input_data = None\n        self.effective_data = None\n        self.kmeans_model = None\n        self.merge_indices = None\n        self.annotation_model.set_domain(None)\n        self.annotation_var = None\n        self.row_side_color_model.set_domain(None)\n        self.annotation_color_var = None\n        self.row_split_model.set_domain(None)\n        self.split_by_var = None\n        self.parts = None\n        self.clear_scene()\n        self.selected_rows = []\n        self.__columns_cache.clear()\n        self.__rows_cache.clear()\n        self.__update_clustering_enable_state(None)\n\n    def clear_scene(self):\n        if self.scene.widget is not None:\n            self.scene.widget.layoutDidActivate.disconnect(\n                self.__on_layout_activate\n            )\n            self.scene.widget.selectionFinished.disconnect(\n                self.on_selection_finished\n            )\n        self.scene.widget = None\n        self.scene.clear()\n\n        self.view.setSceneRect(QRectF())\n        self.view.setHeaderSceneRect(QRectF())\n        self.view.setFooterSceneRect(QRectF())\n\n    @Inputs.data\n    def set_dataset(self, data=None):\n        \"\"\"Set the input dataset to display.\"\"\"\n        self.closeContext()\n        self.clear()\n        self.clear_messages()\n        self._set_input_summary(data)\n\n        if isinstance(data, SqlTable):\n            if data.approx_len() < 4000:\n                data = Table(data)\n            else:\n                self.Information.sampled()\n                data_sample = data.sample_time(1, no_cache=True)\n                data_sample.download_data(2000, partial=True)\n                data = Table(data_sample)\n\n        if data is not None and not len(data):\n            data = None\n\n        if data is not None and sp.issparse(data.X):\n            try:\n                data = data.to_dense()\n            except MemoryError:\n                data = None\n                self.Error.not_enough_memory()\n            else:\n                self.Information.sparse_densified()\n\n        input_data = data\n\n        # Data contains no attributes or meta attributes only\n        if data is not None and len(data.domain.attributes) == 0:\n            self.Error.no_continuous()\n            input_data = data = None\n\n        # Data contains some discrete attributes which must be filtered\n        if data is not None and \\\n                any(var.is_discrete for var in data.domain.attributes):\n            ndisc = sum(var.is_discrete for var in data.domain.attributes)\n            data = data.transform(\n                Domain([var for var in data.domain.attributes\n                        if var.is_continuous],\n                       data.domain.class_vars,\n                       data.domain.metas))\n            if not data.domain.attributes:\n                self.Error.no_continuous()\n                input_data = data = None\n            else:\n                self.Information.discrete_ignored(\n                    ndisc, \"s\" if ndisc > 1 else \"\")\n\n        self.data = data\n        self.input_data = input_data\n\n        if data is not None:\n            self.annotation_model.set_domain(self.input_data.domain)\n            self.row_side_color_model.set_domain(self.input_data.domain)\n            self.annotation_var = None\n            self.annotation_color_var = None\n            self.row_split_model.set_domain(data.domain)\n            if data.domain.has_discrete_class:\n                self.split_by_var = data.domain.class_var\n            else:\n                self.split_by_var = None\n            self.openContext(self.input_data)\n            if self.split_by_var not in self.row_split_model:\n                self.split_by_var = None\n\n        self.update_heatmaps()\n        if data is not None and self.__pending_selection is not None:\n            assert self.scene.widget is not None\n            self.scene.widget.selectRows(self.__pending_selection)\n            self.selected_rows = self.__pending_selection\n            self.__pending_selection = None\n\n        self.unconditional_commit()\n\n    def _set_input_summary(self, data):\n        summary = len(data) if data else self.info.NoInput\n        details = format_summary_details(data) if data else \"\"\n        self.info.set_input_summary(summary, details)\n\n    def __on_split_rows_activated(self):\n        self.set_split_variable(self.row_split_cb.currentData(Qt.EditRole))\n\n    def set_split_variable(self, var):\n        if var != self.split_by_var:\n            self.split_by_var = var\n            self.update_heatmaps()\n\n    def update_heatmaps(self):\n        if self.data is not None:\n            self.clear_scene()\n            self.clear_messages()\n            if self.col_clustering != Clustering.None_ and \\\n                    len(self.data.domain.attributes) < 2:\n                self.Error.not_enough_features()\n            elif (self.col_clustering != Clustering.None_ or\n                  self.row_clustering != Clustering.None_) and \\\n                    len(self.data) < 2:\n                self.Error.not_enough_instances()\n            elif self.merge_kmeans and len(self.data) < 3:\n                self.Error.not_enough_instances_k_means()\n            else:\n                parts = self.construct_heatmaps(self.data, self.split_by_var)\n                self.construct_heatmaps_scene(parts, self.effective_data)\n                self.selected_rows = []\n        else:\n            self.clear()\n        if self.parts is not None and self.effective_data is not None:\n            nrows = sum(len(p.indices) for p in self.parts.rows)\n            self.Warning.missing_split_values(\n                shown=nrows < len(self.effective_data))\n        else:\n            self.Warning.missing_split_values.clear()\n\n    def update_merge(self):\n        self.kmeans_model = None\n        self.merge_indices = None\n        if self.data is not None and self.merge_kmeans:\n            self.update_heatmaps()\n            self.commit()\n\n    def _make_parts(self, data, group_var=None):\n        \"\"\"\n        Make initial `Parts` for data, split by group_var, group_key\n        \"\"\"\n        if group_var is not None:\n            assert group_var.is_discrete\n            _col_data = table_column_data(data, group_var)\n            row_indices = [np.flatnonzero(_col_data == i)\n                           for i in range(len(group_var.values))]\n            row_groups = [RowPart(title=name, indices=ind,\n                                  cluster=None, cluster_ordered=None)\n                          for name, ind in zip(group_var.values, row_indices)]\n        else:\n            row_groups = [RowPart(title=None, indices=range(0, len(data)),\n                                  cluster=None, cluster_ordered=None)]\n\n        col_groups = [\n            ColumnPart(\n                title=None, indices=range(0, len(data.domain.attributes)),\n                domain=data.domain, cluster=None, cluster_ordered=None)\n        ]\n\n        minv, maxv = np.nanmin(data.X), np.nanmax(data.X)\n        return Parts(row_groups, col_groups, span=(minv, maxv))\n\n    def cluster_rows(self, data: Table, parts: 'Parts', ordered=False) -> 'Parts':\n        row_groups = []\n        for row in parts.rows:\n            if row.cluster is not None:\n                cluster = row.cluster\n            else:\n                cluster = None\n            if row.cluster_ordered is not None:\n                cluster_ord = row.cluster_ordered\n            else:\n                cluster_ord = None\n\n            if row.can_cluster:\n                matrix = None\n                need_dist = cluster is None or (ordered and cluster_ord is None)\n                if need_dist:\n                    subset = data[row.indices]\n                    matrix = Orange.distance.Euclidean(subset)\n\n                if cluster is None:\n                    assert len(matrix) < self.MaxClustering\n                    cluster = hierarchical.dist_matrix_clustering(\n                        matrix, linkage=hierarchical.WARD\n                    )\n                if ordered and cluster_ord is None:\n                    assert len(matrix) < self.MaxOrderedClustering\n                    cluster_ord = hierarchical.optimal_leaf_ordering(\n                        cluster, matrix,\n                    )\n            row_groups.append(row._replace(cluster=cluster, cluster_ordered=cluster_ord))\n\n        return parts._replace(rows=row_groups)\n\n    def cluster_columns(self, data, parts, ordered=False):\n        assert len(parts.columns) == 1, \"columns split is no longer supported\"\n        assert all(var.is_continuous for var in data.domain.attributes)\n\n        col0 = parts.columns[0]\n        if col0.cluster is not None:\n            cluster = col0.cluster\n        else:\n            cluster = None\n        if col0.cluster_ordered is not None:\n            cluster_ord = col0.cluster_ordered\n        else:\n            cluster_ord = None\n        need_dist = cluster is None or (ordered and cluster_ord is None)\n        matrix = None\n        if need_dist:\n            data = Orange.distance._preprocess(data)\n            matrix = np.asarray(Orange.distance.PearsonR(data, axis=0))\n            # nan values break clustering below\n            matrix = np.nan_to_num(matrix)\n\n        if cluster is None:\n            assert matrix is not None\n            assert len(matrix) < self.MaxClustering\n            cluster = hierarchical.dist_matrix_clustering(\n                matrix, linkage=hierarchical.WARD\n            )\n        if ordered and cluster_ord is None:\n            assert len(matrix) < self.MaxOrderedClustering\n            cluster_ord = hierarchical.optimal_leaf_ordering(cluster, matrix)\n\n        col_groups = [col._replace(cluster=cluster, cluster_ordered=cluster_ord)\n                      for col in parts.columns]\n        return parts._replace(columns=col_groups)\n\n    def construct_heatmaps(self, data, group_var=None) -> 'Parts':\n        if self.merge_kmeans:\n            if self.kmeans_model is None:\n                effective_data = self.input_data.transform(\n                    Orange.data.Domain(\n                        [var for var in self.input_data.domain.attributes\n                         if var.is_continuous],\n                        self.input_data.domain.class_vars,\n                        self.input_data.domain.metas))\n                nclust = min(self.merge_kmeans_k, len(effective_data) - 1)\n                self.kmeans_model = kmeans_compress(effective_data, k=nclust)\n                effective_data.domain = self.kmeans_model.domain\n                merge_indices = [np.flatnonzero(self.kmeans_model.labels == ind)\n                                 for ind in range(nclust)]\n                not_empty_indices = [i for i, x in enumerate(merge_indices)\n                                     if len(x) > 0]\n                self.merge_indices = \\\n                    [merge_indices[i] for i in not_empty_indices]\n                if len(merge_indices) != len(self.merge_indices):\n                    self.Warning.empty_clusters()\n                effective_data = Orange.data.Table(\n                    Orange.data.Domain(effective_data.domain.attributes),\n                    self.kmeans_model.centroids[not_empty_indices]\n                )\n            else:\n                effective_data = self.effective_data\n\n            group_var = None\n        else:\n            self.kmeans_model = None\n            self.merge_indices = None\n            effective_data = data\n\n        self.effective_data = effective_data\n\n        self.__update_clustering_enable_state(effective_data)\n\n        parts = self._make_parts(effective_data, group_var)\n        # Restore/update the row/columns items descriptions from cache if\n        # available\n        rows_cache_key = (group_var,\n                          self.merge_kmeans_k if self.merge_kmeans else None)\n        if rows_cache_key in self.__rows_cache:\n            parts = parts._replace(rows=self.__rows_cache[rows_cache_key].rows)\n\n        if self.row_clustering != Clustering.None_:\n            parts = self.cluster_rows(\n                effective_data, parts,\n                ordered=self.row_clustering == Clustering.OrderedClustering\n            )\n        if self.col_clustering != Clustering.None_:\n            parts = self.cluster_columns(\n                effective_data, parts,\n                ordered=self.col_clustering == Clustering.OrderedClustering\n            )\n\n        # Cache the updated parts\n        self.__rows_cache[rows_cache_key] = parts\n        return parts\n\n    def construct_heatmaps_scene(self, parts: 'Parts', data: Table) -> None:\n        _T = TypeVar(\"_T\", bound=Union[RowPart, ColumnPart])\n\n        def select_cluster(clustering: Clustering, item: _T) -> _T:\n            if clustering == Clustering.None_:\n                return item._replace(cluster=None, cluster_ordered=None)\n            elif clustering == Clustering.Clustering:\n                return item._replace(cluster=item.cluster, cluster_ordered=None)\n            elif clustering == Clustering.OrderedClustering:\n                return item._replace(cluster=item.cluster_ordered, cluster_ordered=None)\n            else:  # pragma: no cover\n                raise TypeError()\n\n        rows = [select_cluster(self.row_clustering, rowitem)\n                for rowitem in parts.rows]\n        cols = [select_cluster(self.col_clustering, colitem)\n                for colitem in parts.columns]\n        parts = Parts(columns=cols, rows=rows, span=parts.span)\n\n        self.setup_scene(parts, data)\n\n    def setup_scene(self, parts, data):\n        # type: (Parts, Table) -> None\n        widget = HeatmapGridWidget()\n        widget.setColorMap(self.color_map())\n        self.scene.addItem(widget)\n        self.scene.widget = widget\n        columns = [v.name for v in data.domain.attributes]\n        parts = HeatmapGridWidget.Parts(\n            rows=[\n                HeatmapGridWidget.RowItem(r.title, r.indices, r.cluster)\n                for r in parts.rows\n            ],\n            columns=[\n                HeatmapGridWidget.ColumnItem(c.title, c.indices, c.cluster)\n                for c in parts.columns\n            ],\n            data=data.X,\n            span=parts.span,\n            row_names=None,\n            col_names=columns,\n        )\n        widget.setHeatmaps(parts)\n        side = self.row_side_colors()\n        if side is not None:\n            widget.setRowSideColorAnnotations(side[0], side[1], name=side[2].name)\n        widget.setColumnLabelsPosition(self._column_label_pos)\n        widget.setAspectRatioMode(\n            Qt.KeepAspectRatio if self.keep_aspect else Qt.IgnoreAspectRatio\n        )\n        widget.setShowAverages(self.averages)\n        widget.setLegendVisible(self.legend)\n\n        widget.layoutDidActivate.connect(self.__on_layout_activate)\n        widget.selectionFinished.connect(self.on_selection_finished)\n\n        self.update_annotations()\n        self.view.setCentralWidget(widget)\n        self.parts = parts\n\n    def __update_scene_rects(self):\n        widget = self.scene.widget\n        if widget is None:\n            return\n        rect = widget.geometry()\n        self.scene.setSceneRect(rect)\n        self.view.setSceneRect(rect)\n        self.view.setHeaderSceneRect(widget.headerGeometry())\n        self.view.setFooterSceneRect(widget.footerGeometry())\n\n    def __on_layout_activate(self):\n        self.__update_scene_rects()\n\n    def __aspect_mode_changed(self):\n        widget = self.scene.widget\n        if widget is None:\n            return\n        widget.setAspectRatioMode(\n            Qt.KeepAspectRatio if self.keep_aspect else Qt.IgnoreAspectRatio\n        )\n        # when aspect fixed the vertical sh is fixex, when not, it can\n        # shrink vertically\n        sp = widget.sizePolicy()\n        if self.keep_aspect:\n            sp.setVerticalPolicy(QSizePolicy.Fixed)\n        else:\n            sp.setVerticalPolicy(QSizePolicy.Preferred)\n        widget.setSizePolicy(sp)\n\n    def __update_clustering_enable_state(self, data):\n        if data is not None:\n            N = len(data)\n            M = len(data.domain.attributes)\n        else:\n            N = M = 0\n\n        rc_enabled = N <= self.MaxClustering\n        rco_enabled = N <= self.MaxOrderedClustering\n        cc_enabled = M <= self.MaxClustering\n        cco_enabled = M <= self.MaxOrderedClustering\n        row_clust, col_clust = self.row_clustering, self.col_clustering\n\n        row_clust_msg = \"\"\n        col_clust_msg = \"\"\n\n        if not rco_enabled and row_clust == Clustering.OrderedClustering:\n            row_clust = Clustering.Clustering\n            row_clust_msg = \"Row cluster ordering was disabled due to the \" \\\n                            \"input matrix being to big\"\n        if not rc_enabled and row_clust == Clustering.Clustering:\n            row_clust = Clustering.None_\n            row_clust_msg = \"Row clustering was was disabled due to the \" \\\n                            \"input matrix being to big\"\n\n        if not cco_enabled and col_clust == Clustering.OrderedClustering:\n            col_clust = Clustering.Clustering\n            col_clust_msg = \"Column cluster ordering was disabled due to \" \\\n                            \"the input matrix being to big\"\n        if not cc_enabled and col_clust == Clustering.Clustering:\n            col_clust = Clustering.None_\n            col_clust_msg = \"Column clustering was disabled due to the \" \\\n                            \"input matrix being to big\"\n\n        self.col_clustering = col_clust\n        self.row_clustering = row_clust\n\n        self.Information.row_clust(row_clust_msg, shown=bool(row_clust_msg))\n        self.Information.col_clust(col_clust_msg, shown=bool(col_clust_msg))\n\n        # Disable/enable the combobox items for the clustering methods\n        def setenabled(cb: QComboBox, clu: bool, clu_op: bool):\n            model = cb.model()\n            assert isinstance(model, QStandardItemModel)\n            idx = cb.findData(Clustering.OrderedClustering, ClusteringRole)\n            assert idx != -1\n            model.item(idx).setEnabled(clu_op)\n            idx = cb.findData(Clustering.Clustering, ClusteringRole)\n            assert idx != -1\n            model.item(idx).setEnabled(clu)\n\n        setenabled(self.row_cluster_cb, rc_enabled, rco_enabled)\n        setenabled(self.col_cluster_cb, cc_enabled, cco_enabled)\n\n    def update_averages_stripe(self):\n        \"\"\"Update the visibility of the averages stripe.\n        \"\"\"\n        widget = self.scene.widget\n        if widget is not None:\n            widget.setShowAverages(self.averages)\n\n    def update_lowslider(self):\n        low, high = self.controls.threshold_low, self.controls.threshold_high\n        if low.value() >= high.value():\n            low.setSliderPosition(high.value() - 1)\n        self.update_color_schema()\n\n    def update_highslider(self):\n        low, high = self.controls.threshold_low, self.controls.threshold_high\n        if low.value() >= high.value():\n            high.setSliderPosition(low.value() + 1)\n        self.update_color_schema()\n\n    def update_color_schema(self):\n        self.palette_name = self.color_cb.currentData().name\n        w = self.scene.widget\n        if w is not None:\n            w.setColorMap(self.color_map())\n\n    def __update_column_clustering(self):\n        self.update_heatmaps()\n        self.commit()\n\n    def __update_row_clustering(self):\n        self.update_heatmaps()\n        self.commit()\n\n    def update_legend(self):\n        widget = self.scene.widget\n        if widget is not None:\n            widget.setLegendVisible(self.legend)\n\n    def row_annotation_var(self):\n        return self.annotation_var\n\n    def row_annotation_data(self):\n        var = self.row_annotation_var()\n        if var is None:\n            return None\n        return column_str_from_table(self.input_data, var)\n\n    def _merge_row_indices(self):\n        if self.merge_kmeans and self.kmeans_model is not None:\n            return self.merge_indices\n        else:\n            return None\n\n    def set_annotation_var(self, var: Union[None, Variable, int]):\n        if isinstance(var, int):\n            var = self.annotation_model[var]\n        if self.annotation_var != var:\n            self.annotation_var = var\n            self.update_annotations()\n\n    def update_annotations(self):\n        widget = self.scene.widget\n        if widget is not None:\n            annot_col = self.row_annotation_data()\n            merge_indices = self._merge_row_indices()\n            if merge_indices is not None and annot_col is not None:\n                join = lambda _1: join_elided(\", \", 42, _1, \" ({} more)\")\n                annot_col = aggregate_apply(join, annot_col, merge_indices)\n            if annot_col is not None:\n                widget.setRowLabels(annot_col)\n                widget.setRowLabelsVisible(True)\n            else:\n                widget.setRowLabelsVisible(False)\n                widget.setRowLabels(None)\n\n    def row_side_colors(self):\n        var = self.annotation_color_var\n        if var is None:\n            return None\n        column_data = column_data_from_table(self.input_data, var)\n        span = (np.nanmin(column_data), np.nanmax(column_data))\n        merges = self._merge_row_indices()\n        if merges is not None:\n            column_data = aggregate(var, column_data, merges)\n        data, colormap = self._colorize(var, column_data)\n        if var.is_continuous:\n            colormap.span = span\n        return data, colormap, var\n\n    def set_annotation_color_var(self, var: Union[None, Variable, int]):\n        \"\"\"Set the current side color annotation variable.\"\"\"\n        if isinstance(var, int):\n            var = self.row_side_color_model[var]\n        if self.annotation_color_var != var:\n            self.annotation_color_var = var\n            self.update_row_side_colors()\n\n    def update_row_side_colors(self):\n        widget = self.scene.widget\n        if widget is None:\n            return\n        colors = self.row_side_colors()\n        if colors is None:\n            widget.setRowSideColorAnnotations(None)\n        else:\n            widget.setRowSideColorAnnotations(colors[0], colors[1], colors[2].name)\n\n    def _colorize(self, var: Variable, data: np.ndarray) -> Tuple[np.ndarray, ColorMap]:\n        palette = var.palette  # type: Palette\n        colors = np.array(\n            [[c.red(), c.green(), c.blue()] for c in palette.qcolors_w_nan],\n            dtype=np.uint8,\n        )\n        if var.is_discrete:\n            mask = np.isnan(data)\n            data[mask] = -1\n            data = data.astype(int)\n            if mask.any():\n                values = (*var.values, \"N/A\")\n            else:\n                values = var.values\n                colors = colors[: -1]\n            return data, CategoricalColorMap(colors, values)\n        elif var.is_continuous:\n            cmap = GradientColorMap(colors[:-1])\n            return data, cmap\n        else:\n            raise TypeError\n\n    def update_column_annotations(self):\n        widget = self.scene.widget\n        if self.data is not None and widget is not None:\n            widget.setColumnLabelsPosition(self._column_label_pos)\n\n    def __adjust_font_size(self, diff):\n        widget = self.scene.widget\n        if widget is None:\n            return\n        curr = widget.font().pointSizeF()\n        new = curr + diff\n\n        self.__font_dec.setEnabled(new > 1.0)\n        self.__font_inc.setEnabled(new <= 32)\n        if new > 1.0:\n            font = QFont()\n            font.setPointSizeF(new)\n            widget.setFont(font)\n\n    def _on_view_context_menu(self, pos):\n        widget = self.scene.widget\n        if widget is None:\n            return\n        assert isinstance(widget, HeatmapGridWidget)\n        menu = QMenu(self.view.viewport())\n        menu.setAttribute(Qt.WA_DeleteOnClose)\n        menu.addActions(self.view.actions())\n        menu.addSeparator()\n        menu.addActions([self.__font_inc, self.__font_dec])\n        menu.addSeparator()\n        a = QAction(\"Keep aspect ratio\", menu, checkable=True)\n        a.setChecked(self.keep_aspect)\n\n        def ontoggled(state):\n            self.keep_aspect = state\n            self.__aspect_mode_changed()\n        a.toggled.connect(ontoggled)\n        menu.addAction(a)\n        menu.popup(self.view.viewport().mapToGlobal(pos))\n\n    def on_selection_finished(self):\n        if self.scene.widget is not None:\n            self.selected_rows = list(self.scene.widget.selectedRows())\n        else:\n            self.selected_rows = []\n        self.commit()\n\n    def commit(self):\n        data = None\n        indices = None\n        if self.merge_kmeans:\n            merge_indices = self.merge_indices\n        else:\n            merge_indices = None\n\n        if self.input_data is not None and self.selected_rows:\n            indices = self.selected_rows\n            if merge_indices is not None:\n                # expand merged indices\n                indices = np.hstack([merge_indices[i] for i in indices])\n\n            data = self.input_data[indices]\n\n        summary = len(data) if data else self.info.NoOutput\n        details = format_summary_details(data) if data else \"\"\n        self.info.set_output_summary(summary, details)\n        self.Outputs.selected_data.send(data)\n        self.Outputs.annotated_data.send(create_annotated_table(self.input_data, indices))\n\n    def onDeleteWidget(self):\n        self.clear()\n        super().onDeleteWidget()\n\n    def send_report(self):\n        self.report_items((\n            (\"Columns:\", \"Clustering\" if self.col_clustering else \"No sorting\"),\n            (\"Rows:\", \"Clustering\" if self.row_clustering else \"No sorting\"),\n            (\"Split:\",\n             self.split_by_var is not None and self.split_by_var.name),\n            (\"Row annotation\",\n             self.annotation_var is not None and self.annotation_var.name),\n        ))\n        self.report_plot()\n\n    @classmethod\n    def migrate_settings(cls, settings, version):\n        if version is not None and version < 3:\n            def st2cl(state: bool) -> Clustering:\n                return Clustering.OrderedClustering if state else \\\n                    Clustering.None_\n\n            rc = settings.pop(\"row_clustering\", False)\n            cc = settings.pop(\"col_clustering\", False)\n            settings[\"row_clustering_method\"] = st2cl(rc).name\n            settings[\"col_clustering_method\"] = st2cl(cc).name\n\n\n# If StickyGraphicsView ever defines qt signals/slots/properties this will\n# break\nclass GraphicsView(GraphicsWidgetView, StickyGraphicsView):\n    pass\n\n\nclass RowPart(NamedTuple):\n    \"\"\"\n    A row group\n\n    Attributes\n    ----------\n    title: str\n        Group title\n    indices : (N, ) Sequence[int]\n        Indices in the input data to retrieve the row subset for the group.\n    cluster : hierarchical.Tree optional\n    cluster_ordered : hierarchical.Tree optional\n    \"\"\"\n    title: str\n    indices: Sequence[int]\n    cluster: Optional[hierarchical.Tree] = None\n    cluster_ordered: Optional[hierarchical.Tree] = None\n\n    @property\n    def can_cluster(self) -> bool:\n        if isinstance(self.indices, slice):\n            return (self.indices.stop - self.indices.start) > 1\n        else:\n            return len(self.indices) > 1\n\n\nclass ColumnPart(NamedTuple):\n    \"\"\"\n    A column group\n\n    Attributes\n    ----------\n    title : str\n        Column group title\n    indices : (N, ) int ndarray\n        Indexes the input data to retrieve the column subset for the group.\n    domain : List[Variable]\n        List of variables in the group.\n    cluster : hierarchical.Tree optional\n    cluster_ordered : hierarchical.Tree optional\n    \"\"\"\n    title: str\n    indices: Sequence[int]\n    domain: Sequence[int]\n    cluster: Optional[hierarchical.Tree] = None\n    cluster_ordered: Optional[hierarchical.Tree] = None\n\n\nclass Parts(NamedTuple):\n    rows: Sequence[RowPart]\n    columns: Sequence[ColumnPart]\n    span: Tuple[float, float]\n\n\ndef join_elided(sep, maxlen, values, elidetemplate=\"...\"):\n    def generate(sep, ellidetemplate, values):\n        count = len(values)\n        length = 0\n        parts = []\n        for i, val in enumerate(values):\n            elide = ellidetemplate.format(count - i) if count - i > 1 else \"\"\n            parts.append(val)\n            length += len(val) + (len(sep) if parts else 0)\n            yield i, islice(parts, i + 1), length, elide\n\n    best = None\n    for _, parts, length, elide in generate(sep, elidetemplate, values):\n        if length > maxlen:\n            if best is None:\n                best = sep.join(parts) + elide\n            return best\n        fulllen = length + len(elide)\n        if fulllen < maxlen or best is None:\n            best = sep.join(parts) + elide\n    return best\n\n\ndef column_str_from_table(\n        table: Orange.data.Table,\n        column: Union[int, Orange.data.Variable],\n) -> np.ndarray:\n    var = table.domain[column]\n    data, _ = table.get_column_view(column)\n    return np.asarray([var.str_val(v) for v in data], dtype=object)\n\n\ndef column_data_from_table(\n        table: Orange.data.Table,\n        column: Union[int, Orange.data.Variable],\n) -> np.ndarray:\n    var = table.domain[column]\n    data, _ = table.get_column_view(column)\n    if var.is_primitive() and data.dtype.kind != \"f\":\n        data = data.astype(float)\n    return data\n\n\ndef aggregate(\n        var: Variable, data: np.ndarray, groupindices: Sequence[Sequence[int]],\n) -> np.ndarray:\n    if var.is_string:\n        join = lambda values: (join_elided(\", \", 42, values, \" ({} more)\"))\n        # collect all original labels for every merged row\n        values = [data[indices] for indices in groupindices]\n        data = [join(list(map(var.str_val, vals))) for vals in values]\n        return np.array(data, dtype=object)\n    elif var.is_continuous:\n        data = [np.nanmean(data[indices]) if len(indices) else np.nan\n                for indices in groupindices]\n        return np.array(data, dtype=float)\n    elif var.is_discrete:\n        from Orange.statistics.util import nanmode\n        data = [nanmode(data[indices])[0] if len(indices) else np.nan\n                for indices in groupindices]\n        return np.asarray(data, dtype=float)\n    else:\n        raise TypeError(type(var))\n\n\ndef agg_join_str(var, data, groupindices, maxlen=50, elidetemplate=\" ({} more)\"):\n    join_s = lambda values: (\n        join_elided(\", \", maxlen, values, elidetemplate=elidetemplate)\n    )\n    join = lambda values: join_s(map(var.str_val, values))\n    return aggregate_apply(join, data, groupindices)\n\n\n_T = TypeVar(\"_T\")\n\n\ndef aggregate_apply(\n        f: Callable[[Sequence], _T],\n        data: np.ndarray,\n        groupindices: Sequence[Sequence[int]]\n) -> Sequence[_T]:\n    return [f(data[indices]) for indices in groupindices]\n\n\nif __name__ == \"__main__\":  # pragma: no cover\n    WidgetPreview(OWHeatMap).run(Table(\"brown-selected.tab\"))\n",
            "new_file_content": "import enum\nfrom itertools import islice\nfrom typing import (\n    Iterable, Mapping, Any, TypeVar, Type, NamedTuple, Sequence, Optional,\n    Union, Tuple, List, Callable\n)\n\nimport numpy as np\nimport scipy.sparse as sp\n\nfrom AnyQt.QtWidgets import (\n    QGraphicsScene, QGraphicsView, QFormLayout, QComboBox, QGroupBox,\n    QMenu, QAction, QSizePolicy\n)\nfrom AnyQt.QtGui import QStandardItemModel, QStandardItem, QFont, QKeySequence\nfrom AnyQt.QtCore import Qt, QSize, QRectF, QObject\n\nfrom orangewidget.utils.combobox import ComboBox, ComboBoxSearch\nfrom Orange.data import Domain, Table, Variable, DiscreteVariable\nfrom Orange.data.sql.table import SqlTable\nimport Orange.distance\n\nfrom Orange.clustering import hierarchical, kmeans\nfrom Orange.widgets.utils import colorpalettes, apply_all\nfrom Orange.widgets.utils.itemmodels import DomainModel\nfrom Orange.widgets.utils.stickygraphicsview import StickyGraphicsView\nfrom Orange.widgets.utils.graphicsview import GraphicsWidgetView\nfrom Orange.widgets.utils.colorpalettes import DiscretePalette, Palette\n\nfrom Orange.widgets.utils.annotated_data import (create_annotated_table,\n                                                 ANNOTATED_DATA_SIGNAL_NAME)\nfrom Orange.widgets import widget, gui, settings\nfrom Orange.widgets.widget import Msg, Input, Output\n\nfrom Orange.widgets.data.oweditdomain import table_column_data\nfrom Orange.widgets.visualize.utils.heatmap import HeatmapGridWidget, \\\n    ColorMap, CategoricalColorMap, GradientColorMap\nfrom Orange.widgets.utils.widgetpreview import WidgetPreview\nfrom Orange.widgets.utils.state_summary import format_summary_details\n\n\n__all__ = []\n\n\ndef kmeans_compress(X, k=50):\n    km = kmeans.KMeans(n_clusters=k, n_init=5, random_state=42)\n    return km.get_model(X)\n\n\ndef cbselect(cb: QComboBox, value, role: Qt.ItemDataRole = Qt.EditRole) -> None:\n    \"\"\"\n    Find and select the `value` in the `cb` QComboBox.\n\n    Parameters\n    ----------\n    cb: QComboBox\n    value: Any\n    role: Qt.ItemDataRole\n        The data role in the combo box model to match value against\n    \"\"\"\n    cb.setCurrentIndex(cb.findData(value, role))\n\n\nclass Clustering(enum.IntEnum):\n    #: No clustering\n    None_ = 0\n    #: Hierarchical clustering\n    Clustering = 1\n    #: Hierarchical clustering with optimal leaf ordering\n    OrderedClustering = 2\n\n\nClusteringRole = Qt.UserRole + 13\n#: Item data for clustering method selection models\nClusteringModelData = [\n    {\n        Qt.DisplayRole: \"None\",\n        Qt.ToolTipRole: \"No clustering\",\n        ClusteringRole: Clustering.None_,\n    }, {\n        Qt.DisplayRole: \"Clustering\",\n        Qt.ToolTipRole: \"Apply hierarchical clustering\",\n        ClusteringRole: Clustering.Clustering,\n    }, {\n        Qt.DisplayRole: \"Clustering (opt. ordering)\",\n        Qt.ToolTipRole: \"Apply hierarchical clustering with optimal leaf \"\n                        \"ordering.\",\n        ClusteringRole: Clustering.OrderedClustering,\n    }\n]\n\nColumnLabelsPosData = [\n    {Qt.DisplayRole: name, Qt.UserRole: value}\n    for name, value in [\n        (\"None\", HeatmapGridWidget.NoPosition),\n        (\"Top\", HeatmapGridWidget.PositionTop),\n        (\"Bottom\", HeatmapGridWidget.PositionBottom),\n        (\"Top and Bottom\", (HeatmapGridWidget.PositionTop |\n                            HeatmapGridWidget.PositionBottom)),\n    ]\n]\n\n\ndef create_list_model(\n        items: Iterable[Mapping[Qt.ItemDataRole, Any]],\n        parent: Optional[QObject] = None,\n) -> QStandardItemModel:\n    \"\"\"Create list model from an item date iterable.\"\"\"\n    model = QStandardItemModel(parent)\n    for item in items:\n        sitem = QStandardItem()\n        for role, value in item.items():\n            sitem.setData(value, role)\n        model.appendRow([sitem])\n    return model\n\n\nE = TypeVar(\"E\", bound=enum.Enum)  # pylint: disable=invalid-name\n\n\ndef enum_get(etype: Type[E], name: str, default: E) -> E:\n    \"\"\"\n    Return an Enum member by `name`. If no such member exists in `etype`\n    return `default`.\n    \"\"\"\n    try:\n        return etype[name]\n    except LookupError:\n        return default\n\n\nclass OWHeatMap(widget.OWWidget):\n    name = \"Heat Map\"\n    description = \"Plot a data matrix heatmap.\"\n    icon = \"icons/Heatmap.svg\"\n    priority = 260\n    keywords = []\n\n    class Inputs:\n        data = Input(\"Data\", Table)\n\n    class Outputs:\n        selected_data = Output(\"Selected Data\", Table, default=True)\n        annotated_data = Output(ANNOTATED_DATA_SIGNAL_NAME, Table)\n\n    settings_version = 3\n\n    settingsHandler = settings.DomainContextHandler()\n\n    # Disable clustering for inputs bigger than this\n    MaxClustering = 25000\n    # Disable cluster leaf ordering for inputs bigger than this\n    MaxOrderedClustering = 1000\n\n    threshold_low = settings.Setting(0.0)\n    threshold_high = settings.Setting(1.0)\n\n    merge_kmeans = settings.Setting(False)\n    merge_kmeans_k = settings.Setting(50)\n\n    # Display column with averages\n    averages: bool = settings.Setting(True)\n    # Display legend\n    legend: bool = settings.Setting(True)\n    # Annotations\n    #: text row annotation (row names)\n    annotation_var = settings.ContextSetting(None)\n    #: color row annotation\n    annotation_color_var = settings.ContextSetting(None)\n    # Discrete variable used to split that data/heatmaps (vertically)\n    split_by_var = settings.ContextSetting(None)\n    # Selected row/column clustering method (name)\n    col_clustering_method: str = settings.Setting(Clustering.None_.name)\n    row_clustering_method: str = settings.Setting(Clustering.None_.name)\n\n    palette_name = settings.Setting(colorpalettes.DefaultContinuousPaletteName)\n    column_label_pos: int = settings.Setting(1)\n    selected_rows: List[int] = settings.Setting(None, schema_only=True)\n\n    auto_commit = settings.Setting(True)\n\n    graph_name = \"scene\"\n\n    left_side_scrolling = True\n\n    class Information(widget.OWWidget.Information):\n        sampled = Msg(\"Data has been sampled\")\n        discrete_ignored = Msg(\"{} categorical feature{} ignored\")\n        row_clust = Msg(\"{}\")\n        col_clust = Msg(\"{}\")\n        sparse_densified = Msg(\"Showing this data may require a lot of memory\")\n\n    class Error(widget.OWWidget.Error):\n        no_continuous = Msg(\"No numeric features\")\n        not_enough_features = Msg(\"Not enough features for column clustering\")\n        not_enough_instances = Msg(\"Not enough instances for clustering\")\n        not_enough_instances_k_means = Msg(\n            \"Not enough instances for k-means merging\")\n        not_enough_memory = Msg(\"Not enough memory to show this data\")\n\n    class Warning(widget.OWWidget.Warning):\n        empty_clusters = Msg(\"Empty clusters were removed\")\n\n    def __init__(self):\n        super().__init__()\n        self.__pending_selection = self.selected_rows\n\n        # A kingdom for a save_state/restore_state\n        self.col_clustering = enum_get(\n            Clustering, self.col_clustering_method, Clustering.None_)\n        self.row_clustering = enum_get(\n            Clustering, self.row_clustering_method, Clustering.None_)\n\n        @self.settingsAboutToBePacked.connect\n        def _():\n            self.col_clustering_method = self.col_clustering.name\n            self.row_clustering_method = self.row_clustering.name\n\n        self.keep_aspect = False\n\n        #: The original data with all features (retained to\n        #: preserve the domain on the output)\n        self.input_data = None\n        #: The effective data striped of discrete features, and often\n        #: merged using k-means\n        self.data = None\n        self.effective_data = None\n        #: kmeans model used to merge rows of input_data\n        self.kmeans_model = None\n        #: merge indices derived from kmeans\n        #: a list (len==k) of int ndarray where the i-th item contains\n        #: the indices which merge the input_data into the heatmap row i\n        self.merge_indices = None\n        self.parts: Optional[Parts] = None\n        self.__rows_cache = {}\n        self.__columns_cache = {}\n\n        self.info.set_input_summary(self.info.NoInput)\n        self.info.set_output_summary(self.info.NoOutput)\n\n        # GUI definition\n        colorbox = gui.vBox(self.controlArea, \"Color\")\n        self.color_cb = gui.palette_combo_box(self.palette_name)\n        self.color_cb.currentIndexChanged.connect(self.update_color_schema)\n        colorbox.layout().addWidget(self.color_cb)\n\n        form = QFormLayout(\n            formAlignment=Qt.AlignLeft,\n            labelAlignment=Qt.AlignLeft,\n            fieldGrowthPolicy=QFormLayout.AllNonFixedFieldsGrow\n        )\n        lowslider = gui.hSlider(\n            colorbox, self, \"threshold_low\", minValue=0.0, maxValue=1.0,\n            step=0.05, ticks=True, intOnly=False,\n            createLabel=False, callback=self.update_lowslider)\n        highslider = gui.hSlider(\n            colorbox, self, \"threshold_high\", minValue=0.0, maxValue=1.0,\n            step=0.05, ticks=True, intOnly=False,\n            createLabel=False, callback=self.update_highslider)\n\n        form.addRow(\"Low:\", lowslider)\n        form.addRow(\"High:\", highslider)\n\n        colorbox.layout().addLayout(form)\n\n        mergebox = gui.vBox(self.controlArea, \"Merge\",)\n        gui.checkBox(mergebox, self, \"merge_kmeans\", \"Merge by k-means\",\n                     callback=self.__update_row_clustering)\n        ibox = gui.indentedBox(mergebox)\n        gui.spin(ibox, self, \"merge_kmeans_k\", minv=5, maxv=500,\n                 label=\"Clusters:\", keyboardTracking=False,\n                 callbackOnReturn=True, callback=self.update_merge)\n\n        cluster_box = gui.vBox(self.controlArea, \"Clustering\")\n        # Row clustering\n        self.row_cluster_cb = cb = ComboBox()\n        cb.setModel(create_list_model(ClusteringModelData, self))\n        cbselect(cb, self.row_clustering, ClusteringRole)\n        self.connect_control(\n            \"row_clustering\",\n            lambda value, cb=cb: cbselect(cb, value, ClusteringRole)\n        )\n        @cb.activated.connect\n        def _(idx, cb=cb):\n            self.set_row_clustering(cb.itemData(idx, ClusteringRole))\n\n        # Column clustering\n        self.col_cluster_cb = cb = ComboBox()\n        cb.setModel(create_list_model(ClusteringModelData, self))\n        cbselect(cb, self.col_clustering, ClusteringRole)\n        self.connect_control(\n            \"col_clustering\",\n            lambda value, cb=cb: cbselect(cb, value, ClusteringRole)\n        )\n        @cb.activated.connect\n        def _(idx, cb=cb):\n            self.set_col_clustering(cb.itemData(idx, ClusteringRole))\n\n        form = QFormLayout(\n            labelAlignment=Qt.AlignLeft, formAlignment=Qt.AlignLeft,\n            fieldGrowthPolicy=QFormLayout.AllNonFixedFieldsGrow,\n        )\n        form.addRow(\"Rows:\", self.row_cluster_cb)\n        form.addRow(\"Columns:\", self.col_cluster_cb)\n        cluster_box.layout().addLayout(form)\n        box = gui.vBox(self.controlArea, \"Split By\")\n\n        self.row_split_model = DomainModel(\n            placeholder=\"(None)\",\n            valid_types=(Orange.data.DiscreteVariable,),\n            parent=self,\n        )\n        self.row_split_cb = cb = ComboBoxSearch(\n            enabled=not self.merge_kmeans,\n            sizeAdjustPolicy=ComboBox.AdjustToMinimumContentsLengthWithIcon,\n            minimumContentsLength=14,\n            toolTip=\"Split the heatmap vertically by a categorical column\"\n        )\n        self.row_split_cb.setModel(self.row_split_model)\n        self.connect_control(\n            \"split_by_var\", lambda value, cb=cb: cbselect(cb, value)\n        )\n        self.connect_control(\n            \"merge_kmeans\", self.row_split_cb.setDisabled\n        )\n        self.split_by_var = None\n\n        self.row_split_cb.activated.connect(\n            self.__on_split_rows_activated\n        )\n        box.layout().addWidget(self.row_split_cb)\n\n        box = gui.vBox(self.controlArea, 'Annotation && Legends')\n\n        gui.checkBox(box, self, 'legend', 'Show legend',\n                     callback=self.update_legend)\n\n        gui.checkBox(box, self, 'averages', 'Stripes with averages',\n                     callback=self.update_averages_stripe)\n        annotbox = QGroupBox(\"Row Annotations\", flat=True)\n        form = QFormLayout(\n            annotbox,\n            formAlignment=Qt.AlignLeft,\n            labelAlignment=Qt.AlignLeft,\n            fieldGrowthPolicy=QFormLayout.AllNonFixedFieldsGrow\n        )\n        self.annotation_model = DomainModel(placeholder=\"(None)\")\n        self.annotation_text_cb = ComboBoxSearch(\n            minimumContentsLength=12,\n            sizeAdjustPolicy=QComboBox.AdjustToMinimumContentsLength\n        )\n        self.annotation_text_cb.setModel(self.annotation_model)\n        self.annotation_text_cb.activated.connect(self.set_annotation_var)\n        self.connect_control(\"annotation_var\", self.annotation_var_changed)\n\n        self.row_side_color_model = DomainModel(\n            order=(DomainModel.CLASSES, DomainModel.Separator,\n                   DomainModel.METAS),\n            placeholder=\"(None)\", valid_types=DomainModel.PRIMITIVE,\n            flags=Qt.ItemIsSelectable | Qt.ItemIsEnabled,\n            parent=self,\n        )\n        self.row_side_color_cb = ComboBoxSearch(\n            sizeAdjustPolicy=QComboBox.AdjustToMinimumContentsLength,\n            minimumContentsLength=12\n        )\n        self.row_side_color_cb.setModel(self.row_side_color_model)\n        self.row_side_color_cb.activated.connect(self.set_annotation_color_var)\n        self.connect_control(\"annotation_color_var\", self.annotation_color_var_changed)\n        form.addRow(\"Text\", self.annotation_text_cb)\n        form.addRow(\"Color\", self.row_side_color_cb)\n        box.layout().addWidget(annotbox)\n        posbox = gui.vBox(box, \"Column Labels Position\", addSpace=False)\n        posbox.setFlat(True)\n        cb = gui.comboBox(\n            posbox, self, \"column_label_pos\",\n            callback=self.update_column_annotations)\n        cb.setModel(create_list_model(ColumnLabelsPosData, parent=self))\n        cb.setCurrentIndex(self.column_label_pos)\n        gui.checkBox(self.controlArea, self, \"keep_aspect\",\n                     \"Keep aspect ratio\", box=\"Resize\",\n                     callback=self.__aspect_mode_changed)\n\n        gui.rubber(self.controlArea)\n        gui.auto_send(self.controlArea, self, \"auto_commit\")\n\n        # Scene with heatmap\n        class HeatmapScene(QGraphicsScene):\n            widget: Optional[HeatmapGridWidget] = None\n\n        self.scene = self.scene = HeatmapScene(parent=self)\n        self.view = GraphicsView(\n            self.scene,\n            verticalScrollBarPolicy=Qt.ScrollBarAlwaysOn,\n            horizontalScrollBarPolicy=Qt.ScrollBarAlwaysOn,\n            viewportUpdateMode=QGraphicsView.FullViewportUpdate,\n            widgetResizable=True,\n        )\n        self.view.setContextMenuPolicy(Qt.CustomContextMenu)\n        self.view.customContextMenuRequested.connect(\n            self._on_view_context_menu\n        )\n        self.mainArea.layout().addWidget(self.view)\n        self.selected_rows = []\n        self.__font_inc = QAction(\n            \"Increase Font\", self, shortcut=QKeySequence(\"ctrl+>\"))\n        self.__font_dec = QAction(\n            \"Decrease Font\", self, shortcut=QKeySequence(\"ctrl+<\"))\n        self.__font_inc.triggered.connect(lambda: self.__adjust_font_size(1))\n        self.__font_dec.triggered.connect(lambda: self.__adjust_font_size(-1))\n        if hasattr(QAction, \"setShortcutVisibleInContextMenu\"):\n            apply_all(\n                [self.__font_inc, self.__font_dec],\n                lambda a: a.setShortcutVisibleInContextMenu(True)\n            )\n        self.addActions([self.__font_inc, self.__font_dec])\n\n    @property\n    def center_palette(self):\n        palette = self.color_cb.currentData()\n        return bool(palette.flags & palette.Diverging)\n\n    @property\n    def _column_label_pos(self) -> HeatmapGridWidget.Position:\n        return ColumnLabelsPosData[self.column_label_pos][Qt.UserRole]\n\n    def annotation_color_var_changed(self, value):\n        cbselect(self.row_side_color_cb, value, Qt.EditRole)\n\n    def annotation_var_changed(self, value):\n        cbselect(self.annotation_text_cb, value, Qt.EditRole)\n\n    def set_row_clustering(self, method: Clustering) -> None:\n        assert isinstance(method, Clustering)\n        if self.row_clustering != method:\n            self.row_clustering = method\n            cbselect(self.row_cluster_cb, method, ClusteringRole)\n            self.__update_row_clustering()\n\n    def set_col_clustering(self, method: Clustering) -> None:\n        assert isinstance(method, Clustering)\n        if self.col_clustering != method:\n            self.col_clustering = method\n            cbselect(self.col_cluster_cb, method, ClusteringRole)\n            self.__update_column_clustering()\n\n    def sizeHint(self) -> QSize:\n        return super().sizeHint().expandedTo(QSize(900, 700))\n\n    def color_palette(self):\n        return self.color_cb.currentData().lookup_table()\n\n    def color_map(self) -> GradientColorMap:\n        return GradientColorMap(\n            self.color_palette(), (self.threshold_low, self.threshold_high),\n            0 if self.center_palette else None\n        )\n\n    def clear(self):\n        self.data = None\n        self.input_data = None\n        self.effective_data = None\n        self.kmeans_model = None\n        self.merge_indices = None\n        self.annotation_model.set_domain(None)\n        self.annotation_var = None\n        self.row_side_color_model.set_domain(None)\n        self.annotation_color_var = None\n        self.row_split_model.set_domain(None)\n        self.split_by_var = None\n        self.parts = None\n        self.clear_scene()\n        self.selected_rows = []\n        self.__columns_cache.clear()\n        self.__rows_cache.clear()\n        self.__update_clustering_enable_state(None)\n\n    def clear_scene(self):\n        if self.scene.widget is not None:\n            self.scene.widget.layoutDidActivate.disconnect(\n                self.__on_layout_activate\n            )\n            self.scene.widget.selectionFinished.disconnect(\n                self.on_selection_finished\n            )\n        self.scene.widget = None\n        self.scene.clear()\n\n        self.view.setSceneRect(QRectF())\n        self.view.setHeaderSceneRect(QRectF())\n        self.view.setFooterSceneRect(QRectF())\n\n    @Inputs.data\n    def set_dataset(self, data=None):\n        \"\"\"Set the input dataset to display.\"\"\"\n        self.closeContext()\n        self.clear()\n        self.clear_messages()\n        self._set_input_summary(data)\n\n        if isinstance(data, SqlTable):\n            if data.approx_len() < 4000:\n                data = Table(data)\n            else:\n                self.Information.sampled()\n                data_sample = data.sample_time(1, no_cache=True)\n                data_sample.download_data(2000, partial=True)\n                data = Table(data_sample)\n\n        if data is not None and not len(data):\n            data = None\n\n        if data is not None and sp.issparse(data.X):\n            try:\n                data = data.to_dense()\n            except MemoryError:\n                data = None\n                self.Error.not_enough_memory()\n            else:\n                self.Information.sparse_densified()\n\n        input_data = data\n\n        # Data contains no attributes or meta attributes only\n        if data is not None and len(data.domain.attributes) == 0:\n            self.Error.no_continuous()\n            input_data = data = None\n\n        # Data contains some discrete attributes which must be filtered\n        if data is not None and \\\n                any(var.is_discrete for var in data.domain.attributes):\n            ndisc = sum(var.is_discrete for var in data.domain.attributes)\n            data = data.transform(\n                Domain([var for var in data.domain.attributes\n                        if var.is_continuous],\n                       data.domain.class_vars,\n                       data.domain.metas))\n            if not data.domain.attributes:\n                self.Error.no_continuous()\n                input_data = data = None\n            else:\n                self.Information.discrete_ignored(\n                    ndisc, \"s\" if ndisc > 1 else \"\")\n\n        self.data = data\n        self.input_data = input_data\n\n        if data is not None:\n            self.annotation_model.set_domain(self.input_data.domain)\n            self.row_side_color_model.set_domain(self.input_data.domain)\n            self.annotation_var = None\n            self.annotation_color_var = None\n            self.row_split_model.set_domain(data.domain)\n            if data.domain.has_discrete_class:\n                self.split_by_var = data.domain.class_var\n            else:\n                self.split_by_var = None\n            self.openContext(self.input_data)\n            if self.split_by_var not in self.row_split_model:\n                self.split_by_var = None\n\n        self.update_heatmaps()\n        if data is not None and self.__pending_selection is not None:\n            assert self.scene.widget is not None\n            self.scene.widget.selectRows(self.__pending_selection)\n            self.selected_rows = self.__pending_selection\n            self.__pending_selection = None\n\n        self.unconditional_commit()\n\n    def _set_input_summary(self, data):\n        summary = len(data) if data else self.info.NoInput\n        details = format_summary_details(data) if data else \"\"\n        self.info.set_input_summary(summary, details)\n\n    def __on_split_rows_activated(self):\n        self.set_split_variable(self.row_split_cb.currentData(Qt.EditRole))\n\n    def set_split_variable(self, var):\n        if var != self.split_by_var:\n            self.split_by_var = var\n            self.update_heatmaps()\n\n    def update_heatmaps(self):\n        if self.data is not None:\n            self.clear_scene()\n            self.clear_messages()\n            if self.col_clustering != Clustering.None_ and \\\n                    len(self.data.domain.attributes) < 2:\n                self.Error.not_enough_features()\n            elif (self.col_clustering != Clustering.None_ or\n                  self.row_clustering != Clustering.None_) and \\\n                    len(self.data) < 2:\n                self.Error.not_enough_instances()\n            elif self.merge_kmeans and len(self.data) < 3:\n                self.Error.not_enough_instances_k_means()\n            else:\n                parts = self.construct_heatmaps(self.data, self.split_by_var)\n                self.construct_heatmaps_scene(parts, self.effective_data)\n                self.selected_rows = []\n        else:\n            self.clear()\n\n    def update_merge(self):\n        self.kmeans_model = None\n        self.merge_indices = None\n        if self.data is not None and self.merge_kmeans:\n            self.update_heatmaps()\n            self.commit()\n\n    def _make_parts(self, data, group_var=None):\n        \"\"\"\n        Make initial `Parts` for data, split by group_var, group_key\n        \"\"\"\n        if group_var is not None:\n            assert group_var.is_discrete\n            _col_data = table_column_data(data, group_var)\n            row_indices = [np.flatnonzero(_col_data == i)\n                           for i in range(len(group_var.values))]\n\n            row_groups = [RowPart(title=name, indices=ind,\n                                  cluster=None, cluster_ordered=None)\n                          for name, ind in zip(group_var.values, row_indices)]\n            if np.any(_col_data.mask):\n                row_groups.append(RowPart(\n                    title=\"N/A\", indices=np.flatnonzero(_col_data.mask),\n                    cluster=None, cluster_ordered=None\n                ))\n        else:\n            row_groups = [RowPart(title=None, indices=range(0, len(data)),\n                                  cluster=None, cluster_ordered=None)]\n\n        col_groups = [\n            ColumnPart(\n                title=None, indices=range(0, len(data.domain.attributes)),\n                domain=data.domain, cluster=None, cluster_ordered=None)\n        ]\n\n        minv, maxv = np.nanmin(data.X), np.nanmax(data.X)\n        return Parts(row_groups, col_groups, span=(minv, maxv))\n\n    def cluster_rows(self, data: Table, parts: 'Parts', ordered=False) -> 'Parts':\n        row_groups = []\n        for row in parts.rows:\n            if row.cluster is not None:\n                cluster = row.cluster\n            else:\n                cluster = None\n            if row.cluster_ordered is not None:\n                cluster_ord = row.cluster_ordered\n            else:\n                cluster_ord = None\n\n            if row.can_cluster:\n                matrix = None\n                need_dist = cluster is None or (ordered and cluster_ord is None)\n                if need_dist:\n                    subset = data[row.indices]\n                    matrix = Orange.distance.Euclidean(subset)\n\n                if cluster is None:\n                    assert len(matrix) < self.MaxClustering\n                    cluster = hierarchical.dist_matrix_clustering(\n                        matrix, linkage=hierarchical.WARD\n                    )\n                if ordered and cluster_ord is None:\n                    assert len(matrix) < self.MaxOrderedClustering\n                    cluster_ord = hierarchical.optimal_leaf_ordering(\n                        cluster, matrix,\n                    )\n            row_groups.append(row._replace(cluster=cluster, cluster_ordered=cluster_ord))\n\n        return parts._replace(rows=row_groups)\n\n    def cluster_columns(self, data, parts, ordered=False):\n        assert len(parts.columns) == 1, \"columns split is no longer supported\"\n        assert all(var.is_continuous for var in data.domain.attributes)\n\n        col0 = parts.columns[0]\n        if col0.cluster is not None:\n            cluster = col0.cluster\n        else:\n            cluster = None\n        if col0.cluster_ordered is not None:\n            cluster_ord = col0.cluster_ordered\n        else:\n            cluster_ord = None\n        need_dist = cluster is None or (ordered and cluster_ord is None)\n        matrix = None\n        if need_dist:\n            data = Orange.distance._preprocess(data)\n            matrix = np.asarray(Orange.distance.PearsonR(data, axis=0))\n            # nan values break clustering below\n            matrix = np.nan_to_num(matrix)\n\n        if cluster is None:\n            assert matrix is not None\n            assert len(matrix) < self.MaxClustering\n            cluster = hierarchical.dist_matrix_clustering(\n                matrix, linkage=hierarchical.WARD\n            )\n        if ordered and cluster_ord is None:\n            assert len(matrix) < self.MaxOrderedClustering\n            cluster_ord = hierarchical.optimal_leaf_ordering(cluster, matrix)\n\n        col_groups = [col._replace(cluster=cluster, cluster_ordered=cluster_ord)\n                      for col in parts.columns]\n        return parts._replace(columns=col_groups)\n\n    def construct_heatmaps(self, data, group_var=None) -> 'Parts':\n        if self.merge_kmeans:\n            if self.kmeans_model is None:\n                effective_data = self.input_data.transform(\n                    Orange.data.Domain(\n                        [var for var in self.input_data.domain.attributes\n                         if var.is_continuous],\n                        self.input_data.domain.class_vars,\n                        self.input_data.domain.metas))\n                nclust = min(self.merge_kmeans_k, len(effective_data) - 1)\n                self.kmeans_model = kmeans_compress(effective_data, k=nclust)\n                effective_data.domain = self.kmeans_model.domain\n                merge_indices = [np.flatnonzero(self.kmeans_model.labels == ind)\n                                 for ind in range(nclust)]\n                not_empty_indices = [i for i, x in enumerate(merge_indices)\n                                     if len(x) > 0]\n                self.merge_indices = \\\n                    [merge_indices[i] for i in not_empty_indices]\n                if len(merge_indices) != len(self.merge_indices):\n                    self.Warning.empty_clusters()\n                effective_data = Orange.data.Table(\n                    Orange.data.Domain(effective_data.domain.attributes),\n                    self.kmeans_model.centroids[not_empty_indices]\n                )\n            else:\n                effective_data = self.effective_data\n\n            group_var = None\n        else:\n            self.kmeans_model = None\n            self.merge_indices = None\n            effective_data = data\n\n        self.effective_data = effective_data\n\n        self.__update_clustering_enable_state(effective_data)\n\n        parts = self._make_parts(effective_data, group_var)\n        # Restore/update the row/columns items descriptions from cache if\n        # available\n        rows_cache_key = (group_var,\n                          self.merge_kmeans_k if self.merge_kmeans else None)\n        if rows_cache_key in self.__rows_cache:\n            parts = parts._replace(rows=self.__rows_cache[rows_cache_key].rows)\n\n        if self.row_clustering != Clustering.None_:\n            parts = self.cluster_rows(\n                effective_data, parts,\n                ordered=self.row_clustering == Clustering.OrderedClustering\n            )\n        if self.col_clustering != Clustering.None_:\n            parts = self.cluster_columns(\n                effective_data, parts,\n                ordered=self.col_clustering == Clustering.OrderedClustering\n            )\n\n        # Cache the updated parts\n        self.__rows_cache[rows_cache_key] = parts\n        return parts\n\n    def construct_heatmaps_scene(self, parts: 'Parts', data: Table) -> None:\n        _T = TypeVar(\"_T\", bound=Union[RowPart, ColumnPart])\n\n        def select_cluster(clustering: Clustering, item: _T) -> _T:\n            if clustering == Clustering.None_:\n                return item._replace(cluster=None, cluster_ordered=None)\n            elif clustering == Clustering.Clustering:\n                return item._replace(cluster=item.cluster, cluster_ordered=None)\n            elif clustering == Clustering.OrderedClustering:\n                return item._replace(cluster=item.cluster_ordered, cluster_ordered=None)\n            else:  # pragma: no cover\n                raise TypeError()\n\n        rows = [select_cluster(self.row_clustering, rowitem)\n                for rowitem in parts.rows]\n        cols = [select_cluster(self.col_clustering, colitem)\n                for colitem in parts.columns]\n        parts = Parts(columns=cols, rows=rows, span=parts.span)\n\n        self.setup_scene(parts, data)\n\n    def setup_scene(self, parts, data):\n        # type: (Parts, Table) -> None\n        widget = HeatmapGridWidget()\n        widget.setColorMap(self.color_map())\n        self.scene.addItem(widget)\n        self.scene.widget = widget\n        columns = [v.name for v in data.domain.attributes]\n        parts = HeatmapGridWidget.Parts(\n            rows=[\n                HeatmapGridWidget.RowItem(r.title, r.indices, r.cluster)\n                for r in parts.rows\n            ],\n            columns=[\n                HeatmapGridWidget.ColumnItem(c.title, c.indices, c.cluster)\n                for c in parts.columns\n            ],\n            data=data.X,\n            span=parts.span,\n            row_names=None,\n            col_names=columns,\n        )\n        widget.setHeatmaps(parts)\n        side = self.row_side_colors()\n        if side is not None:\n            widget.setRowSideColorAnnotations(side[0], side[1], name=side[2].name)\n        widget.setColumnLabelsPosition(self._column_label_pos)\n        widget.setAspectRatioMode(\n            Qt.KeepAspectRatio if self.keep_aspect else Qt.IgnoreAspectRatio\n        )\n        widget.setShowAverages(self.averages)\n        widget.setLegendVisible(self.legend)\n\n        widget.layoutDidActivate.connect(self.__on_layout_activate)\n        widget.selectionFinished.connect(self.on_selection_finished)\n\n        self.update_annotations()\n        self.view.setCentralWidget(widget)\n        self.parts = parts\n\n    def __update_scene_rects(self):\n        widget = self.scene.widget\n        if widget is None:\n            return\n        rect = widget.geometry()\n        self.scene.setSceneRect(rect)\n        self.view.setSceneRect(rect)\n        self.view.setHeaderSceneRect(widget.headerGeometry())\n        self.view.setFooterSceneRect(widget.footerGeometry())\n\n    def __on_layout_activate(self):\n        self.__update_scene_rects()\n\n    def __aspect_mode_changed(self):\n        widget = self.scene.widget\n        if widget is None:\n            return\n        widget.setAspectRatioMode(\n            Qt.KeepAspectRatio if self.keep_aspect else Qt.IgnoreAspectRatio\n        )\n        # when aspect fixed the vertical sh is fixex, when not, it can\n        # shrink vertically\n        sp = widget.sizePolicy()\n        if self.keep_aspect:\n            sp.setVerticalPolicy(QSizePolicy.Fixed)\n        else:\n            sp.setVerticalPolicy(QSizePolicy.Preferred)\n        widget.setSizePolicy(sp)\n\n    def __update_clustering_enable_state(self, data):\n        if data is not None:\n            N = len(data)\n            M = len(data.domain.attributes)\n        else:\n            N = M = 0\n\n        rc_enabled = N <= self.MaxClustering\n        rco_enabled = N <= self.MaxOrderedClustering\n        cc_enabled = M <= self.MaxClustering\n        cco_enabled = M <= self.MaxOrderedClustering\n        row_clust, col_clust = self.row_clustering, self.col_clustering\n\n        row_clust_msg = \"\"\n        col_clust_msg = \"\"\n\n        if not rco_enabled and row_clust == Clustering.OrderedClustering:\n            row_clust = Clustering.Clustering\n            row_clust_msg = \"Row cluster ordering was disabled due to the \" \\\n                            \"input matrix being to big\"\n        if not rc_enabled and row_clust == Clustering.Clustering:\n            row_clust = Clustering.None_\n            row_clust_msg = \"Row clustering was was disabled due to the \" \\\n                            \"input matrix being to big\"\n\n        if not cco_enabled and col_clust == Clustering.OrderedClustering:\n            col_clust = Clustering.Clustering\n            col_clust_msg = \"Column cluster ordering was disabled due to \" \\\n                            \"the input matrix being to big\"\n        if not cc_enabled and col_clust == Clustering.Clustering:\n            col_clust = Clustering.None_\n            col_clust_msg = \"Column clustering was disabled due to the \" \\\n                            \"input matrix being to big\"\n\n        self.col_clustering = col_clust\n        self.row_clustering = row_clust\n\n        self.Information.row_clust(row_clust_msg, shown=bool(row_clust_msg))\n        self.Information.col_clust(col_clust_msg, shown=bool(col_clust_msg))\n\n        # Disable/enable the combobox items for the clustering methods\n        def setenabled(cb: QComboBox, clu: bool, clu_op: bool):\n            model = cb.model()\n            assert isinstance(model, QStandardItemModel)\n            idx = cb.findData(Clustering.OrderedClustering, ClusteringRole)\n            assert idx != -1\n            model.item(idx).setEnabled(clu_op)\n            idx = cb.findData(Clustering.Clustering, ClusteringRole)\n            assert idx != -1\n            model.item(idx).setEnabled(clu)\n\n        setenabled(self.row_cluster_cb, rc_enabled, rco_enabled)\n        setenabled(self.col_cluster_cb, cc_enabled, cco_enabled)\n\n    def update_averages_stripe(self):\n        \"\"\"Update the visibility of the averages stripe.\n        \"\"\"\n        widget = self.scene.widget\n        if widget is not None:\n            widget.setShowAverages(self.averages)\n\n    def update_lowslider(self):\n        low, high = self.controls.threshold_low, self.controls.threshold_high\n        if low.value() >= high.value():\n            low.setSliderPosition(high.value() - 1)\n        self.update_color_schema()\n\n    def update_highslider(self):\n        low, high = self.controls.threshold_low, self.controls.threshold_high\n        if low.value() >= high.value():\n            high.setSliderPosition(low.value() + 1)\n        self.update_color_schema()\n\n    def update_color_schema(self):\n        self.palette_name = self.color_cb.currentData().name\n        w = self.scene.widget\n        if w is not None:\n            w.setColorMap(self.color_map())\n\n    def __update_column_clustering(self):\n        self.update_heatmaps()\n        self.commit()\n\n    def __update_row_clustering(self):\n        self.update_heatmaps()\n        self.commit()\n\n    def update_legend(self):\n        widget = self.scene.widget\n        if widget is not None:\n            widget.setLegendVisible(self.legend)\n\n    def row_annotation_var(self):\n        return self.annotation_var\n\n    def row_annotation_data(self):\n        var = self.row_annotation_var()\n        if var is None:\n            return None\n        return column_str_from_table(self.input_data, var)\n\n    def _merge_row_indices(self):\n        if self.merge_kmeans and self.kmeans_model is not None:\n            return self.merge_indices\n        else:\n            return None\n\n    def set_annotation_var(self, var: Union[None, Variable, int]):\n        if isinstance(var, int):\n            var = self.annotation_model[var]\n        if self.annotation_var != var:\n            self.annotation_var = var\n            self.update_annotations()\n\n    def update_annotations(self):\n        widget = self.scene.widget\n        if widget is not None:\n            annot_col = self.row_annotation_data()\n            merge_indices = self._merge_row_indices()\n            if merge_indices is not None and annot_col is not None:\n                join = lambda _1: join_elided(\", \", 42, _1, \" ({} more)\")\n                annot_col = aggregate_apply(join, annot_col, merge_indices)\n            if annot_col is not None:\n                widget.setRowLabels(annot_col)\n                widget.setRowLabelsVisible(True)\n            else:\n                widget.setRowLabelsVisible(False)\n                widget.setRowLabels(None)\n\n    def row_side_colors(self):\n        var = self.annotation_color_var\n        if var is None:\n            return None\n        column_data = column_data_from_table(self.input_data, var)\n        span = (np.nanmin(column_data), np.nanmax(column_data))\n        merges = self._merge_row_indices()\n        if merges is not None:\n            column_data = aggregate(var, column_data, merges)\n        data, colormap = self._colorize(var, column_data)\n        if var.is_continuous:\n            colormap.span = span\n        return data, colormap, var\n\n    def set_annotation_color_var(self, var: Union[None, Variable, int]):\n        \"\"\"Set the current side color annotation variable.\"\"\"\n        if isinstance(var, int):\n            var = self.row_side_color_model[var]\n        if self.annotation_color_var != var:\n            self.annotation_color_var = var\n            self.update_row_side_colors()\n\n    def update_row_side_colors(self):\n        widget = self.scene.widget\n        if widget is None:\n            return\n        colors = self.row_side_colors()\n        if colors is None:\n            widget.setRowSideColorAnnotations(None)\n        else:\n            widget.setRowSideColorAnnotations(colors[0], colors[1], colors[2].name)\n\n    def _colorize(self, var: Variable, data: np.ndarray) -> Tuple[np.ndarray, ColorMap]:\n        palette = var.palette  # type: Palette\n        colors = np.array(\n            [[c.red(), c.green(), c.blue()] for c in palette.qcolors_w_nan],\n            dtype=np.uint8,\n        )\n        if var.is_discrete:\n            mask = np.isnan(data)\n            data[mask] = -1\n            data = data.astype(int)\n            if mask.any():\n                values = (*var.values, \"N/A\")\n            else:\n                values = var.values\n                colors = colors[: -1]\n            return data, CategoricalColorMap(colors, values)\n        elif var.is_continuous:\n            cmap = GradientColorMap(colors[:-1])\n            return data, cmap\n        else:\n            raise TypeError\n\n    def update_column_annotations(self):\n        widget = self.scene.widget\n        if self.data is not None and widget is not None:\n            widget.setColumnLabelsPosition(self._column_label_pos)\n\n    def __adjust_font_size(self, diff):\n        widget = self.scene.widget\n        if widget is None:\n            return\n        curr = widget.font().pointSizeF()\n        new = curr + diff\n\n        self.__font_dec.setEnabled(new > 1.0)\n        self.__font_inc.setEnabled(new <= 32)\n        if new > 1.0:\n            font = QFont()\n            font.setPointSizeF(new)\n            widget.setFont(font)\n\n    def _on_view_context_menu(self, pos):\n        widget = self.scene.widget\n        if widget is None:\n            return\n        assert isinstance(widget, HeatmapGridWidget)\n        menu = QMenu(self.view.viewport())\n        menu.setAttribute(Qt.WA_DeleteOnClose)\n        menu.addActions(self.view.actions())\n        menu.addSeparator()\n        menu.addActions([self.__font_inc, self.__font_dec])\n        menu.addSeparator()\n        a = QAction(\"Keep aspect ratio\", menu, checkable=True)\n        a.setChecked(self.keep_aspect)\n\n        def ontoggled(state):\n            self.keep_aspect = state\n            self.__aspect_mode_changed()\n        a.toggled.connect(ontoggled)\n        menu.addAction(a)\n        menu.popup(self.view.viewport().mapToGlobal(pos))\n\n    def on_selection_finished(self):\n        if self.scene.widget is not None:\n            self.selected_rows = list(self.scene.widget.selectedRows())\n        else:\n            self.selected_rows = []\n        self.commit()\n\n    def commit(self):\n        data = None\n        indices = None\n        if self.merge_kmeans:\n            merge_indices = self.merge_indices\n        else:\n            merge_indices = None\n\n        if self.input_data is not None and self.selected_rows:\n            indices = self.selected_rows\n            if merge_indices is not None:\n                # expand merged indices\n                indices = np.hstack([merge_indices[i] for i in indices])\n\n            data = self.input_data[indices]\n\n        summary = len(data) if data else self.info.NoOutput\n        details = format_summary_details(data) if data else \"\"\n        self.info.set_output_summary(summary, details)\n        self.Outputs.selected_data.send(data)\n        self.Outputs.annotated_data.send(create_annotated_table(self.input_data, indices))\n\n    def onDeleteWidget(self):\n        self.clear()\n        super().onDeleteWidget()\n\n    def send_report(self):\n        self.report_items((\n            (\"Columns:\", \"Clustering\" if self.col_clustering else \"No sorting\"),\n            (\"Rows:\", \"Clustering\" if self.row_clustering else \"No sorting\"),\n            (\"Split:\",\n             self.split_by_var is not None and self.split_by_var.name),\n            (\"Row annotation\",\n             self.annotation_var is not None and self.annotation_var.name),\n        ))\n        self.report_plot()\n\n    @classmethod\n    def migrate_settings(cls, settings, version):\n        if version is not None and version < 3:\n            def st2cl(state: bool) -> Clustering:\n                return Clustering.OrderedClustering if state else \\\n                    Clustering.None_\n\n            rc = settings.pop(\"row_clustering\", False)\n            cc = settings.pop(\"col_clustering\", False)\n            settings[\"row_clustering_method\"] = st2cl(rc).name\n            settings[\"col_clustering_method\"] = st2cl(cc).name\n\n\n# If StickyGraphicsView ever defines qt signals/slots/properties this will\n# break\nclass GraphicsView(GraphicsWidgetView, StickyGraphicsView):\n    pass\n\n\nclass RowPart(NamedTuple):\n    \"\"\"\n    A row group\n\n    Attributes\n    ----------\n    title: str\n        Group title\n    indices : (N, ) Sequence[int]\n        Indices in the input data to retrieve the row subset for the group.\n    cluster : hierarchical.Tree optional\n    cluster_ordered : hierarchical.Tree optional\n    \"\"\"\n    title: str\n    indices: Sequence[int]\n    cluster: Optional[hierarchical.Tree] = None\n    cluster_ordered: Optional[hierarchical.Tree] = None\n\n    @property\n    def can_cluster(self) -> bool:\n        if isinstance(self.indices, slice):\n            return (self.indices.stop - self.indices.start) > 1\n        else:\n            return len(self.indices) > 1\n\n\nclass ColumnPart(NamedTuple):\n    \"\"\"\n    A column group\n\n    Attributes\n    ----------\n    title : str\n        Column group title\n    indices : (N, ) int ndarray\n        Indexes the input data to retrieve the column subset for the group.\n    domain : List[Variable]\n        List of variables in the group.\n    cluster : hierarchical.Tree optional\n    cluster_ordered : hierarchical.Tree optional\n    \"\"\"\n    title: str\n    indices: Sequence[int]\n    domain: Sequence[int]\n    cluster: Optional[hierarchical.Tree] = None\n    cluster_ordered: Optional[hierarchical.Tree] = None\n\n\nclass Parts(NamedTuple):\n    rows: Sequence[RowPart]\n    columns: Sequence[ColumnPart]\n    span: Tuple[float, float]\n\n\ndef join_elided(sep, maxlen, values, elidetemplate=\"...\"):\n    def generate(sep, ellidetemplate, values):\n        count = len(values)\n        length = 0\n        parts = []\n        for i, val in enumerate(values):\n            elide = ellidetemplate.format(count - i) if count - i > 1 else \"\"\n            parts.append(val)\n            length += len(val) + (len(sep) if parts else 0)\n            yield i, islice(parts, i + 1), length, elide\n\n    best = None\n    for _, parts, length, elide in generate(sep, elidetemplate, values):\n        if length > maxlen:\n            if best is None:\n                best = sep.join(parts) + elide\n            return best\n        fulllen = length + len(elide)\n        if fulllen < maxlen or best is None:\n            best = sep.join(parts) + elide\n    return best\n\n\ndef column_str_from_table(\n        table: Orange.data.Table,\n        column: Union[int, Orange.data.Variable],\n) -> np.ndarray:\n    var = table.domain[column]\n    data, _ = table.get_column_view(column)\n    return np.asarray([var.str_val(v) for v in data], dtype=object)\n\n\ndef column_data_from_table(\n        table: Orange.data.Table,\n        column: Union[int, Orange.data.Variable],\n) -> np.ndarray:\n    var = table.domain[column]\n    data, _ = table.get_column_view(column)\n    if var.is_primitive() and data.dtype.kind != \"f\":\n        data = data.astype(float)\n    return data\n\n\ndef aggregate(\n        var: Variable, data: np.ndarray, groupindices: Sequence[Sequence[int]],\n) -> np.ndarray:\n    if var.is_string:\n        join = lambda values: (join_elided(\", \", 42, values, \" ({} more)\"))\n        # collect all original labels for every merged row\n        values = [data[indices] for indices in groupindices]\n        data = [join(list(map(var.str_val, vals))) for vals in values]\n        return np.array(data, dtype=object)\n    elif var.is_continuous:\n        data = [np.nanmean(data[indices]) if len(indices) else np.nan\n                for indices in groupindices]\n        return np.array(data, dtype=float)\n    elif var.is_discrete:\n        from Orange.statistics.util import nanmode\n        data = [nanmode(data[indices])[0] if len(indices) else np.nan\n                for indices in groupindices]\n        return np.asarray(data, dtype=float)\n    else:\n        raise TypeError(type(var))\n\n\ndef agg_join_str(var, data, groupindices, maxlen=50, elidetemplate=\" ({} more)\"):\n    join_s = lambda values: (\n        join_elided(\", \", maxlen, values, elidetemplate=elidetemplate)\n    )\n    join = lambda values: join_s(map(var.str_val, values))\n    return aggregate_apply(join, data, groupindices)\n\n\n_T = TypeVar(\"_T\")\n\n\ndef aggregate_apply(\n        f: Callable[[Sequence], _T],\n        data: np.ndarray,\n        groupindices: Sequence[Sequence[int]]\n) -> Sequence[_T]:\n    return [f(data[indices]) for indices in groupindices]\n\n\nif __name__ == \"__main__\":  # pragma: no cover\n    WidgetPreview(OWHeatMap).run(Table(\"brown-selected.tab\"))\n",
            "header": {
                "file": {
                    "path": "Orange/widgets/visualize/owheatmap.py"
                },
                "misc_line": null
            },
            "index_line": {
                "old_commit_hash": "5d1aabf8b",
                "new_commit_hash": "6a28b7c9f",
                "mode": "100644"
            },
            "is_binary_file": false,
            "binary_line": null,
            "minus_file": {
                "path": "a/Orange/widgets/visualize/owheatmap.py"
            },
            "plus_file": {
                "path": "b/Orange/widgets/visualize/owheatmap.py"
            },
            "hunks": [
                {
                    "descriptor": {
                        "old_range": {
                            "start": 200,
                            "length": 8
                        },
                        "new_range": {
                            "start": 200,
                            "length": 6
                        },
                        "section": "class OWHeatMap(widget.OWWidget):"
                    },
                    "line_group": {
                        "all_lines": [
                            {
                                "content": "",
                                "type": "context"
                            },
                            {
                                "content": "    class Warning(widget.OWWidget.Warning):",
                                "type": "context"
                            },
                            {
                                "content": "        empty_clusters = Msg(\"Empty clusters were removed\")",
                                "type": "context"
                            },
                            {
                                "content": "        missing_split_values = Msg(",
                                "type": "deleted"
                            },
                            {
                                "content": "            \"Instances with undefined split variable values are not shown\")",
                                "type": "deleted"
                            },
                            {
                                "content": "",
                                "type": "context"
                            },
                            {
                                "content": "    def __init__(self):",
                                "type": "context"
                            },
                            {
                                "content": "        super().__init__()",
                                "type": "context"
                            }
                        ]
                    },
                    "modified_entities": [
                        {
                            "file_name": "Orange/widgets/visualize/owheatmap.py",
                            "type": "class",
                            "name": "OWHeatMap",
                            "content": "class OWHeatMap(widget.OWWidget):\n    name = \"Heat Map\"\n    description = \"Plot a data matrix heatmap.\"\n    icon = \"icons/Heatmap.svg\"\n    priority = 260\n    keywords = []\n\n    class Inputs:\n        data = Input(\"Data\", Table)\n\n    class Outputs:\n        selected_data = Output(\"Selected Data\", Table, default=True)\n        annotated_data = Output(ANNOTATED_DATA_SIGNAL_NAME, Table)\n\n    settings_version = 3\n\n    settingsHandler = settings.DomainContextHandler()\n\n    # Disable clustering for inputs bigger than this\n    MaxClustering = 25000\n    # Disable cluster leaf ordering for inputs bigger than this\n    MaxOrderedClustering = 1000\n\n    threshold_low = settings.Setting(0.0)\n    threshold_high = settings.Setting(1.0)\n\n    merge_kmeans = settings.Setting(False)\n    merge_kmeans_k = settings.Setting(50)\n\n    # Display column with averages\n    averages: bool = settings.Setting(True)\n    # Display legend\n    legend: bool = settings.Setting(True)\n    # Annotations\n    #: text row annotation (row names)\n    annotation_var = settings.ContextSetting(None)\n    #: color row annotation\n    annotation_color_var = settings.ContextSetting(None)\n    # Discrete variable used to split that data/heatmaps (vertically)\n    split_by_var = settings.ContextSetting(None)\n    # Selected row/column clustering method (name)\n    col_clustering_method: str = settings.Setting(Clustering.None_.name)\n    row_clustering_method: str = settings.Setting(Clustering.None_.name)\n\n    palette_name = settings.Setting(colorpalettes.DefaultContinuousPaletteName)\n    column_label_pos: int = settings.Setting(1)\n    selected_rows: List[int] = settings.Setting(None, schema_only=True)\n\n    auto_commit = settings.Setting(True)\n\n    graph_name = \"scene\"\n\n    left_side_scrolling = True\n\n    class Information(widget.OWWidget.Information):\n        sampled = Msg(\"Data has been sampled\")\n        discrete_ignored = Msg(\"{} categorical feature{} ignored\")\n        row_clust = Msg(\"{}\")\n        col_clust = Msg(\"{}\")\n        sparse_densified = Msg(\"Showing this data may require a lot of memory\")\n\n    class Error(widget.OWWidget.Error):\n        no_continuous = Msg(\"No numeric features\")\n        not_enough_features = Msg(\"Not enough features for column clustering\")\n        not_enough_instances = Msg(\"Not enough instances for clustering\")\n        not_enough_instances_k_means = Msg(\n            \"Not enough instances for k-means merging\")\n        not_enough_memory = Msg(\"Not enough memory to show this data\")\n\n    class Warning(widget.OWWidget.Warning):\n        empty_clusters = Msg(\"Empty clusters were removed\")\n        missing_split_values = Msg(\n            \"Instances with undefined split variable values are not shown\")\n\n    def __init__(self):\n        super().__init__()\n        self.__pending_selection = self.selected_rows\n\n        # A kingdom for a save_state/restore_state\n        self.col_clustering = enum_get(\n            Clustering, self.col_clustering_method, Clustering.None_)\n        self.row_clustering = enum_get(\n            Clustering, self.row_clustering_method, Clustering.None_)\n\n        @self.settingsAboutToBePacked.connect\n        def _():\n            self.col_clustering_method = self.col_clustering.name\n            self.row_clustering_method = self.row_clustering.name\n\n        self.keep_aspect = False\n\n        #: The original data with all features (retained to\n        #: preserve the domain on the output)\n        self.input_data = None\n        #: The effective data striped of discrete features, and often\n        #: merged using k-means\n        self.data = None\n        self.effective_data = None\n        #: kmeans model used to merge rows of input_data\n        self.kmeans_model = None\n        #: merge indices derived from kmeans\n        #: a list (len==k) of int ndarray where the i-th item contains\n        #: the indices which merge the input_data into the heatmap row i\n        self.merge_indices = None\n        self.parts: Optional[Parts] = None\n        self.__rows_cache = {}\n        self.__columns_cache = {}\n\n        self.info.set_input_summary(self.info.NoInput)\n        self.info.set_output_summary(self.info.NoOutput)\n\n        # GUI definition\n        colorbox = gui.vBox(self.controlArea, \"Color\")\n        self.color_cb = gui.palette_combo_box(self.palette_name)\n        self.color_cb.currentIndexChanged.connect(self.update_color_schema)\n        colorbox.layout().addWidget(self.color_cb)\n\n        form = QFormLayout(\n            formAlignment=Qt.AlignLeft,\n            labelAlignment=Qt.AlignLeft,\n            fieldGrowthPolicy=QFormLayout.AllNonFixedFieldsGrow\n        )\n        lowslider = gui.hSlider(\n            colorbox, self, \"threshold_low\", minValue=0.0, maxValue=1.0,\n            step=0.05, ticks=True, intOnly=False,\n            createLabel=False, callback=self.update_lowslider)\n        highslider = gui.hSlider(\n            colorbox, self, \"threshold_high\", minValue=0.0, maxValue=1.0,\n            step=0.05, ticks=True, intOnly=False,\n            createLabel=False, callback=self.update_highslider)\n\n        form.addRow(\"Low:\", lowslider)\n        form.addRow(\"High:\", highslider)\n\n        colorbox.layout().addLayout(form)\n\n        mergebox = gui.vBox(self.controlArea, \"Merge\",)\n        gui.checkBox(mergebox, self, \"merge_kmeans\", \"Merge by k-means\",\n                     callback=self.__update_row_clustering)\n        ibox = gui.indentedBox(mergebox)\n        gui.spin(ibox, self, \"merge_kmeans_k\", minv=5, maxv=500,\n                 label=\"Clusters:\", keyboardTracking=False,\n                 callbackOnReturn=True, callback=self.update_merge)\n\n        cluster_box = gui.vBox(self.controlArea, \"Clustering\")\n        # Row clustering\n        self.row_cluster_cb = cb = ComboBox()\n        cb.setModel(create_list_model(ClusteringModelData, self))\n        cbselect(cb, self.row_clustering, ClusteringRole)\n        self.connect_control(\n            \"row_clustering\",\n            lambda value, cb=cb: cbselect(cb, value, ClusteringRole)\n        )\n        @cb.activated.connect\n        def _(idx, cb=cb):\n            self.set_row_clustering(cb.itemData(idx, ClusteringRole))\n\n        # Column clustering\n        self.col_cluster_cb = cb = ComboBox()\n        cb.setModel(create_list_model(ClusteringModelData, self))\n        cbselect(cb, self.col_clustering, ClusteringRole)\n        self.connect_control(\n            \"col_clustering\",\n            lambda value, cb=cb: cbselect(cb, value, ClusteringRole)\n        )\n        @cb.activated.connect\n        def _(idx, cb=cb):\n            self.set_col_clustering(cb.itemData(idx, ClusteringRole))\n\n        form = QFormLayout(\n            labelAlignment=Qt.AlignLeft, formAlignment=Qt.AlignLeft,\n            fieldGrowthPolicy=QFormLayout.AllNonFixedFieldsGrow,\n        )\n        form.addRow(\"Rows:\", self.row_cluster_cb)\n        form.addRow(\"Columns:\", self.col_cluster_cb)\n        cluster_box.layout().addLayout(form)\n        box = gui.vBox(self.controlArea, \"Split By\")\n\n        self.row_split_model = DomainModel(\n            placeholder=\"(None)\",\n            valid_types=(Orange.data.DiscreteVariable,),\n            parent=self,\n        )\n        self.row_split_cb = cb = ComboBoxSearch(\n            enabled=not self.merge_kmeans,\n            sizeAdjustPolicy=ComboBox.AdjustToMinimumContentsLengthWithIcon,\n            minimumContentsLength=14,\n            toolTip=\"Split the heatmap vertically by a categorical column\"\n        )\n        self.row_split_cb.setModel(self.row_split_model)\n        self.connect_control(\n            \"split_by_var\", lambda value, cb=cb: cbselect(cb, value)\n        )\n        self.connect_control(\n            \"merge_kmeans\", self.row_split_cb.setDisabled\n        )\n        self.split_by_var = None\n\n        self.row_split_cb.activated.connect(\n            self.__on_split_rows_activated\n        )\n        box.layout().addWidget(self.row_split_cb)\n\n        box = gui.vBox(self.controlArea, 'Annotation && Legends')\n\n        gui.checkBox(box, self, 'legend', 'Show legend',\n                     callback=self.update_legend)\n\n        gui.checkBox(box, self, 'averages', 'Stripes with averages',\n                     callback=self.update_averages_stripe)\n        annotbox = QGroupBox(\"Row Annotations\", flat=True)\n        form = QFormLayout(\n            annotbox,\n            formAlignment=Qt.AlignLeft,\n            labelAlignment=Qt.AlignLeft,\n            fieldGrowthPolicy=QFormLayout.AllNonFixedFieldsGrow\n        )\n        self.annotation_model = DomainModel(placeholder=\"(None)\")\n        self.annotation_text_cb = ComboBoxSearch(\n            minimumContentsLength=12,\n            sizeAdjustPolicy=QComboBox.AdjustToMinimumContentsLength\n        )\n        self.annotation_text_cb.setModel(self.annotation_model)\n        self.annotation_text_cb.activated.connect(self.set_annotation_var)\n        self.connect_control(\"annotation_var\", self.annotation_var_changed)\n\n        self.row_side_color_model = DomainModel(\n            order=(DomainModel.CLASSES, DomainModel.Separator,\n                   DomainModel.METAS),\n            placeholder=\"(None)\", valid_types=DomainModel.PRIMITIVE,\n            flags=Qt.ItemIsSelectable | Qt.ItemIsEnabled,\n            parent=self,\n        )\n        self.row_side_color_cb = ComboBoxSearch(\n            sizeAdjustPolicy=QComboBox.AdjustToMinimumContentsLength,\n            minimumContentsLength=12\n        )\n        self.row_side_color_cb.setModel(self.row_side_color_model)\n        self.row_side_color_cb.activated.connect(self.set_annotation_color_var)\n        self.connect_control(\"annotation_color_var\", self.annotation_color_var_changed)\n        form.addRow(\"Text\", self.annotation_text_cb)\n        form.addRow(\"Color\", self.row_side_color_cb)\n        box.layout().addWidget(annotbox)\n        posbox = gui.vBox(box, \"Column Labels Position\", addSpace=False)\n        posbox.setFlat(True)\n        cb = gui.comboBox(\n            posbox, self, \"column_label_pos\",\n            callback=self.update_column_annotations)\n        cb.setModel(create_list_model(ColumnLabelsPosData, parent=self))\n        cb.setCurrentIndex(self.column_label_pos)\n        gui.checkBox(self.controlArea, self, \"keep_aspect\",\n                     \"Keep aspect ratio\", box=\"Resize\",\n                     callback=self.__aspect_mode_changed)\n\n        gui.rubber(self.controlArea)\n        gui.auto_send(self.controlArea, self, \"auto_commit\")\n\n        # Scene with heatmap\n        class HeatmapScene(QGraphicsScene):\n            widget: Optional[HeatmapGridWidget] = None\n\n        self.scene = self.scene = HeatmapScene(parent=self)\n        self.view = GraphicsView(\n            self.scene,\n            verticalScrollBarPolicy=Qt.ScrollBarAlwaysOn,\n            horizontalScrollBarPolicy=Qt.ScrollBarAlwaysOn,\n            viewportUpdateMode=QGraphicsView.FullViewportUpdate,\n            widgetResizable=True,\n        )\n        self.view.setContextMenuPolicy(Qt.CustomContextMenu)\n        self.view.customContextMenuRequested.connect(\n            self._on_view_context_menu\n        )\n        self.mainArea.layout().addWidget(self.view)\n        self.selected_rows = []\n        self.__font_inc = QAction(\n            \"Increase Font\", self, shortcut=QKeySequence(\"ctrl+>\"))\n        self.__font_dec = QAction(\n            \"Decrease Font\", self, shortcut=QKeySequence(\"ctrl+<\"))\n        self.__font_inc.triggered.connect(lambda: self.__adjust_font_size(1))\n        self.__font_dec.triggered.connect(lambda: self.__adjust_font_size(-1))\n        if hasattr(QAction, \"setShortcutVisibleInContextMenu\"):\n            apply_all(\n                [self.__font_inc, self.__font_dec],\n                lambda a: a.setShortcutVisibleInContextMenu(True)\n            )\n        self.addActions([self.__font_inc, self.__font_dec])\n\n    @property\n    def center_palette(self):\n        palette = self.color_cb.currentData()\n        return bool(palette.flags & palette.Diverging)\n\n    @property\n    def _column_label_pos(self) -> HeatmapGridWidget.Position:\n        return ColumnLabelsPosData[self.column_label_pos][Qt.UserRole]\n\n    def annotation_color_var_changed(self, value):\n        cbselect(self.row_side_color_cb, value, Qt.EditRole)\n\n    def annotation_var_changed(self, value):\n        cbselect(self.annotation_text_cb, value, Qt.EditRole)\n\n    def set_row_clustering(self, method: Clustering) -> None:\n        assert isinstance(method, Clustering)\n        if self.row_clustering != method:\n            self.row_clustering = method\n            cbselect(self.row_cluster_cb, method, ClusteringRole)\n            self.__update_row_clustering()\n\n    def set_col_clustering(self, method: Clustering) -> None:\n        assert isinstance(method, Clustering)\n        if self.col_clustering != method:\n            self.col_clustering = method\n            cbselect(self.col_cluster_cb, method, ClusteringRole)\n            self.__update_column_clustering()\n\n    def sizeHint(self) -> QSize:\n        return super().sizeHint().expandedTo(QSize(900, 700))\n\n    def color_palette(self):\n        return self.color_cb.currentData().lookup_table()\n\n    def color_map(self) -> GradientColorMap:\n        return GradientColorMap(\n            self.color_palette(), (self.threshold_low, self.threshold_high),\n            0 if self.center_palette else None\n        )\n\n    def clear(self):\n        self.data = None\n        self.input_data = None\n        self.effective_data = None\n        self.kmeans_model = None\n        self.merge_indices = None\n        self.annotation_model.set_domain(None)\n        self.annotation_var = None\n        self.row_side_color_model.set_domain(None)\n        self.annotation_color_var = None\n        self.row_split_model.set_domain(None)\n        self.split_by_var = None\n        self.parts = None\n        self.clear_scene()\n        self.selected_rows = []\n        self.__columns_cache.clear()\n        self.__rows_cache.clear()\n        self.__update_clustering_enable_state(None)\n\n    def clear_scene(self):\n        if self.scene.widget is not None:\n            self.scene.widget.layoutDidActivate.disconnect(\n                self.__on_layout_activate\n            )\n            self.scene.widget.selectionFinished.disconnect(\n                self.on_selection_finished\n            )\n        self.scene.widget = None\n        self.scene.clear()\n\n        self.view.setSceneRect(QRectF())\n        self.view.setHeaderSceneRect(QRectF())\n        self.view.setFooterSceneRect(QRectF())\n\n    @Inputs.data\n    def set_dataset(self, data=None):\n        \"\"\"Set the input dataset to display.\"\"\"\n        self.closeContext()\n        self.clear()\n        self.clear_messages()\n        self._set_input_summary(data)\n\n        if isinstance(data, SqlTable):\n            if data.approx_len() < 4000:\n                data = Table(data)\n            else:\n                self.Information.sampled()\n                data_sample = data.sample_time(1, no_cache=True)\n                data_sample.download_data(2000, partial=True)\n                data = Table(data_sample)\n\n        if data is not None and not len(data):\n            data = None\n\n        if data is not None and sp.issparse(data.X):\n            try:\n                data = data.to_dense()\n            except MemoryError:\n                data = None\n                self.Error.not_enough_memory()\n            else:\n                self.Information.sparse_densified()\n\n        input_data = data\n\n        # Data contains no attributes or meta attributes only\n        if data is not None and len(data.domain.attributes) == 0:\n            self.Error.no_continuous()\n            input_data = data = None\n\n        # Data contains some discrete attributes which must be filtered\n        if data is not None and \\\n                any(var.is_discrete for var in data.domain.attributes):\n            ndisc = sum(var.is_discrete for var in data.domain.attributes)\n            data = data.transform(\n                Domain([var for var in data.domain.attributes\n                        if var.is_continuous],\n                       data.domain.class_vars,\n                       data.domain.metas))\n            if not data.domain.attributes:\n                self.Error.no_continuous()\n                input_data = data = None\n            else:\n                self.Information.discrete_ignored(\n                    ndisc, \"s\" if ndisc > 1 else \"\")\n\n        self.data = data\n        self.input_data = input_data\n\n        if data is not None:\n            self.annotation_model.set_domain(self.input_data.domain)\n            self.row_side_color_model.set_domain(self.input_data.domain)\n            self.annotation_var = None\n            self.annotation_color_var = None\n            self.row_split_model.set_domain(data.domain)\n            if data.domain.has_discrete_class:\n                self.split_by_var = data.domain.class_var\n            else:\n                self.split_by_var = None\n            self.openContext(self.input_data)\n            if self.split_by_var not in self.row_split_model:\n                self.split_by_var = None\n\n        self.update_heatmaps()\n        if data is not None and self.__pending_selection is not None:\n            assert self.scene.widget is not None\n            self.scene.widget.selectRows(self.__pending_selection)\n            self.selected_rows = self.__pending_selection\n            self.__pending_selection = None\n\n        self.unconditional_commit()\n\n    def _set_input_summary(self, data):\n        summary = len(data) if data else self.info.NoInput\n        details = format_summary_details(data) if data else \"\"\n        self.info.set_input_summary(summary, details)\n\n    def __on_split_rows_activated(self):\n        self.set_split_variable(self.row_split_cb.currentData(Qt.EditRole))\n\n    def set_split_variable(self, var):\n        if var != self.split_by_var:\n            self.split_by_var = var\n            self.update_heatmaps()\n\n    def update_heatmaps(self):\n        if self.data is not None:\n            self.clear_scene()\n            self.clear_messages()\n            if self.col_clustering != Clustering.None_ and \\\n                    len(self.data.domain.attributes) < 2:\n                self.Error.not_enough_features()\n            elif (self.col_clustering != Clustering.None_ or\n                  self.row_clustering != Clustering.None_) and \\\n                    len(self.data) < 2:\n                self.Error.not_enough_instances()\n            elif self.merge_kmeans and len(self.data) < 3:\n                self.Error.not_enough_instances_k_means()\n            else:\n                parts = self.construct_heatmaps(self.data, self.split_by_var)\n                self.construct_heatmaps_scene(parts, self.effective_data)\n                self.selected_rows = []\n        else:\n            self.clear()\n        if self.parts is not None and self.effective_data is not None:\n            nrows = sum(len(p.indices) for p in self.parts.rows)\n            self.Warning.missing_split_values(\n                shown=nrows < len(self.effective_data))\n        else:\n            self.Warning.missing_split_values.clear()\n\n    def update_merge(self):\n        self.kmeans_model = None\n        self.merge_indices = None\n        if self.data is not None and self.merge_kmeans:\n            self.update_heatmaps()\n            self.commit()\n\n    def _make_parts(self, data, group_var=None):\n        \"\"\"\n        Make initial `Parts` for data, split by group_var, group_key\n        \"\"\"\n        if group_var is not None:\n            assert group_var.is_discrete\n            _col_data = table_column_data(data, group_var)\n            row_indices = [np.flatnonzero(_col_data == i)\n                           for i in range(len(group_var.values))]\n            row_groups = [RowPart(title=name, indices=ind,\n                                  cluster=None, cluster_ordered=None)\n                          for name, ind in zip(group_var.values, row_indices)]\n        else:\n            row_groups = [RowPart(title=None, indices=range(0, len(data)),\n                                  cluster=None, cluster_ordered=None)]\n\n        col_groups = [\n            ColumnPart(\n                title=None, indices=range(0, len(data.domain.attributes)),\n                domain=data.domain, cluster=None, cluster_ordered=None)\n        ]\n\n        minv, maxv = np.nanmin(data.X), np.nanmax(data.X)\n        return Parts(row_groups, col_groups, span=(minv, maxv))\n\n    def cluster_rows(self, data: Table, parts: 'Parts', ordered=False) -> 'Parts':\n        row_groups = []\n        for row in parts.rows:\n            if row.cluster is not None:\n                cluster = row.cluster\n            else:\n                cluster = None\n            if row.cluster_ordered is not None:\n                cluster_ord = row.cluster_ordered\n            else:\n                cluster_ord = None\n\n            if row.can_cluster:\n                matrix = None\n                need_dist = cluster is None or (ordered and cluster_ord is None)\n                if need_dist:\n                    subset = data[row.indices]\n                    matrix = Orange.distance.Euclidean(subset)\n\n                if cluster is None:\n                    assert len(matrix) < self.MaxClustering\n                    cluster = hierarchical.dist_matrix_clustering(\n                        matrix, linkage=hierarchical.WARD\n                    )\n                if ordered and cluster_ord is None:\n                    assert len(matrix) < self.MaxOrderedClustering\n                    cluster_ord = hierarchical.optimal_leaf_ordering(\n                        cluster, matrix,\n                    )\n            row_groups.append(row._replace(cluster=cluster, cluster_ordered=cluster_ord))\n\n        return parts._replace(rows=row_groups)\n\n    def cluster_columns(self, data, parts, ordered=False):\n        assert len(parts.columns) == 1, \"columns split is no longer supported\"\n        assert all(var.is_continuous for var in data.domain.attributes)\n\n        col0 = parts.columns[0]\n        if col0.cluster is not None:\n            cluster = col0.cluster\n        else:\n            cluster = None\n        if col0.cluster_ordered is not None:\n            cluster_ord = col0.cluster_ordered\n        else:\n            cluster_ord = None\n        need_dist = cluster is None or (ordered and cluster_ord is None)\n        matrix = None\n        if need_dist:\n            data = Orange.distance._preprocess(data)\n            matrix = np.asarray(Orange.distance.PearsonR(data, axis=0))\n            # nan values break clustering below\n            matrix = np.nan_to_num(matrix)\n\n        if cluster is None:\n            assert matrix is not None\n            assert len(matrix) < self.MaxClustering\n            cluster = hierarchical.dist_matrix_clustering(\n                matrix, linkage=hierarchical.WARD\n            )\n        if ordered and cluster_ord is None:\n            assert len(matrix) < self.MaxOrderedClustering\n            cluster_ord = hierarchical.optimal_leaf_ordering(cluster, matrix)\n\n        col_groups = [col._replace(cluster=cluster, cluster_ordered=cluster_ord)\n                      for col in parts.columns]\n        return parts._replace(columns=col_groups)\n\n    def construct_heatmaps(self, data, group_var=None) -> 'Parts':\n        if self.merge_kmeans:\n            if self.kmeans_model is None:\n                effective_data = self.input_data.transform(\n                    Orange.data.Domain(\n                        [var for var in self.input_data.domain.attributes\n                         if var.is_continuous],\n                        self.input_data.domain.class_vars,\n                        self.input_data.domain.metas))\n                nclust = min(self.merge_kmeans_k, len(effective_data) - 1)\n                self.kmeans_model = kmeans_compress(effective_data, k=nclust)\n                effective_data.domain = self.kmeans_model.domain\n                merge_indices = [np.flatnonzero(self.kmeans_model.labels == ind)\n                                 for ind in range(nclust)]\n                not_empty_indices = [i for i, x in enumerate(merge_indices)\n                                     if len(x) > 0]\n                self.merge_indices = \\\n                    [merge_indices[i] for i in not_empty_indices]\n                if len(merge_indices) != len(self.merge_indices):\n                    self.Warning.empty_clusters()\n                effective_data = Orange.data.Table(\n                    Orange.data.Domain(effective_data.domain.attributes),\n                    self.kmeans_model.centroids[not_empty_indices]\n                )\n            else:\n                effective_data = self.effective_data\n\n            group_var = None\n        else:\n            self.kmeans_model = None\n            self.merge_indices = None\n            effective_data = data\n\n        self.effective_data = effective_data\n\n        self.__update_clustering_enable_state(effective_data)\n\n        parts = self._make_parts(effective_data, group_var)\n        # Restore/update the row/columns items descriptions from cache if\n        # available\n        rows_cache_key = (group_var,\n                          self.merge_kmeans_k if self.merge_kmeans else None)\n        if rows_cache_key in self.__rows_cache:\n            parts = parts._replace(rows=self.__rows_cache[rows_cache_key].rows)\n\n        if self.row_clustering != Clustering.None_:\n            parts = self.cluster_rows(\n                effective_data, parts,\n                ordered=self.row_clustering == Clustering.OrderedClustering\n            )\n        if self.col_clustering != Clustering.None_:\n            parts = self.cluster_columns(\n                effective_data, parts,\n                ordered=self.col_clustering == Clustering.OrderedClustering\n            )\n\n        # Cache the updated parts\n        self.__rows_cache[rows_cache_key] = parts\n        return parts\n\n    def construct_heatmaps_scene(self, parts: 'Parts', data: Table) -> None:\n        _T = TypeVar(\"_T\", bound=Union[RowPart, ColumnPart])\n\n        def select_cluster(clustering: Clustering, item: _T) -> _T:\n            if clustering == Clustering.None_:\n                return item._replace(cluster=None, cluster_ordered=None)\n            elif clustering == Clustering.Clustering:\n                return item._replace(cluster=item.cluster, cluster_ordered=None)\n            elif clustering == Clustering.OrderedClustering:\n                return item._replace(cluster=item.cluster_ordered, cluster_ordered=None)\n            else:  # pragma: no cover\n                raise TypeError()\n\n        rows = [select_cluster(self.row_clustering, rowitem)\n                for rowitem in parts.rows]\n        cols = [select_cluster(self.col_clustering, colitem)\n                for colitem in parts.columns]\n        parts = Parts(columns=cols, rows=rows, span=parts.span)\n\n        self.setup_scene(parts, data)\n\n    def setup_scene(self, parts, data):\n        # type: (Parts, Table) -> None\n        widget = HeatmapGridWidget()\n        widget.setColorMap(self.color_map())\n        self.scene.addItem(widget)\n        self.scene.widget = widget\n        columns = [v.name for v in data.domain.attributes]\n        parts = HeatmapGridWidget.Parts(\n            rows=[\n                HeatmapGridWidget.RowItem(r.title, r.indices, r.cluster)\n                for r in parts.rows\n            ],\n            columns=[\n                HeatmapGridWidget.ColumnItem(c.title, c.indices, c.cluster)\n                for c in parts.columns\n            ],\n            data=data.X,\n            span=parts.span,\n            row_names=None,\n            col_names=columns,\n        )\n        widget.setHeatmaps(parts)\n        side = self.row_side_colors()\n        if side is not None:\n            widget.setRowSideColorAnnotations(side[0], side[1], name=side[2].name)\n        widget.setColumnLabelsPosition(self._column_label_pos)\n        widget.setAspectRatioMode(\n            Qt.KeepAspectRatio if self.keep_aspect else Qt.IgnoreAspectRatio\n        )\n        widget.setShowAverages(self.averages)\n        widget.setLegendVisible(self.legend)\n\n        widget.layoutDidActivate.connect(self.__on_layout_activate)\n        widget.selectionFinished.connect(self.on_selection_finished)\n\n        self.update_annotations()\n        self.view.setCentralWidget(widget)\n        self.parts = parts\n\n    def __update_scene_rects(self):\n        widget = self.scene.widget\n        if widget is None:\n            return\n        rect = widget.geometry()\n        self.scene.setSceneRect(rect)\n        self.view.setSceneRect(rect)\n        self.view.setHeaderSceneRect(widget.headerGeometry())\n        self.view.setFooterSceneRect(widget.footerGeometry())\n\n    def __on_layout_activate(self):\n        self.__update_scene_rects()\n\n    def __aspect_mode_changed(self):\n        widget = self.scene.widget\n        if widget is None:\n            return\n        widget.setAspectRatioMode(\n            Qt.KeepAspectRatio if self.keep_aspect else Qt.IgnoreAspectRatio\n        )\n        # when aspect fixed the vertical sh is fixex, when not, it can\n        # shrink vertically\n        sp = widget.sizePolicy()\n        if self.keep_aspect:\n            sp.setVerticalPolicy(QSizePolicy.Fixed)\n        else:\n            sp.setVerticalPolicy(QSizePolicy.Preferred)\n        widget.setSizePolicy(sp)\n\n    def __update_clustering_enable_state(self, data):\n        if data is not None:\n            N = len(data)\n            M = len(data.domain.attributes)\n        else:\n            N = M = 0\n\n        rc_enabled = N <= self.MaxClustering\n        rco_enabled = N <= self.MaxOrderedClustering\n        cc_enabled = M <= self.MaxClustering\n        cco_enabled = M <= self.MaxOrderedClustering\n        row_clust, col_clust = self.row_clustering, self.col_clustering\n\n        row_clust_msg = \"\"\n        col_clust_msg = \"\"\n\n        if not rco_enabled and row_clust == Clustering.OrderedClustering:\n            row_clust = Clustering.Clustering\n            row_clust_msg = \"Row cluster ordering was disabled due to the \" \\\n                            \"input matrix being to big\"\n        if not rc_enabled and row_clust == Clustering.Clustering:\n            row_clust = Clustering.None_\n            row_clust_msg = \"Row clustering was was disabled due to the \" \\\n                            \"input matrix being to big\"\n\n        if not cco_enabled and col_clust == Clustering.OrderedClustering:\n            col_clust = Clustering.Clustering\n            col_clust_msg = \"Column cluster ordering was disabled due to \" \\\n                            \"the input matrix being to big\"\n        if not cc_enabled and col_clust == Clustering.Clustering:\n            col_clust = Clustering.None_\n            col_clust_msg = \"Column clustering was disabled due to the \" \\\n                            \"input matrix being to big\"\n\n        self.col_clustering = col_clust\n        self.row_clustering = row_clust\n\n        self.Information.row_clust(row_clust_msg, shown=bool(row_clust_msg))\n        self.Information.col_clust(col_clust_msg, shown=bool(col_clust_msg))\n\n        # Disable/enable the combobox items for the clustering methods\n        def setenabled(cb: QComboBox, clu: bool, clu_op: bool):\n            model = cb.model()\n            assert isinstance(model, QStandardItemModel)\n            idx = cb.findData(Clustering.OrderedClustering, ClusteringRole)\n            assert idx != -1\n            model.item(idx).setEnabled(clu_op)\n            idx = cb.findData(Clustering.Clustering, ClusteringRole)\n            assert idx != -1\n            model.item(idx).setEnabled(clu)\n\n        setenabled(self.row_cluster_cb, rc_enabled, rco_enabled)\n        setenabled(self.col_cluster_cb, cc_enabled, cco_enabled)\n\n    def update_averages_stripe(self):\n        \"\"\"Update the visibility of the averages stripe.\n        \"\"\"\n        widget = self.scene.widget\n        if widget is not None:\n            widget.setShowAverages(self.averages)\n\n    def update_lowslider(self):\n        low, high = self.controls.threshold_low, self.controls.threshold_high\n        if low.value() >= high.value():\n            low.setSliderPosition(high.value() - 1)\n        self.update_color_schema()\n\n    def update_highslider(self):\n        low, high = self.controls.threshold_low, self.controls.threshold_high\n        if low.value() >= high.value():\n            high.setSliderPosition(low.value() + 1)\n        self.update_color_schema()\n\n    def update_color_schema(self):\n        self.palette_name = self.color_cb.currentData().name\n        w = self.scene.widget\n        if w is not None:\n            w.setColorMap(self.color_map())\n\n    def __update_column_clustering(self):\n        self.update_heatmaps()\n        self.commit()\n\n    def __update_row_clustering(self):\n        self.update_heatmaps()\n        self.commit()\n\n    def update_legend(self):\n        widget = self.scene.widget\n        if widget is not None:\n            widget.setLegendVisible(self.legend)\n\n    def row_annotation_var(self):\n        return self.annotation_var\n\n    def row_annotation_data(self):\n        var = self.row_annotation_var()\n        if var is None:\n            return None\n        return column_str_from_table(self.input_data, var)\n\n    def _merge_row_indices(self):\n        if self.merge_kmeans and self.kmeans_model is not None:\n            return self.merge_indices\n        else:\n            return None\n\n    def set_annotation_var(self, var: Union[None, Variable, int]):\n        if isinstance(var, int):\n            var = self.annotation_model[var]\n        if self.annotation_var != var:\n            self.annotation_var = var\n            self.update_annotations()\n\n    def update_annotations(self):\n        widget = self.scene.widget\n        if widget is not None:\n            annot_col = self.row_annotation_data()\n            merge_indices = self._merge_row_indices()\n            if merge_indices is not None and annot_col is not None:\n                join = lambda _1: join_elided(\", \", 42, _1, \" ({} more)\")\n                annot_col = aggregate_apply(join, annot_col, merge_indices)\n            if annot_col is not None:\n                widget.setRowLabels(annot_col)\n                widget.setRowLabelsVisible(True)\n            else:\n                widget.setRowLabelsVisible(False)\n                widget.setRowLabels(None)\n\n    def row_side_colors(self):\n        var = self.annotation_color_var\n        if var is None:\n            return None\n        column_data = column_data_from_table(self.input_data, var)\n        span = (np.nanmin(column_data), np.nanmax(column_data))\n        merges = self._merge_row_indices()\n        if merges is not None:\n            column_data = aggregate(var, column_data, merges)\n        data, colormap = self._colorize(var, column_data)\n        if var.is_continuous:\n            colormap.span = span\n        return data, colormap, var\n\n    def set_annotation_color_var(self, var: Union[None, Variable, int]):\n        \"\"\"Set the current side color annotation variable.\"\"\"\n        if isinstance(var, int):\n            var = self.row_side_color_model[var]\n        if self.annotation_color_var != var:\n            self.annotation_color_var = var\n            self.update_row_side_colors()\n\n    def update_row_side_colors(self):\n        widget = self.scene.widget\n        if widget is None:\n            return\n        colors = self.row_side_colors()\n        if colors is None:\n            widget.setRowSideColorAnnotations(None)\n        else:\n            widget.setRowSideColorAnnotations(colors[0], colors[1], colors[2].name)\n\n    def _colorize(self, var: Variable, data: np.ndarray) -> Tuple[np.ndarray, ColorMap]:\n        palette = var.palette  # type: Palette\n        colors = np.array(\n            [[c.red(), c.green(), c.blue()] for c in palette.qcolors_w_nan],\n            dtype=np.uint8,\n        )\n        if var.is_discrete:\n            mask = np.isnan(data)\n            data[mask] = -1\n            data = data.astype(int)\n            if mask.any():\n                values = (*var.values, \"N/A\")\n            else:\n                values = var.values\n                colors = colors[: -1]\n            return data, CategoricalColorMap(colors, values)\n        elif var.is_continuous:\n            cmap = GradientColorMap(colors[:-1])\n            return data, cmap\n        else:\n            raise TypeError\n\n    def update_column_annotations(self):\n        widget = self.scene.widget\n        if self.data is not None and widget is not None:\n            widget.setColumnLabelsPosition(self._column_label_pos)\n\n    def __adjust_font_size(self, diff):\n        widget = self.scene.widget\n        if widget is None:\n            return\n        curr = widget.font().pointSizeF()\n        new = curr + diff\n\n        self.__font_dec.setEnabled(new > 1.0)\n        self.__font_inc.setEnabled(new <= 32)\n        if new > 1.0:\n            font = QFont()\n            font.setPointSizeF(new)\n            widget.setFont(font)\n\n    def _on_view_context_menu(self, pos):\n        widget = self.scene.widget\n        if widget is None:\n            return\n        assert isinstance(widget, HeatmapGridWidget)\n        menu = QMenu(self.view.viewport())\n        menu.setAttribute(Qt.WA_DeleteOnClose)\n        menu.addActions(self.view.actions())\n        menu.addSeparator()\n        menu.addActions([self.__font_inc, self.__font_dec])\n        menu.addSeparator()\n        a = QAction(\"Keep aspect ratio\", menu, checkable=True)\n        a.setChecked(self.keep_aspect)\n\n        def ontoggled(state):\n            self.keep_aspect = state\n            self.__aspect_mode_changed()\n        a.toggled.connect(ontoggled)\n        menu.addAction(a)\n        menu.popup(self.view.viewport().mapToGlobal(pos))\n\n    def on_selection_finished(self):\n        if self.scene.widget is not None:\n            self.selected_rows = list(self.scene.widget.selectedRows())\n        else:\n            self.selected_rows = []\n        self.commit()\n\n    def commit(self):\n        data = None\n        indices = None\n        if self.merge_kmeans:\n            merge_indices = self.merge_indices\n        else:\n            merge_indices = None\n\n        if self.input_data is not None and self.selected_rows:\n            indices = self.selected_rows\n            if merge_indices is not None:\n                # expand merged indices\n                indices = np.hstack([merge_indices[i] for i in indices])\n\n            data = self.input_data[indices]\n\n        summary = len(data) if data else self.info.NoOutput\n        details = format_summary_details(data) if data else \"\"\n        self.info.set_output_summary(summary, details)\n        self.Outputs.selected_data.send(data)\n        self.Outputs.annotated_data.send(create_annotated_table(self.input_data, indices))\n\n    def onDeleteWidget(self):\n        self.clear()\n        super().onDeleteWidget()\n\n    def send_report(self):\n        self.report_items((\n            (\"Columns:\", \"Clustering\" if self.col_clustering else \"No sorting\"),\n            (\"Rows:\", \"Clustering\" if self.row_clustering else \"No sorting\"),\n            (\"Split:\",\n             self.split_by_var is not None and self.split_by_var.name),\n            (\"Row annotation\",\n             self.annotation_var is not None and self.annotation_var.name),\n        ))\n        self.report_plot()\n\n    @classmethod\n    def migrate_settings(cls, settings, version):\n        if version is not None and version < 3:\n            def st2cl(state: bool) -> Clustering:\n                return Clustering.OrderedClustering if state else \\\n                    Clustering.None_\n\n            rc = settings.pop(\"row_clustering\", False)\n            cc = settings.pop(\"col_clustering\", False)\n            settings[\"row_clustering_method\"] = st2cl(rc).name\n            settings[\"col_clustering_method\"] = st2cl(cc).name",
                            "ast_type_str": "ClassDef",
                            "start_lineno": 132,
                            "end_lineno": 1138,
                            "parent": null
                        }
                    ],
                    "added_entities": [],
                    "deleted_entities": []
                },
                {
                    "descriptor": {
                        "old_range": {
                            "start": 602,
                            "length": 12
                        },
                        "new_range": {
                            "start": 600,
                            "length": 6
                        },
                        "section": "class OWHeatMap(widget.OWWidget):"
                    },
                    "line_group": {
                        "all_lines": [
                            {
                                "content": "                self.selected_rows = []",
                                "type": "context"
                            },
                            {
                                "content": "        else:",
                                "type": "context"
                            },
                            {
                                "content": "            self.clear()",
                                "type": "context"
                            },
                            {
                                "content": "        if self.parts is not None and self.effective_data is not None:",
                                "type": "deleted"
                            },
                            {
                                "content": "            nrows = sum(len(p.indices) for p in self.parts.rows)",
                                "type": "deleted"
                            },
                            {
                                "content": "            self.Warning.missing_split_values(",
                                "type": "deleted"
                            },
                            {
                                "content": "                shown=nrows < len(self.effective_data))",
                                "type": "deleted"
                            },
                            {
                                "content": "        else:",
                                "type": "deleted"
                            },
                            {
                                "content": "            self.Warning.missing_split_values.clear()",
                                "type": "deleted"
                            },
                            {
                                "content": "",
                                "type": "context"
                            },
                            {
                                "content": "    def update_merge(self):",
                                "type": "context"
                            },
                            {
                                "content": "        self.kmeans_model = None",
                                "type": "context"
                            }
                        ]
                    },
                    "modified_entities": [
                        {
                            "file_name": "Orange/widgets/visualize/owheatmap.py",
                            "type": "method",
                            "name": "OWHeatMap.update_heatmaps",
                            "content": "class OWHeatMap(widget.OWWidget):\n    name = \"Heat Map\"\n    description = \"Plot a data matrix heatmap.\"\n    icon = \"icons/Heatmap.svg\"\n    priority = 260\n    keywords = []\n\n    class Inputs:\n        data = Input(\"Data\", Table)\n\n    class Outputs:\n        selected_data = Output(\"Selected Data\", Table, default=True)\n        annotated_data = Output(ANNOTATED_DATA_SIGNAL_NAME, Table)\n\n    settings_version = 3\n\n    settingsHandler = settings.DomainContextHandler()\n\n    # Disable clustering for inputs bigger than this\n    MaxClustering = 25000\n    # Disable cluster leaf ordering for inputs bigger than this\n    MaxOrderedClustering = 1000\n\n    threshold_low = settings.Setting(0.0)\n    threshold_high = settings.Setting(1.0)\n\n    merge_kmeans = settings.Setting(False)\n    merge_kmeans_k = settings.Setting(50)\n\n    # Display column with averages\n    averages: bool = settings.Setting(True)\n    # Display legend\n    legend: bool = settings.Setting(True)\n    # Annotations\n    #: text row annotation (row names)\n    annotation_var = settings.ContextSetting(None)\n    #: color row annotation\n    annotation_color_var = settings.ContextSetting(None)\n    # Discrete variable used to split that data/heatmaps (vertically)\n    split_by_var = settings.ContextSetting(None)\n    # Selected row/column clustering method (name)\n    col_clustering_method: str = settings.Setting(Clustering.None_.name)\n    row_clustering_method: str = settings.Setting(Clustering.None_.name)\n\n    palette_name = settings.Setting(colorpalettes.DefaultContinuousPaletteName)\n    column_label_pos: int = settings.Setting(1)\n    selected_rows: List[int] = settings.Setting(None, schema_only=True)\n\n    auto_commit = settings.Setting(True)\n\n    graph_name = \"scene\"\n\n    left_side_scrolling = True\n\n    class Information(widget.OWWidget.Information):\n        sampled = Msg(\"Data has been sampled\")\n        discrete_ignored = Msg(\"{} categorical feature{} ignored\")\n        row_clust = Msg(\"{}\")\n        col_clust = Msg(\"{}\")\n        sparse_densified = Msg(\"Showing this data may require a lot of memory\")\n\n    class Error(widget.OWWidget.Error):\n        no_continuous = Msg(\"No numeric features\")\n        not_enough_features = Msg(\"Not enough features for column clustering\")\n        not_enough_instances = Msg(\"Not enough instances for clustering\")\n        not_enough_instances_k_means = Msg(\n            \"Not enough instances for k-means merging\")\n        not_enough_memory = Msg(\"Not enough memory to show this data\")\n\n    class Warning(widget.OWWidget.Warning):\n        empty_clusters = Msg(\"Empty clusters were removed\")\n        missing_split_values = Msg(\n            \"Instances with undefined split variable values are not shown\")\n\n    def __init__(self):\n        super().__init__()\n        self.__pending_selection = self.selected_rows\n\n        # A kingdom for a save_state/restore_state\n        self.col_clustering = enum_get(\n            Clustering, self.col_clustering_method, Clustering.None_)\n        self.row_clustering = enum_get(\n            Clustering, self.row_clustering_method, Clustering.None_)\n\n        @self.settingsAboutToBePacked.connect\n        def _():\n            self.col_clustering_method = self.col_clustering.name\n            self.row_clustering_method = self.row_clustering.name\n\n        self.keep_aspect = False\n\n        #: The original data with all features (retained to\n        #: preserve the domain on the output)\n        self.input_data = None\n        #: The effective data striped of discrete features, and often\n        #: merged using k-means\n        self.data = None\n        self.effective_data = None\n        #: kmeans model used to merge rows of input_data\n        self.kmeans_model = None\n        #: merge indices derived from kmeans\n        #: a list (len==k) of int ndarray where the i-th item contains\n        #: the indices which merge the input_data into the heatmap row i\n        self.merge_indices = None\n        self.parts: Optional[Parts] = None\n        self.__rows_cache = {}\n        self.__columns_cache = {}\n\n        self.info.set_input_summary(self.info.NoInput)\n        self.info.set_output_summary(self.info.NoOutput)\n\n        # GUI definition\n        colorbox = gui.vBox(self.controlArea, \"Color\")\n        self.color_cb = gui.palette_combo_box(self.palette_name)\n        self.color_cb.currentIndexChanged.connect(self.update_color_schema)\n        colorbox.layout().addWidget(self.color_cb)\n\n        form = QFormLayout(\n            formAlignment=Qt.AlignLeft,\n            labelAlignment=Qt.AlignLeft,\n            fieldGrowthPolicy=QFormLayout.AllNonFixedFieldsGrow\n        )\n        lowslider = gui.hSlider(\n            colorbox, self, \"threshold_low\", minValue=0.0, maxValue=1.0,\n            step=0.05, ticks=True, intOnly=False,\n            createLabel=False, callback=self.update_lowslider)\n        highslider = gui.hSlider(\n            colorbox, self, \"threshold_high\", minValue=0.0, maxValue=1.0,\n            step=0.05, ticks=True, intOnly=False,\n            createLabel=False, callback=self.update_highslider)\n\n        form.addRow(\"Low:\", lowslider)\n        form.addRow(\"High:\", highslider)\n\n        colorbox.layout().addLayout(form)\n\n        mergebox = gui.vBox(self.controlArea, \"Merge\",)\n        gui.checkBox(mergebox, self, \"merge_kmeans\", \"Merge by k-means\",\n                     callback=self.__update_row_clustering)\n        ibox = gui.indentedBox(mergebox)\n        gui.spin(ibox, self, \"merge_kmeans_k\", minv=5, maxv=500,\n                 label=\"Clusters:\", keyboardTracking=False,\n                 callbackOnReturn=True, callback=self.update_merge)\n\n        cluster_box = gui.vBox(self.controlArea, \"Clustering\")\n        # Row clustering\n        self.row_cluster_cb = cb = ComboBox()\n        cb.setModel(create_list_model(ClusteringModelData, self))\n        cbselect(cb, self.row_clustering, ClusteringRole)\n        self.connect_control(\n            \"row_clustering\",\n            lambda value, cb=cb: cbselect(cb, value, ClusteringRole)\n        )\n        @cb.activated.connect\n        def _(idx, cb=cb):\n            self.set_row_clustering(cb.itemData(idx, ClusteringRole))\n\n        # Column clustering\n        self.col_cluster_cb = cb = ComboBox()\n        cb.setModel(create_list_model(ClusteringModelData, self))\n        cbselect(cb, self.col_clustering, ClusteringRole)\n        self.connect_control(\n            \"col_clustering\",\n            lambda value, cb=cb: cbselect(cb, value, ClusteringRole)\n        )\n        @cb.activated.connect\n        def _(idx, cb=cb):\n            self.set_col_clustering(cb.itemData(idx, ClusteringRole))\n\n        form = QFormLayout(\n            labelAlignment=Qt.AlignLeft, formAlignment=Qt.AlignLeft,\n            fieldGrowthPolicy=QFormLayout.AllNonFixedFieldsGrow,\n        )\n        form.addRow(\"Rows:\", self.row_cluster_cb)\n        form.addRow(\"Columns:\", self.col_cluster_cb)\n        cluster_box.layout().addLayout(form)\n        box = gui.vBox(self.controlArea, \"Split By\")\n\n        self.row_split_model = DomainModel(\n            placeholder=\"(None)\",\n            valid_types=(Orange.data.DiscreteVariable,),\n            parent=self,\n        )\n        self.row_split_cb = cb = ComboBoxSearch(\n            enabled=not self.merge_kmeans,\n            sizeAdjustPolicy=ComboBox.AdjustToMinimumContentsLengthWithIcon,\n            minimumContentsLength=14,\n            toolTip=\"Split the heatmap vertically by a categorical column\"\n        )\n        self.row_split_cb.setModel(self.row_split_model)\n        self.connect_control(\n            \"split_by_var\", lambda value, cb=cb: cbselect(cb, value)\n        )\n        self.connect_control(\n            \"merge_kmeans\", self.row_split_cb.setDisabled\n        )\n        self.split_by_var = None\n\n        self.row_split_cb.activated.connect(\n            self.__on_split_rows_activated\n        )\n        box.layout().addWidget(self.row_split_cb)\n\n        box = gui.vBox(self.controlArea, 'Annotation && Legends')\n\n        gui.checkBox(box, self, 'legend', 'Show legend',\n                     callback=self.update_legend)\n\n        gui.checkBox(box, self, 'averages', 'Stripes with averages',\n                     callback=self.update_averages_stripe)\n        annotbox = QGroupBox(\"Row Annotations\", flat=True)\n        form = QFormLayout(\n            annotbox,\n            formAlignment=Qt.AlignLeft,\n            labelAlignment=Qt.AlignLeft,\n            fieldGrowthPolicy=QFormLayout.AllNonFixedFieldsGrow\n        )\n        self.annotation_model = DomainModel(placeholder=\"(None)\")\n        self.annotation_text_cb = ComboBoxSearch(\n            minimumContentsLength=12,\n            sizeAdjustPolicy=QComboBox.AdjustToMinimumContentsLength\n        )\n        self.annotation_text_cb.setModel(self.annotation_model)\n        self.annotation_text_cb.activated.connect(self.set_annotation_var)\n        self.connect_control(\"annotation_var\", self.annotation_var_changed)\n\n        self.row_side_color_model = DomainModel(\n            order=(DomainModel.CLASSES, DomainModel.Separator,\n                   DomainModel.METAS),\n            placeholder=\"(None)\", valid_types=DomainModel.PRIMITIVE,\n            flags=Qt.ItemIsSelectable | Qt.ItemIsEnabled,\n            parent=self,\n        )\n        self.row_side_color_cb = ComboBoxSearch(\n            sizeAdjustPolicy=QComboBox.AdjustToMinimumContentsLength,\n            minimumContentsLength=12\n        )\n        self.row_side_color_cb.setModel(self.row_side_color_model)\n        self.row_side_color_cb.activated.connect(self.set_annotation_color_var)\n        self.connect_control(\"annotation_color_var\", self.annotation_color_var_changed)\n        form.addRow(\"Text\", self.annotation_text_cb)\n        form.addRow(\"Color\", self.row_side_color_cb)\n        box.layout().addWidget(annotbox)\n        posbox = gui.vBox(box, \"Column Labels Position\", addSpace=False)\n        posbox.setFlat(True)\n        cb = gui.comboBox(\n            posbox, self, \"column_label_pos\",\n            callback=self.update_column_annotations)\n        cb.setModel(create_list_model(ColumnLabelsPosData, parent=self))\n        cb.setCurrentIndex(self.column_label_pos)\n        gui.checkBox(self.controlArea, self, \"keep_aspect\",\n                     \"Keep aspect ratio\", box=\"Resize\",\n                     callback=self.__aspect_mode_changed)\n\n        gui.rubber(self.controlArea)\n        gui.auto_send(self.controlArea, self, \"auto_commit\")\n\n        # Scene with heatmap\n        class HeatmapScene(QGraphicsScene):\n            widget: Optional[HeatmapGridWidget] = None\n\n        self.scene = self.scene = HeatmapScene(parent=self)\n        self.view = GraphicsView(\n            self.scene,\n            verticalScrollBarPolicy=Qt.ScrollBarAlwaysOn,\n            horizontalScrollBarPolicy=Qt.ScrollBarAlwaysOn,\n            viewportUpdateMode=QGraphicsView.FullViewportUpdate,\n            widgetResizable=True,\n        )\n        self.view.setContextMenuPolicy(Qt.CustomContextMenu)\n        self.view.customContextMenuRequested.connect(\n            self._on_view_context_menu\n        )\n        self.mainArea.layout().addWidget(self.view)\n        self.selected_rows = []\n        self.__font_inc = QAction(\n            \"Increase Font\", self, shortcut=QKeySequence(\"ctrl+>\"))\n        self.__font_dec = QAction(\n            \"Decrease Font\", self, shortcut=QKeySequence(\"ctrl+<\"))\n        self.__font_inc.triggered.connect(lambda: self.__adjust_font_size(1))\n        self.__font_dec.triggered.connect(lambda: self.__adjust_font_size(-1))\n        if hasattr(QAction, \"setShortcutVisibleInContextMenu\"):\n            apply_all(\n                [self.__font_inc, self.__font_dec],\n                lambda a: a.setShortcutVisibleInContextMenu(True)\n            )\n        self.addActions([self.__font_inc, self.__font_dec])\n\n    @property\n    def center_palette(self):\n        palette = self.color_cb.currentData()\n        return bool(palette.flags & palette.Diverging)\n\n    @property\n    def _column_label_pos(self) -> HeatmapGridWidget.Position:\n        return ColumnLabelsPosData[self.column_label_pos][Qt.UserRole]\n\n    def annotation_color_var_changed(self, value):\n        cbselect(self.row_side_color_cb, value, Qt.EditRole)\n\n    def annotation_var_changed(self, value):\n        cbselect(self.annotation_text_cb, value, Qt.EditRole)\n\n    def set_row_clustering(self, method: Clustering) -> None:\n        assert isinstance(method, Clustering)\n        if self.row_clustering != method:\n            self.row_clustering = method\n            cbselect(self.row_cluster_cb, method, ClusteringRole)\n            self.__update_row_clustering()\n\n    def set_col_clustering(self, method: Clustering) -> None:\n        assert isinstance(method, Clustering)\n        if self.col_clustering != method:\n            self.col_clustering = method\n            cbselect(self.col_cluster_cb, method, ClusteringRole)\n            self.__update_column_clustering()\n\n    def sizeHint(self) -> QSize:\n        return super().sizeHint().expandedTo(QSize(900, 700))\n\n    def color_palette(self):\n        return self.color_cb.currentData().lookup_table()\n\n    def color_map(self) -> GradientColorMap:\n        return GradientColorMap(\n            self.color_palette(), (self.threshold_low, self.threshold_high),\n            0 if self.center_palette else None\n        )\n\n    def clear(self):\n        self.data = None\n        self.input_data = None\n        self.effective_data = None\n        self.kmeans_model = None\n        self.merge_indices = None\n        self.annotation_model.set_domain(None)\n        self.annotation_var = None\n        self.row_side_color_model.set_domain(None)\n        self.annotation_color_var = None\n        self.row_split_model.set_domain(None)\n        self.split_by_var = None\n        self.parts = None\n        self.clear_scene()\n        self.selected_rows = []\n        self.__columns_cache.clear()\n        self.__rows_cache.clear()\n        self.__update_clustering_enable_state(None)\n\n    def clear_scene(self):\n        if self.scene.widget is not None:\n            self.scene.widget.layoutDidActivate.disconnect(\n                self.__on_layout_activate\n            )\n            self.scene.widget.selectionFinished.disconnect(\n                self.on_selection_finished\n            )\n        self.scene.widget = None\n        self.scene.clear()\n\n        self.view.setSceneRect(QRectF())\n        self.view.setHeaderSceneRect(QRectF())\n        self.view.setFooterSceneRect(QRectF())\n\n    @Inputs.data\n    def set_dataset(self, data=None):\n        \"\"\"Set the input dataset to display.\"\"\"\n        self.closeContext()\n        self.clear()\n        self.clear_messages()\n        self._set_input_summary(data)\n\n        if isinstance(data, SqlTable):\n            if data.approx_len() < 4000:\n                data = Table(data)\n            else:\n                self.Information.sampled()\n                data_sample = data.sample_time(1, no_cache=True)\n                data_sample.download_data(2000, partial=True)\n                data = Table(data_sample)\n\n        if data is not None and not len(data):\n            data = None\n\n        if data is not None and sp.issparse(data.X):\n            try:\n                data = data.to_dense()\n            except MemoryError:\n                data = None\n                self.Error.not_enough_memory()\n            else:\n                self.Information.sparse_densified()\n\n        input_data = data\n\n        # Data contains no attributes or meta attributes only\n        if data is not None and len(data.domain.attributes) == 0:\n            self.Error.no_continuous()\n            input_data = data = None\n\n        # Data contains some discrete attributes which must be filtered\n        if data is not None and \\\n                any(var.is_discrete for var in data.domain.attributes):\n            ndisc = sum(var.is_discrete for var in data.domain.attributes)\n            data = data.transform(\n                Domain([var for var in data.domain.attributes\n                        if var.is_continuous],\n                       data.domain.class_vars,\n                       data.domain.metas))\n            if not data.domain.attributes:\n                self.Error.no_continuous()\n                input_data = data = None\n            else:\n                self.Information.discrete_ignored(\n                    ndisc, \"s\" if ndisc > 1 else \"\")\n\n        self.data = data\n        self.input_data = input_data\n\n        if data is not None:\n            self.annotation_model.set_domain(self.input_data.domain)\n            self.row_side_color_model.set_domain(self.input_data.domain)\n            self.annotation_var = None\n            self.annotation_color_var = None\n            self.row_split_model.set_domain(data.domain)\n            if data.domain.has_discrete_class:\n                self.split_by_var = data.domain.class_var\n            else:\n                self.split_by_var = None\n            self.openContext(self.input_data)\n            if self.split_by_var not in self.row_split_model:\n                self.split_by_var = None\n\n        self.update_heatmaps()\n        if data is not None and self.__pending_selection is not None:\n            assert self.scene.widget is not None\n            self.scene.widget.selectRows(self.__pending_selection)\n            self.selected_rows = self.__pending_selection\n            self.__pending_selection = None\n\n        self.unconditional_commit()\n\n    def _set_input_summary(self, data):\n        summary = len(data) if data else self.info.NoInput\n        details = format_summary_details(data) if data else \"\"\n        self.info.set_input_summary(summary, details)\n\n    def __on_split_rows_activated(self):\n        self.set_split_variable(self.row_split_cb.currentData(Qt.EditRole))\n\n    def set_split_variable(self, var):\n        if var != self.split_by_var:\n            self.split_by_var = var\n            self.update_heatmaps()\n\n    def update_heatmaps(self):\n        if self.data is not None:\n            self.clear_scene()\n            self.clear_messages()\n            if self.col_clustering != Clustering.None_ and \\\n                    len(self.data.domain.attributes) < 2:\n                self.Error.not_enough_features()\n            elif (self.col_clustering != Clustering.None_ or\n                  self.row_clustering != Clustering.None_) and \\\n                    len(self.data) < 2:\n                self.Error.not_enough_instances()\n            elif self.merge_kmeans and len(self.data) < 3:\n                self.Error.not_enough_instances_k_means()\n            else:\n                parts = self.construct_heatmaps(self.data, self.split_by_var)\n                self.construct_heatmaps_scene(parts, self.effective_data)\n                self.selected_rows = []\n        else:\n            self.clear()\n        if self.parts is not None and self.effective_data is not None:\n            nrows = sum(len(p.indices) for p in self.parts.rows)\n            self.Warning.missing_split_values(\n                shown=nrows < len(self.effective_data))\n        else:\n            self.Warning.missing_split_values.clear()\n\n    def update_merge(self):\n        self.kmeans_model = None\n        self.merge_indices = None\n        if self.data is not None and self.merge_kmeans:\n            self.update_heatmaps()\n            self.commit()\n\n    def _make_parts(self, data, group_var=None):\n        \"\"\"\n        Make initial `Parts` for data, split by group_var, group_key\n        \"\"\"\n        if group_var is not None:\n            assert group_var.is_discrete\n            _col_data = table_column_data(data, group_var)\n            row_indices = [np.flatnonzero(_col_data == i)\n                           for i in range(len(group_var.values))]\n            row_groups = [RowPart(title=name, indices=ind,\n                                  cluster=None, cluster_ordered=None)\n                          for name, ind in zip(group_var.values, row_indices)]\n        else:\n            row_groups = [RowPart(title=None, indices=range(0, len(data)),\n                                  cluster=None, cluster_ordered=None)]\n\n        col_groups = [\n            ColumnPart(\n                title=None, indices=range(0, len(data.domain.attributes)),\n                domain=data.domain, cluster=None, cluster_ordered=None)\n        ]\n\n        minv, maxv = np.nanmin(data.X), np.nanmax(data.X)\n        return Parts(row_groups, col_groups, span=(minv, maxv))\n\n    def cluster_rows(self, data: Table, parts: 'Parts', ordered=False) -> 'Parts':\n        row_groups = []\n        for row in parts.rows:\n            if row.cluster is not None:\n                cluster = row.cluster\n            else:\n                cluster = None\n            if row.cluster_ordered is not None:\n                cluster_ord = row.cluster_ordered\n            else:\n                cluster_ord = None\n\n            if row.can_cluster:\n                matrix = None\n                need_dist = cluster is None or (ordered and cluster_ord is None)\n                if need_dist:\n                    subset = data[row.indices]\n                    matrix = Orange.distance.Euclidean(subset)\n\n                if cluster is None:\n                    assert len(matrix) < self.MaxClustering\n                    cluster = hierarchical.dist_matrix_clustering(\n                        matrix, linkage=hierarchical.WARD\n                    )\n                if ordered and cluster_ord is None:\n                    assert len(matrix) < self.MaxOrderedClustering\n                    cluster_ord = hierarchical.optimal_leaf_ordering(\n                        cluster, matrix,\n                    )\n            row_groups.append(row._replace(cluster=cluster, cluster_ordered=cluster_ord))\n\n        return parts._replace(rows=row_groups)\n\n    def cluster_columns(self, data, parts, ordered=False):\n        assert len(parts.columns) == 1, \"columns split is no longer supported\"\n        assert all(var.is_continuous for var in data.domain.attributes)\n\n        col0 = parts.columns[0]\n        if col0.cluster is not None:\n            cluster = col0.cluster\n        else:\n            cluster = None\n        if col0.cluster_ordered is not None:\n            cluster_ord = col0.cluster_ordered\n        else:\n            cluster_ord = None\n        need_dist = cluster is None or (ordered and cluster_ord is None)\n        matrix = None\n        if need_dist:\n            data = Orange.distance._preprocess(data)\n            matrix = np.asarray(Orange.distance.PearsonR(data, axis=0))\n            # nan values break clustering below\n            matrix = np.nan_to_num(matrix)\n\n        if cluster is None:\n            assert matrix is not None\n            assert len(matrix) < self.MaxClustering\n            cluster = hierarchical.dist_matrix_clustering(\n                matrix, linkage=hierarchical.WARD\n            )\n        if ordered and cluster_ord is None:\n            assert len(matrix) < self.MaxOrderedClustering\n            cluster_ord = hierarchical.optimal_leaf_ordering(cluster, matrix)\n\n        col_groups = [col._replace(cluster=cluster, cluster_ordered=cluster_ord)\n                      for col in parts.columns]\n        return parts._replace(columns=col_groups)\n\n    def construct_heatmaps(self, data, group_var=None) -> 'Parts':\n        if self.merge_kmeans:\n            if self.kmeans_model is None:\n                effective_data = self.input_data.transform(\n                    Orange.data.Domain(\n                        [var for var in self.input_data.domain.attributes\n                         if var.is_continuous],\n                        self.input_data.domain.class_vars,\n                        self.input_data.domain.metas))\n                nclust = min(self.merge_kmeans_k, len(effective_data) - 1)\n                self.kmeans_model = kmeans_compress(effective_data, k=nclust)\n                effective_data.domain = self.kmeans_model.domain\n                merge_indices = [np.flatnonzero(self.kmeans_model.labels == ind)\n                                 for ind in range(nclust)]\n                not_empty_indices = [i for i, x in enumerate(merge_indices)\n                                     if len(x) > 0]\n                self.merge_indices = \\\n                    [merge_indices[i] for i in not_empty_indices]\n                if len(merge_indices) != len(self.merge_indices):\n                    self.Warning.empty_clusters()\n                effective_data = Orange.data.Table(\n                    Orange.data.Domain(effective_data.domain.attributes),\n                    self.kmeans_model.centroids[not_empty_indices]\n                )\n            else:\n                effective_data = self.effective_data\n\n            group_var = None\n        else:\n            self.kmeans_model = None\n            self.merge_indices = None\n            effective_data = data\n\n        self.effective_data = effective_data\n\n        self.__update_clustering_enable_state(effective_data)\n\n        parts = self._make_parts(effective_data, group_var)\n        # Restore/update the row/columns items descriptions from cache if\n        # available\n        rows_cache_key = (group_var,\n                          self.merge_kmeans_k if self.merge_kmeans else None)\n        if rows_cache_key in self.__rows_cache:\n            parts = parts._replace(rows=self.__rows_cache[rows_cache_key].rows)\n\n        if self.row_clustering != Clustering.None_:\n            parts = self.cluster_rows(\n                effective_data, parts,\n                ordered=self.row_clustering == Clustering.OrderedClustering\n            )\n        if self.col_clustering != Clustering.None_:\n            parts = self.cluster_columns(\n                effective_data, parts,\n                ordered=self.col_clustering == Clustering.OrderedClustering\n            )\n\n        # Cache the updated parts\n        self.__rows_cache[rows_cache_key] = parts\n        return parts\n\n    def construct_heatmaps_scene(self, parts: 'Parts', data: Table) -> None:\n        _T = TypeVar(\"_T\", bound=Union[RowPart, ColumnPart])\n\n        def select_cluster(clustering: Clustering, item: _T) -> _T:\n            if clustering == Clustering.None_:\n                return item._replace(cluster=None, cluster_ordered=None)\n            elif clustering == Clustering.Clustering:\n                return item._replace(cluster=item.cluster, cluster_ordered=None)\n            elif clustering == Clustering.OrderedClustering:\n                return item._replace(cluster=item.cluster_ordered, cluster_ordered=None)\n            else:  # pragma: no cover\n                raise TypeError()\n\n        rows = [select_cluster(self.row_clustering, rowitem)\n                for rowitem in parts.rows]\n        cols = [select_cluster(self.col_clustering, colitem)\n                for colitem in parts.columns]\n        parts = Parts(columns=cols, rows=rows, span=parts.span)\n\n        self.setup_scene(parts, data)\n\n    def setup_scene(self, parts, data):\n        # type: (Parts, Table) -> None\n        widget = HeatmapGridWidget()\n        widget.setColorMap(self.color_map())\n        self.scene.addItem(widget)\n        self.scene.widget = widget\n        columns = [v.name for v in data.domain.attributes]\n        parts = HeatmapGridWidget.Parts(\n            rows=[\n                HeatmapGridWidget.RowItem(r.title, r.indices, r.cluster)\n                for r in parts.rows\n            ],\n            columns=[\n                HeatmapGridWidget.ColumnItem(c.title, c.indices, c.cluster)\n                for c in parts.columns\n            ],\n            data=data.X,\n            span=parts.span,\n            row_names=None,\n            col_names=columns,\n        )\n        widget.setHeatmaps(parts)\n        side = self.row_side_colors()\n        if side is not None:\n            widget.setRowSideColorAnnotations(side[0], side[1], name=side[2].name)\n        widget.setColumnLabelsPosition(self._column_label_pos)\n        widget.setAspectRatioMode(\n            Qt.KeepAspectRatio if self.keep_aspect else Qt.IgnoreAspectRatio\n        )\n        widget.setShowAverages(self.averages)\n        widget.setLegendVisible(self.legend)\n\n        widget.layoutDidActivate.connect(self.__on_layout_activate)\n        widget.selectionFinished.connect(self.on_selection_finished)\n\n        self.update_annotations()\n        self.view.setCentralWidget(widget)\n        self.parts = parts\n\n    def __update_scene_rects(self):\n        widget = self.scene.widget\n        if widget is None:\n            return\n        rect = widget.geometry()\n        self.scene.setSceneRect(rect)\n        self.view.setSceneRect(rect)\n        self.view.setHeaderSceneRect(widget.headerGeometry())\n        self.view.setFooterSceneRect(widget.footerGeometry())\n\n    def __on_layout_activate(self):\n        self.__update_scene_rects()\n\n    def __aspect_mode_changed(self):\n        widget = self.scene.widget\n        if widget is None:\n            return\n        widget.setAspectRatioMode(\n            Qt.KeepAspectRatio if self.keep_aspect else Qt.IgnoreAspectRatio\n        )\n        # when aspect fixed the vertical sh is fixex, when not, it can\n        # shrink vertically\n        sp = widget.sizePolicy()\n        if self.keep_aspect:\n            sp.setVerticalPolicy(QSizePolicy.Fixed)\n        else:\n            sp.setVerticalPolicy(QSizePolicy.Preferred)\n        widget.setSizePolicy(sp)\n\n    def __update_clustering_enable_state(self, data):\n        if data is not None:\n            N = len(data)\n            M = len(data.domain.attributes)\n        else:\n            N = M = 0\n\n        rc_enabled = N <= self.MaxClustering\n        rco_enabled = N <= self.MaxOrderedClustering\n        cc_enabled = M <= self.MaxClustering\n        cco_enabled = M <= self.MaxOrderedClustering\n        row_clust, col_clust = self.row_clustering, self.col_clustering\n\n        row_clust_msg = \"\"\n        col_clust_msg = \"\"\n\n        if not rco_enabled and row_clust == Clustering.OrderedClustering:\n            row_clust = Clustering.Clustering\n            row_clust_msg = \"Row cluster ordering was disabled due to the \" \\\n                            \"input matrix being to big\"\n        if not rc_enabled and row_clust == Clustering.Clustering:\n            row_clust = Clustering.None_\n            row_clust_msg = \"Row clustering was was disabled due to the \" \\\n                            \"input matrix being to big\"\n\n        if not cco_enabled and col_clust == Clustering.OrderedClustering:\n            col_clust = Clustering.Clustering\n            col_clust_msg = \"Column cluster ordering was disabled due to \" \\\n                            \"the input matrix being to big\"\n        if not cc_enabled and col_clust == Clustering.Clustering:\n            col_clust = Clustering.None_\n            col_clust_msg = \"Column clustering was disabled due to the \" \\\n                            \"input matrix being to big\"\n\n        self.col_clustering = col_clust\n        self.row_clustering = row_clust\n\n        self.Information.row_clust(row_clust_msg, shown=bool(row_clust_msg))\n        self.Information.col_clust(col_clust_msg, shown=bool(col_clust_msg))\n\n        # Disable/enable the combobox items for the clustering methods\n        def setenabled(cb: QComboBox, clu: bool, clu_op: bool):\n            model = cb.model()\n            assert isinstance(model, QStandardItemModel)\n            idx = cb.findData(Clustering.OrderedClustering, ClusteringRole)\n            assert idx != -1\n            model.item(idx).setEnabled(clu_op)\n            idx = cb.findData(Clustering.Clustering, ClusteringRole)\n            assert idx != -1\n            model.item(idx).setEnabled(clu)\n\n        setenabled(self.row_cluster_cb, rc_enabled, rco_enabled)\n        setenabled(self.col_cluster_cb, cc_enabled, cco_enabled)\n\n    def update_averages_stripe(self):\n        \"\"\"Update the visibility of the averages stripe.\n        \"\"\"\n        widget = self.scene.widget\n        if widget is not None:\n            widget.setShowAverages(self.averages)\n\n    def update_lowslider(self):\n        low, high = self.controls.threshold_low, self.controls.threshold_high\n        if low.value() >= high.value():\n            low.setSliderPosition(high.value() - 1)\n        self.update_color_schema()\n\n    def update_highslider(self):\n        low, high = self.controls.threshold_low, self.controls.threshold_high\n        if low.value() >= high.value():\n            high.setSliderPosition(low.value() + 1)\n        self.update_color_schema()\n\n    def update_color_schema(self):\n        self.palette_name = self.color_cb.currentData().name\n        w = self.scene.widget\n        if w is not None:\n            w.setColorMap(self.color_map())\n\n    def __update_column_clustering(self):\n        self.update_heatmaps()\n        self.commit()\n\n    def __update_row_clustering(self):\n        self.update_heatmaps()\n        self.commit()\n\n    def update_legend(self):\n        widget = self.scene.widget\n        if widget is not None:\n            widget.setLegendVisible(self.legend)\n\n    def row_annotation_var(self):\n        return self.annotation_var\n\n    def row_annotation_data(self):\n        var = self.row_annotation_var()\n        if var is None:\n            return None\n        return column_str_from_table(self.input_data, var)\n\n    def _merge_row_indices(self):\n        if self.merge_kmeans and self.kmeans_model is not None:\n            return self.merge_indices\n        else:\n            return None\n\n    def set_annotation_var(self, var: Union[None, Variable, int]):\n        if isinstance(var, int):\n            var = self.annotation_model[var]\n        if self.annotation_var != var:\n            self.annotation_var = var\n            self.update_annotations()\n\n    def update_annotations(self):\n        widget = self.scene.widget\n        if widget is not None:\n            annot_col = self.row_annotation_data()\n            merge_indices = self._merge_row_indices()\n            if merge_indices is not None and annot_col is not None:\n                join = lambda _1: join_elided(\", \", 42, _1, \" ({} more)\")\n                annot_col = aggregate_apply(join, annot_col, merge_indices)\n            if annot_col is not None:\n                widget.setRowLabels(annot_col)\n                widget.setRowLabelsVisible(True)\n            else:\n                widget.setRowLabelsVisible(False)\n                widget.setRowLabels(None)\n\n    def row_side_colors(self):\n        var = self.annotation_color_var\n        if var is None:\n            return None\n        column_data = column_data_from_table(self.input_data, var)\n        span = (np.nanmin(column_data), np.nanmax(column_data))\n        merges = self._merge_row_indices()\n        if merges is not None:\n            column_data = aggregate(var, column_data, merges)\n        data, colormap = self._colorize(var, column_data)\n        if var.is_continuous:\n            colormap.span = span\n        return data, colormap, var\n\n    def set_annotation_color_var(self, var: Union[None, Variable, int]):\n        \"\"\"Set the current side color annotation variable.\"\"\"\n        if isinstance(var, int):\n            var = self.row_side_color_model[var]\n        if self.annotation_color_var != var:\n            self.annotation_color_var = var\n            self.update_row_side_colors()\n\n    def update_row_side_colors(self):\n        widget = self.scene.widget\n        if widget is None:\n            return\n        colors = self.row_side_colors()\n        if colors is None:\n            widget.setRowSideColorAnnotations(None)\n        else:\n            widget.setRowSideColorAnnotations(colors[0], colors[1], colors[2].name)\n\n    def _colorize(self, var: Variable, data: np.ndarray) -> Tuple[np.ndarray, ColorMap]:\n        palette = var.palette  # type: Palette\n        colors = np.array(\n            [[c.red(), c.green(), c.blue()] for c in palette.qcolors_w_nan],\n            dtype=np.uint8,\n        )\n        if var.is_discrete:\n            mask = np.isnan(data)\n            data[mask] = -1\n            data = data.astype(int)\n            if mask.any():\n                values = (*var.values, \"N/A\")\n            else:\n                values = var.values\n                colors = colors[: -1]\n            return data, CategoricalColorMap(colors, values)\n        elif var.is_continuous:\n            cmap = GradientColorMap(colors[:-1])\n            return data, cmap\n        else:\n            raise TypeError\n\n    def update_column_annotations(self):\n        widget = self.scene.widget\n        if self.data is not None and widget is not None:\n            widget.setColumnLabelsPosition(self._column_label_pos)\n\n    def __adjust_font_size(self, diff):\n        widget = self.scene.widget\n        if widget is None:\n            return\n        curr = widget.font().pointSizeF()\n        new = curr + diff\n\n        self.__font_dec.setEnabled(new > 1.0)\n        self.__font_inc.setEnabled(new <= 32)\n        if new > 1.0:\n            font = QFont()\n            font.setPointSizeF(new)\n            widget.setFont(font)\n\n    def _on_view_context_menu(self, pos):\n        widget = self.scene.widget\n        if widget is None:\n            return\n        assert isinstance(widget, HeatmapGridWidget)\n        menu = QMenu(self.view.viewport())\n        menu.setAttribute(Qt.WA_DeleteOnClose)\n        menu.addActions(self.view.actions())\n        menu.addSeparator()\n        menu.addActions([self.__font_inc, self.__font_dec])\n        menu.addSeparator()\n        a = QAction(\"Keep aspect ratio\", menu, checkable=True)\n        a.setChecked(self.keep_aspect)\n\n        def ontoggled(state):\n            self.keep_aspect = state\n            self.__aspect_mode_changed()\n        a.toggled.connect(ontoggled)\n        menu.addAction(a)\n        menu.popup(self.view.viewport().mapToGlobal(pos))\n\n    def on_selection_finished(self):\n        if self.scene.widget is not None:\n            self.selected_rows = list(self.scene.widget.selectedRows())\n        else:\n            self.selected_rows = []\n        self.commit()\n\n    def commit(self):\n        data = None\n        indices = None\n        if self.merge_kmeans:\n            merge_indices = self.merge_indices\n        else:\n            merge_indices = None\n\n        if self.input_data is not None and self.selected_rows:\n            indices = self.selected_rows\n            if merge_indices is not None:\n                # expand merged indices\n                indices = np.hstack([merge_indices[i] for i in indices])\n\n            data = self.input_data[indices]\n\n        summary = len(data) if data else self.info.NoOutput\n        details = format_summary_details(data) if data else \"\"\n        self.info.set_output_summary(summary, details)\n        self.Outputs.selected_data.send(data)\n        self.Outputs.annotated_data.send(create_annotated_table(self.input_data, indices))\n\n    def onDeleteWidget(self):\n        self.clear()\n        super().onDeleteWidget()\n\n    def send_report(self):\n        self.report_items((\n            (\"Columns:\", \"Clustering\" if self.col_clustering else \"No sorting\"),\n            (\"Rows:\", \"Clustering\" if self.row_clustering else \"No sorting\"),\n            (\"Split:\",\n             self.split_by_var is not None and self.split_by_var.name),\n            (\"Row annotation\",\n             self.annotation_var is not None and self.annotation_var.name),\n        ))\n        self.report_plot()\n\n    @classmethod\n    def migrate_settings(cls, settings, version):\n        if version is not None and version < 3:\n            def st2cl(state: bool) -> Clustering:\n                return Clustering.OrderedClustering if state else \\\n                    Clustering.None_\n\n            rc = settings.pop(\"row_clustering\", False)\n            cc = settings.pop(\"col_clustering\", False)\n            settings[\"row_clustering_method\"] = st2cl(rc).name\n            settings[\"col_clustering_method\"] = st2cl(cc).name",
                            "ast_type_str": "ClassDef",
                            "start_lineno": 586,
                            "end_lineno": 610,
                            "parent": {
                                "file_name": "Orange/widgets/visualize/owheatmap.py",
                                "type": "class",
                                "name": "OWHeatMap",
                                "content": "class OWHeatMap(widget.OWWidget):\n    name = \"Heat Map\"\n    description = \"Plot a data matrix heatmap.\"\n    icon = \"icons/Heatmap.svg\"\n    priority = 260\n    keywords = []\n\n    class Inputs:\n        data = Input(\"Data\", Table)\n\n    class Outputs:\n        selected_data = Output(\"Selected Data\", Table, default=True)\n        annotated_data = Output(ANNOTATED_DATA_SIGNAL_NAME, Table)\n\n    settings_version = 3\n\n    settingsHandler = settings.DomainContextHandler()\n\n    # Disable clustering for inputs bigger than this\n    MaxClustering = 25000\n    # Disable cluster leaf ordering for inputs bigger than this\n    MaxOrderedClustering = 1000\n\n    threshold_low = settings.Setting(0.0)\n    threshold_high = settings.Setting(1.0)\n\n    merge_kmeans = settings.Setting(False)\n    merge_kmeans_k = settings.Setting(50)\n\n    # Display column with averages\n    averages: bool = settings.Setting(True)\n    # Display legend\n    legend: bool = settings.Setting(True)\n    # Annotations\n    #: text row annotation (row names)\n    annotation_var = settings.ContextSetting(None)\n    #: color row annotation\n    annotation_color_var = settings.ContextSetting(None)\n    # Discrete variable used to split that data/heatmaps (vertically)\n    split_by_var = settings.ContextSetting(None)\n    # Selected row/column clustering method (name)\n    col_clustering_method: str = settings.Setting(Clustering.None_.name)\n    row_clustering_method: str = settings.Setting(Clustering.None_.name)\n\n    palette_name = settings.Setting(colorpalettes.DefaultContinuousPaletteName)\n    column_label_pos: int = settings.Setting(1)\n    selected_rows: List[int] = settings.Setting(None, schema_only=True)\n\n    auto_commit = settings.Setting(True)\n\n    graph_name = \"scene\"\n\n    left_side_scrolling = True\n\n    class Information(widget.OWWidget.Information):\n        sampled = Msg(\"Data has been sampled\")\n        discrete_ignored = Msg(\"{} categorical feature{} ignored\")\n        row_clust = Msg(\"{}\")\n        col_clust = Msg(\"{}\")\n        sparse_densified = Msg(\"Showing this data may require a lot of memory\")\n\n    class Error(widget.OWWidget.Error):\n        no_continuous = Msg(\"No numeric features\")\n        not_enough_features = Msg(\"Not enough features for column clustering\")\n        not_enough_instances = Msg(\"Not enough instances for clustering\")\n        not_enough_instances_k_means = Msg(\n            \"Not enough instances for k-means merging\")\n        not_enough_memory = Msg(\"Not enough memory to show this data\")\n\n    class Warning(widget.OWWidget.Warning):\n        empty_clusters = Msg(\"Empty clusters were removed\")\n        missing_split_values = Msg(\n            \"Instances with undefined split variable values are not shown\")\n\n    def __init__(self):\n        super().__init__()\n        self.__pending_selection = self.selected_rows\n\n        # A kingdom for a save_state/restore_state\n        self.col_clustering = enum_get(\n            Clustering, self.col_clustering_method, Clustering.None_)\n        self.row_clustering = enum_get(\n            Clustering, self.row_clustering_method, Clustering.None_)\n\n        @self.settingsAboutToBePacked.connect\n        def _():\n            self.col_clustering_method = self.col_clustering.name\n            self.row_clustering_method = self.row_clustering.name\n\n        self.keep_aspect = False\n\n        #: The original data with all features (retained to\n        #: preserve the domain on the output)\n        self.input_data = None\n        #: The effective data striped of discrete features, and often\n        #: merged using k-means\n        self.data = None\n        self.effective_data = None\n        #: kmeans model used to merge rows of input_data\n        self.kmeans_model = None\n        #: merge indices derived from kmeans\n        #: a list (len==k) of int ndarray where the i-th item contains\n        #: the indices which merge the input_data into the heatmap row i\n        self.merge_indices = None\n        self.parts: Optional[Parts] = None\n        self.__rows_cache = {}\n        self.__columns_cache = {}\n\n        self.info.set_input_summary(self.info.NoInput)\n        self.info.set_output_summary(self.info.NoOutput)\n\n        # GUI definition\n        colorbox = gui.vBox(self.controlArea, \"Color\")\n        self.color_cb = gui.palette_combo_box(self.palette_name)\n        self.color_cb.currentIndexChanged.connect(self.update_color_schema)\n        colorbox.layout().addWidget(self.color_cb)\n\n        form = QFormLayout(\n            formAlignment=Qt.AlignLeft,\n            labelAlignment=Qt.AlignLeft,\n            fieldGrowthPolicy=QFormLayout.AllNonFixedFieldsGrow\n        )\n        lowslider = gui.hSlider(\n            colorbox, self, \"threshold_low\", minValue=0.0, maxValue=1.0,\n            step=0.05, ticks=True, intOnly=False,\n            createLabel=False, callback=self.update_lowslider)\n        highslider = gui.hSlider(\n            colorbox, self, \"threshold_high\", minValue=0.0, maxValue=1.0,\n            step=0.05, ticks=True, intOnly=False,\n            createLabel=False, callback=self.update_highslider)\n\n        form.addRow(\"Low:\", lowslider)\n        form.addRow(\"High:\", highslider)\n\n        colorbox.layout().addLayout(form)\n\n        mergebox = gui.vBox(self.controlArea, \"Merge\",)\n        gui.checkBox(mergebox, self, \"merge_kmeans\", \"Merge by k-means\",\n                     callback=self.__update_row_clustering)\n        ibox = gui.indentedBox(mergebox)\n        gui.spin(ibox, self, \"merge_kmeans_k\", minv=5, maxv=500,\n                 label=\"Clusters:\", keyboardTracking=False,\n                 callbackOnReturn=True, callback=self.update_merge)\n\n        cluster_box = gui.vBox(self.controlArea, \"Clustering\")\n        # Row clustering\n        self.row_cluster_cb = cb = ComboBox()\n        cb.setModel(create_list_model(ClusteringModelData, self))\n        cbselect(cb, self.row_clustering, ClusteringRole)\n        self.connect_control(\n            \"row_clustering\",\n            lambda value, cb=cb: cbselect(cb, value, ClusteringRole)\n        )\n        @cb.activated.connect\n        def _(idx, cb=cb):\n            self.set_row_clustering(cb.itemData(idx, ClusteringRole))\n\n        # Column clustering\n        self.col_cluster_cb = cb = ComboBox()\n        cb.setModel(create_list_model(ClusteringModelData, self))\n        cbselect(cb, self.col_clustering, ClusteringRole)\n        self.connect_control(\n            \"col_clustering\",\n            lambda value, cb=cb: cbselect(cb, value, ClusteringRole)\n        )\n        @cb.activated.connect\n        def _(idx, cb=cb):\n            self.set_col_clustering(cb.itemData(idx, ClusteringRole))\n\n        form = QFormLayout(\n            labelAlignment=Qt.AlignLeft, formAlignment=Qt.AlignLeft,\n            fieldGrowthPolicy=QFormLayout.AllNonFixedFieldsGrow,\n        )\n        form.addRow(\"Rows:\", self.row_cluster_cb)\n        form.addRow(\"Columns:\", self.col_cluster_cb)\n        cluster_box.layout().addLayout(form)\n        box = gui.vBox(self.controlArea, \"Split By\")\n\n        self.row_split_model = DomainModel(\n            placeholder=\"(None)\",\n            valid_types=(Orange.data.DiscreteVariable,),\n            parent=self,\n        )\n        self.row_split_cb = cb = ComboBoxSearch(\n            enabled=not self.merge_kmeans,\n            sizeAdjustPolicy=ComboBox.AdjustToMinimumContentsLengthWithIcon,\n            minimumContentsLength=14,\n            toolTip=\"Split the heatmap vertically by a categorical column\"\n        )\n        self.row_split_cb.setModel(self.row_split_model)\n        self.connect_control(\n            \"split_by_var\", lambda value, cb=cb: cbselect(cb, value)\n        )\n        self.connect_control(\n            \"merge_kmeans\", self.row_split_cb.setDisabled\n        )\n        self.split_by_var = None\n\n        self.row_split_cb.activated.connect(\n            self.__on_split_rows_activated\n        )\n        box.layout().addWidget(self.row_split_cb)\n\n        box = gui.vBox(self.controlArea, 'Annotation && Legends')\n\n        gui.checkBox(box, self, 'legend', 'Show legend',\n                     callback=self.update_legend)\n\n        gui.checkBox(box, self, 'averages', 'Stripes with averages',\n                     callback=self.update_averages_stripe)\n        annotbox = QGroupBox(\"Row Annotations\", flat=True)\n        form = QFormLayout(\n            annotbox,\n            formAlignment=Qt.AlignLeft,\n            labelAlignment=Qt.AlignLeft,\n            fieldGrowthPolicy=QFormLayout.AllNonFixedFieldsGrow\n        )\n        self.annotation_model = DomainModel(placeholder=\"(None)\")\n        self.annotation_text_cb = ComboBoxSearch(\n            minimumContentsLength=12,\n            sizeAdjustPolicy=QComboBox.AdjustToMinimumContentsLength\n        )\n        self.annotation_text_cb.setModel(self.annotation_model)\n        self.annotation_text_cb.activated.connect(self.set_annotation_var)\n        self.connect_control(\"annotation_var\", self.annotation_var_changed)\n\n        self.row_side_color_model = DomainModel(\n            order=(DomainModel.CLASSES, DomainModel.Separator,\n                   DomainModel.METAS),\n            placeholder=\"(None)\", valid_types=DomainModel.PRIMITIVE,\n            flags=Qt.ItemIsSelectable | Qt.ItemIsEnabled,\n            parent=self,\n        )\n        self.row_side_color_cb = ComboBoxSearch(\n            sizeAdjustPolicy=QComboBox.AdjustToMinimumContentsLength,\n            minimumContentsLength=12\n        )\n        self.row_side_color_cb.setModel(self.row_side_color_model)\n        self.row_side_color_cb.activated.connect(self.set_annotation_color_var)\n        self.connect_control(\"annotation_color_var\", self.annotation_color_var_changed)\n        form.addRow(\"Text\", self.annotation_text_cb)\n        form.addRow(\"Color\", self.row_side_color_cb)\n        box.layout().addWidget(annotbox)\n        posbox = gui.vBox(box, \"Column Labels Position\", addSpace=False)\n        posbox.setFlat(True)\n        cb = gui.comboBox(\n            posbox, self, \"column_label_pos\",\n            callback=self.update_column_annotations)\n        cb.setModel(create_list_model(ColumnLabelsPosData, parent=self))\n        cb.setCurrentIndex(self.column_label_pos)\n        gui.checkBox(self.controlArea, self, \"keep_aspect\",\n                     \"Keep aspect ratio\", box=\"Resize\",\n                     callback=self.__aspect_mode_changed)\n\n        gui.rubber(self.controlArea)\n        gui.auto_send(self.controlArea, self, \"auto_commit\")\n\n        # Scene with heatmap\n        class HeatmapScene(QGraphicsScene):\n            widget: Optional[HeatmapGridWidget] = None\n\n        self.scene = self.scene = HeatmapScene(parent=self)\n        self.view = GraphicsView(\n            self.scene,\n            verticalScrollBarPolicy=Qt.ScrollBarAlwaysOn,\n            horizontalScrollBarPolicy=Qt.ScrollBarAlwaysOn,\n            viewportUpdateMode=QGraphicsView.FullViewportUpdate,\n            widgetResizable=True,\n        )\n        self.view.setContextMenuPolicy(Qt.CustomContextMenu)\n        self.view.customContextMenuRequested.connect(\n            self._on_view_context_menu\n        )\n        self.mainArea.layout().addWidget(self.view)\n        self.selected_rows = []\n        self.__font_inc = QAction(\n            \"Increase Font\", self, shortcut=QKeySequence(\"ctrl+>\"))\n        self.__font_dec = QAction(\n            \"Decrease Font\", self, shortcut=QKeySequence(\"ctrl+<\"))\n        self.__font_inc.triggered.connect(lambda: self.__adjust_font_size(1))\n        self.__font_dec.triggered.connect(lambda: self.__adjust_font_size(-1))\n        if hasattr(QAction, \"setShortcutVisibleInContextMenu\"):\n            apply_all(\n                [self.__font_inc, self.__font_dec],\n                lambda a: a.setShortcutVisibleInContextMenu(True)\n            )\n        self.addActions([self.__font_inc, self.__font_dec])\n\n    @property\n    def center_palette(self):\n        palette = self.color_cb.currentData()\n        return bool(palette.flags & palette.Diverging)\n\n    @property\n    def _column_label_pos(self) -> HeatmapGridWidget.Position:\n        return ColumnLabelsPosData[self.column_label_pos][Qt.UserRole]\n\n    def annotation_color_var_changed(self, value):\n        cbselect(self.row_side_color_cb, value, Qt.EditRole)\n\n    def annotation_var_changed(self, value):\n        cbselect(self.annotation_text_cb, value, Qt.EditRole)\n\n    def set_row_clustering(self, method: Clustering) -> None:\n        assert isinstance(method, Clustering)\n        if self.row_clustering != method:\n            self.row_clustering = method\n            cbselect(self.row_cluster_cb, method, ClusteringRole)\n            self.__update_row_clustering()\n\n    def set_col_clustering(self, method: Clustering) -> None:\n        assert isinstance(method, Clustering)\n        if self.col_clustering != method:\n            self.col_clustering = method\n            cbselect(self.col_cluster_cb, method, ClusteringRole)\n            self.__update_column_clustering()\n\n    def sizeHint(self) -> QSize:\n        return super().sizeHint().expandedTo(QSize(900, 700))\n\n    def color_palette(self):\n        return self.color_cb.currentData().lookup_table()\n\n    def color_map(self) -> GradientColorMap:\n        return GradientColorMap(\n            self.color_palette(), (self.threshold_low, self.threshold_high),\n            0 if self.center_palette else None\n        )\n\n    def clear(self):\n        self.data = None\n        self.input_data = None\n        self.effective_data = None\n        self.kmeans_model = None\n        self.merge_indices = None\n        self.annotation_model.set_domain(None)\n        self.annotation_var = None\n        self.row_side_color_model.set_domain(None)\n        self.annotation_color_var = None\n        self.row_split_model.set_domain(None)\n        self.split_by_var = None\n        self.parts = None\n        self.clear_scene()\n        self.selected_rows = []\n        self.__columns_cache.clear()\n        self.__rows_cache.clear()\n        self.__update_clustering_enable_state(None)\n\n    def clear_scene(self):\n        if self.scene.widget is not None:\n            self.scene.widget.layoutDidActivate.disconnect(\n                self.__on_layout_activate\n            )\n            self.scene.widget.selectionFinished.disconnect(\n                self.on_selection_finished\n            )\n        self.scene.widget = None\n        self.scene.clear()\n\n        self.view.setSceneRect(QRectF())\n        self.view.setHeaderSceneRect(QRectF())\n        self.view.setFooterSceneRect(QRectF())\n\n    @Inputs.data\n    def set_dataset(self, data=None):\n        \"\"\"Set the input dataset to display.\"\"\"\n        self.closeContext()\n        self.clear()\n        self.clear_messages()\n        self._set_input_summary(data)\n\n        if isinstance(data, SqlTable):\n            if data.approx_len() < 4000:\n                data = Table(data)\n            else:\n                self.Information.sampled()\n                data_sample = data.sample_time(1, no_cache=True)\n                data_sample.download_data(2000, partial=True)\n                data = Table(data_sample)\n\n        if data is not None and not len(data):\n            data = None\n\n        if data is not None and sp.issparse(data.X):\n            try:\n                data = data.to_dense()\n            except MemoryError:\n                data = None\n                self.Error.not_enough_memory()\n            else:\n                self.Information.sparse_densified()\n\n        input_data = data\n\n        # Data contains no attributes or meta attributes only\n        if data is not None and len(data.domain.attributes) == 0:\n            self.Error.no_continuous()\n            input_data = data = None\n\n        # Data contains some discrete attributes which must be filtered\n        if data is not None and \\\n                any(var.is_discrete for var in data.domain.attributes):\n            ndisc = sum(var.is_discrete for var in data.domain.attributes)\n            data = data.transform(\n                Domain([var for var in data.domain.attributes\n                        if var.is_continuous],\n                       data.domain.class_vars,\n                       data.domain.metas))\n            if not data.domain.attributes:\n                self.Error.no_continuous()\n                input_data = data = None\n            else:\n                self.Information.discrete_ignored(\n                    ndisc, \"s\" if ndisc > 1 else \"\")\n\n        self.data = data\n        self.input_data = input_data\n\n        if data is not None:\n            self.annotation_model.set_domain(self.input_data.domain)\n            self.row_side_color_model.set_domain(self.input_data.domain)\n            self.annotation_var = None\n            self.annotation_color_var = None\n            self.row_split_model.set_domain(data.domain)\n            if data.domain.has_discrete_class:\n                self.split_by_var = data.domain.class_var\n            else:\n                self.split_by_var = None\n            self.openContext(self.input_data)\n            if self.split_by_var not in self.row_split_model:\n                self.split_by_var = None\n\n        self.update_heatmaps()\n        if data is not None and self.__pending_selection is not None:\n            assert self.scene.widget is not None\n            self.scene.widget.selectRows(self.__pending_selection)\n            self.selected_rows = self.__pending_selection\n            self.__pending_selection = None\n\n        self.unconditional_commit()\n\n    def _set_input_summary(self, data):\n        summary = len(data) if data else self.info.NoInput\n        details = format_summary_details(data) if data else \"\"\n        self.info.set_input_summary(summary, details)\n\n    def __on_split_rows_activated(self):\n        self.set_split_variable(self.row_split_cb.currentData(Qt.EditRole))\n\n    def set_split_variable(self, var):\n        if var != self.split_by_var:\n            self.split_by_var = var\n            self.update_heatmaps()\n\n    def update_heatmaps(self):\n        if self.data is not None:\n            self.clear_scene()\n            self.clear_messages()\n            if self.col_clustering != Clustering.None_ and \\\n                    len(self.data.domain.attributes) < 2:\n                self.Error.not_enough_features()\n            elif (self.col_clustering != Clustering.None_ or\n                  self.row_clustering != Clustering.None_) and \\\n                    len(self.data) < 2:\n                self.Error.not_enough_instances()\n            elif self.merge_kmeans and len(self.data) < 3:\n                self.Error.not_enough_instances_k_means()\n            else:\n                parts = self.construct_heatmaps(self.data, self.split_by_var)\n                self.construct_heatmaps_scene(parts, self.effective_data)\n                self.selected_rows = []\n        else:\n            self.clear()\n        if self.parts is not None and self.effective_data is not None:\n            nrows = sum(len(p.indices) for p in self.parts.rows)\n            self.Warning.missing_split_values(\n                shown=nrows < len(self.effective_data))\n        else:\n            self.Warning.missing_split_values.clear()\n\n    def update_merge(self):\n        self.kmeans_model = None\n        self.merge_indices = None\n        if self.data is not None and self.merge_kmeans:\n            self.update_heatmaps()\n            self.commit()\n\n    def _make_parts(self, data, group_var=None):\n        \"\"\"\n        Make initial `Parts` for data, split by group_var, group_key\n        \"\"\"\n        if group_var is not None:\n            assert group_var.is_discrete\n            _col_data = table_column_data(data, group_var)\n            row_indices = [np.flatnonzero(_col_data == i)\n                           for i in range(len(group_var.values))]\n            row_groups = [RowPart(title=name, indices=ind,\n                                  cluster=None, cluster_ordered=None)\n                          for name, ind in zip(group_var.values, row_indices)]\n        else:\n            row_groups = [RowPart(title=None, indices=range(0, len(data)),\n                                  cluster=None, cluster_ordered=None)]\n\n        col_groups = [\n            ColumnPart(\n                title=None, indices=range(0, len(data.domain.attributes)),\n                domain=data.domain, cluster=None, cluster_ordered=None)\n        ]\n\n        minv, maxv = np.nanmin(data.X), np.nanmax(data.X)\n        return Parts(row_groups, col_groups, span=(minv, maxv))\n\n    def cluster_rows(self, data: Table, parts: 'Parts', ordered=False) -> 'Parts':\n        row_groups = []\n        for row in parts.rows:\n            if row.cluster is not None:\n                cluster = row.cluster\n            else:\n                cluster = None\n            if row.cluster_ordered is not None:\n                cluster_ord = row.cluster_ordered\n            else:\n                cluster_ord = None\n\n            if row.can_cluster:\n                matrix = None\n                need_dist = cluster is None or (ordered and cluster_ord is None)\n                if need_dist:\n                    subset = data[row.indices]\n                    matrix = Orange.distance.Euclidean(subset)\n\n                if cluster is None:\n                    assert len(matrix) < self.MaxClustering\n                    cluster = hierarchical.dist_matrix_clustering(\n                        matrix, linkage=hierarchical.WARD\n                    )\n                if ordered and cluster_ord is None:\n                    assert len(matrix) < self.MaxOrderedClustering\n                    cluster_ord = hierarchical.optimal_leaf_ordering(\n                        cluster, matrix,\n                    )\n            row_groups.append(row._replace(cluster=cluster, cluster_ordered=cluster_ord))\n\n        return parts._replace(rows=row_groups)\n\n    def cluster_columns(self, data, parts, ordered=False):\n        assert len(parts.columns) == 1, \"columns split is no longer supported\"\n        assert all(var.is_continuous for var in data.domain.attributes)\n\n        col0 = parts.columns[0]\n        if col0.cluster is not None:\n            cluster = col0.cluster\n        else:\n            cluster = None\n        if col0.cluster_ordered is not None:\n            cluster_ord = col0.cluster_ordered\n        else:\n            cluster_ord = None\n        need_dist = cluster is None or (ordered and cluster_ord is None)\n        matrix = None\n        if need_dist:\n            data = Orange.distance._preprocess(data)\n            matrix = np.asarray(Orange.distance.PearsonR(data, axis=0))\n            # nan values break clustering below\n            matrix = np.nan_to_num(matrix)\n\n        if cluster is None:\n            assert matrix is not None\n            assert len(matrix) < self.MaxClustering\n            cluster = hierarchical.dist_matrix_clustering(\n                matrix, linkage=hierarchical.WARD\n            )\n        if ordered and cluster_ord is None:\n            assert len(matrix) < self.MaxOrderedClustering\n            cluster_ord = hierarchical.optimal_leaf_ordering(cluster, matrix)\n\n        col_groups = [col._replace(cluster=cluster, cluster_ordered=cluster_ord)\n                      for col in parts.columns]\n        return parts._replace(columns=col_groups)\n\n    def construct_heatmaps(self, data, group_var=None) -> 'Parts':\n        if self.merge_kmeans:\n            if self.kmeans_model is None:\n                effective_data = self.input_data.transform(\n                    Orange.data.Domain(\n                        [var for var in self.input_data.domain.attributes\n                         if var.is_continuous],\n                        self.input_data.domain.class_vars,\n                        self.input_data.domain.metas))\n                nclust = min(self.merge_kmeans_k, len(effective_data) - 1)\n                self.kmeans_model = kmeans_compress(effective_data, k=nclust)\n                effective_data.domain = self.kmeans_model.domain\n                merge_indices = [np.flatnonzero(self.kmeans_model.labels == ind)\n                                 for ind in range(nclust)]\n                not_empty_indices = [i for i, x in enumerate(merge_indices)\n                                     if len(x) > 0]\n                self.merge_indices = \\\n                    [merge_indices[i] for i in not_empty_indices]\n                if len(merge_indices) != len(self.merge_indices):\n                    self.Warning.empty_clusters()\n                effective_data = Orange.data.Table(\n                    Orange.data.Domain(effective_data.domain.attributes),\n                    self.kmeans_model.centroids[not_empty_indices]\n                )\n            else:\n                effective_data = self.effective_data\n\n            group_var = None\n        else:\n            self.kmeans_model = None\n            self.merge_indices = None\n            effective_data = data\n\n        self.effective_data = effective_data\n\n        self.__update_clustering_enable_state(effective_data)\n\n        parts = self._make_parts(effective_data, group_var)\n        # Restore/update the row/columns items descriptions from cache if\n        # available\n        rows_cache_key = (group_var,\n                          self.merge_kmeans_k if self.merge_kmeans else None)\n        if rows_cache_key in self.__rows_cache:\n            parts = parts._replace(rows=self.__rows_cache[rows_cache_key].rows)\n\n        if self.row_clustering != Clustering.None_:\n            parts = self.cluster_rows(\n                effective_data, parts,\n                ordered=self.row_clustering == Clustering.OrderedClustering\n            )\n        if self.col_clustering != Clustering.None_:\n            parts = self.cluster_columns(\n                effective_data, parts,\n                ordered=self.col_clustering == Clustering.OrderedClustering\n            )\n\n        # Cache the updated parts\n        self.__rows_cache[rows_cache_key] = parts\n        return parts\n\n    def construct_heatmaps_scene(self, parts: 'Parts', data: Table) -> None:\n        _T = TypeVar(\"_T\", bound=Union[RowPart, ColumnPart])\n\n        def select_cluster(clustering: Clustering, item: _T) -> _T:\n            if clustering == Clustering.None_:\n                return item._replace(cluster=None, cluster_ordered=None)\n            elif clustering == Clustering.Clustering:\n                return item._replace(cluster=item.cluster, cluster_ordered=None)\n            elif clustering == Clustering.OrderedClustering:\n                return item._replace(cluster=item.cluster_ordered, cluster_ordered=None)\n            else:  # pragma: no cover\n                raise TypeError()\n\n        rows = [select_cluster(self.row_clustering, rowitem)\n                for rowitem in parts.rows]\n        cols = [select_cluster(self.col_clustering, colitem)\n                for colitem in parts.columns]\n        parts = Parts(columns=cols, rows=rows, span=parts.span)\n\n        self.setup_scene(parts, data)\n\n    def setup_scene(self, parts, data):\n        # type: (Parts, Table) -> None\n        widget = HeatmapGridWidget()\n        widget.setColorMap(self.color_map())\n        self.scene.addItem(widget)\n        self.scene.widget = widget\n        columns = [v.name for v in data.domain.attributes]\n        parts = HeatmapGridWidget.Parts(\n            rows=[\n                HeatmapGridWidget.RowItem(r.title, r.indices, r.cluster)\n                for r in parts.rows\n            ],\n            columns=[\n                HeatmapGridWidget.ColumnItem(c.title, c.indices, c.cluster)\n                for c in parts.columns\n            ],\n            data=data.X,\n            span=parts.span,\n            row_names=None,\n            col_names=columns,\n        )\n        widget.setHeatmaps(parts)\n        side = self.row_side_colors()\n        if side is not None:\n            widget.setRowSideColorAnnotations(side[0], side[1], name=side[2].name)\n        widget.setColumnLabelsPosition(self._column_label_pos)\n        widget.setAspectRatioMode(\n            Qt.KeepAspectRatio if self.keep_aspect else Qt.IgnoreAspectRatio\n        )\n        widget.setShowAverages(self.averages)\n        widget.setLegendVisible(self.legend)\n\n        widget.layoutDidActivate.connect(self.__on_layout_activate)\n        widget.selectionFinished.connect(self.on_selection_finished)\n\n        self.update_annotations()\n        self.view.setCentralWidget(widget)\n        self.parts = parts\n\n    def __update_scene_rects(self):\n        widget = self.scene.widget\n        if widget is None:\n            return\n        rect = widget.geometry()\n        self.scene.setSceneRect(rect)\n        self.view.setSceneRect(rect)\n        self.view.setHeaderSceneRect(widget.headerGeometry())\n        self.view.setFooterSceneRect(widget.footerGeometry())\n\n    def __on_layout_activate(self):\n        self.__update_scene_rects()\n\n    def __aspect_mode_changed(self):\n        widget = self.scene.widget\n        if widget is None:\n            return\n        widget.setAspectRatioMode(\n            Qt.KeepAspectRatio if self.keep_aspect else Qt.IgnoreAspectRatio\n        )\n        # when aspect fixed the vertical sh is fixex, when not, it can\n        # shrink vertically\n        sp = widget.sizePolicy()\n        if self.keep_aspect:\n            sp.setVerticalPolicy(QSizePolicy.Fixed)\n        else:\n            sp.setVerticalPolicy(QSizePolicy.Preferred)\n        widget.setSizePolicy(sp)\n\n    def __update_clustering_enable_state(self, data):\n        if data is not None:\n            N = len(data)\n            M = len(data.domain.attributes)\n        else:\n            N = M = 0\n\n        rc_enabled = N <= self.MaxClustering\n        rco_enabled = N <= self.MaxOrderedClustering\n        cc_enabled = M <= self.MaxClustering\n        cco_enabled = M <= self.MaxOrderedClustering\n        row_clust, col_clust = self.row_clustering, self.col_clustering\n\n        row_clust_msg = \"\"\n        col_clust_msg = \"\"\n\n        if not rco_enabled and row_clust == Clustering.OrderedClustering:\n            row_clust = Clustering.Clustering\n            row_clust_msg = \"Row cluster ordering was disabled due to the \" \\\n                            \"input matrix being to big\"\n        if not rc_enabled and row_clust == Clustering.Clustering:\n            row_clust = Clustering.None_\n            row_clust_msg = \"Row clustering was was disabled due to the \" \\\n                            \"input matrix being to big\"\n\n        if not cco_enabled and col_clust == Clustering.OrderedClustering:\n            col_clust = Clustering.Clustering\n            col_clust_msg = \"Column cluster ordering was disabled due to \" \\\n                            \"the input matrix being to big\"\n        if not cc_enabled and col_clust == Clustering.Clustering:\n            col_clust = Clustering.None_\n            col_clust_msg = \"Column clustering was disabled due to the \" \\\n                            \"input matrix being to big\"\n\n        self.col_clustering = col_clust\n        self.row_clustering = row_clust\n\n        self.Information.row_clust(row_clust_msg, shown=bool(row_clust_msg))\n        self.Information.col_clust(col_clust_msg, shown=bool(col_clust_msg))\n\n        # Disable/enable the combobox items for the clustering methods\n        def setenabled(cb: QComboBox, clu: bool, clu_op: bool):\n            model = cb.model()\n            assert isinstance(model, QStandardItemModel)\n            idx = cb.findData(Clustering.OrderedClustering, ClusteringRole)\n            assert idx != -1\n            model.item(idx).setEnabled(clu_op)\n            idx = cb.findData(Clustering.Clustering, ClusteringRole)\n            assert idx != -1\n            model.item(idx).setEnabled(clu)\n\n        setenabled(self.row_cluster_cb, rc_enabled, rco_enabled)\n        setenabled(self.col_cluster_cb, cc_enabled, cco_enabled)\n\n    def update_averages_stripe(self):\n        \"\"\"Update the visibility of the averages stripe.\n        \"\"\"\n        widget = self.scene.widget\n        if widget is not None:\n            widget.setShowAverages(self.averages)\n\n    def update_lowslider(self):\n        low, high = self.controls.threshold_low, self.controls.threshold_high\n        if low.value() >= high.value():\n            low.setSliderPosition(high.value() - 1)\n        self.update_color_schema()\n\n    def update_highslider(self):\n        low, high = self.controls.threshold_low, self.controls.threshold_high\n        if low.value() >= high.value():\n            high.setSliderPosition(low.value() + 1)\n        self.update_color_schema()\n\n    def update_color_schema(self):\n        self.palette_name = self.color_cb.currentData().name\n        w = self.scene.widget\n        if w is not None:\n            w.setColorMap(self.color_map())\n\n    def __update_column_clustering(self):\n        self.update_heatmaps()\n        self.commit()\n\n    def __update_row_clustering(self):\n        self.update_heatmaps()\n        self.commit()\n\n    def update_legend(self):\n        widget = self.scene.widget\n        if widget is not None:\n            widget.setLegendVisible(self.legend)\n\n    def row_annotation_var(self):\n        return self.annotation_var\n\n    def row_annotation_data(self):\n        var = self.row_annotation_var()\n        if var is None:\n            return None\n        return column_str_from_table(self.input_data, var)\n\n    def _merge_row_indices(self):\n        if self.merge_kmeans and self.kmeans_model is not None:\n            return self.merge_indices\n        else:\n            return None\n\n    def set_annotation_var(self, var: Union[None, Variable, int]):\n        if isinstance(var, int):\n            var = self.annotation_model[var]\n        if self.annotation_var != var:\n            self.annotation_var = var\n            self.update_annotations()\n\n    def update_annotations(self):\n        widget = self.scene.widget\n        if widget is not None:\n            annot_col = self.row_annotation_data()\n            merge_indices = self._merge_row_indices()\n            if merge_indices is not None and annot_col is not None:\n                join = lambda _1: join_elided(\", \", 42, _1, \" ({} more)\")\n                annot_col = aggregate_apply(join, annot_col, merge_indices)\n            if annot_col is not None:\n                widget.setRowLabels(annot_col)\n                widget.setRowLabelsVisible(True)\n            else:\n                widget.setRowLabelsVisible(False)\n                widget.setRowLabels(None)\n\n    def row_side_colors(self):\n        var = self.annotation_color_var\n        if var is None:\n            return None\n        column_data = column_data_from_table(self.input_data, var)\n        span = (np.nanmin(column_data), np.nanmax(column_data))\n        merges = self._merge_row_indices()\n        if merges is not None:\n            column_data = aggregate(var, column_data, merges)\n        data, colormap = self._colorize(var, column_data)\n        if var.is_continuous:\n            colormap.span = span\n        return data, colormap, var\n\n    def set_annotation_color_var(self, var: Union[None, Variable, int]):\n        \"\"\"Set the current side color annotation variable.\"\"\"\n        if isinstance(var, int):\n            var = self.row_side_color_model[var]\n        if self.annotation_color_var != var:\n            self.annotation_color_var = var\n            self.update_row_side_colors()\n\n    def update_row_side_colors(self):\n        widget = self.scene.widget\n        if widget is None:\n            return\n        colors = self.row_side_colors()\n        if colors is None:\n            widget.setRowSideColorAnnotations(None)\n        else:\n            widget.setRowSideColorAnnotations(colors[0], colors[1], colors[2].name)\n\n    def _colorize(self, var: Variable, data: np.ndarray) -> Tuple[np.ndarray, ColorMap]:\n        palette = var.palette  # type: Palette\n        colors = np.array(\n            [[c.red(), c.green(), c.blue()] for c in palette.qcolors_w_nan],\n            dtype=np.uint8,\n        )\n        if var.is_discrete:\n            mask = np.isnan(data)\n            data[mask] = -1\n            data = data.astype(int)\n            if mask.any():\n                values = (*var.values, \"N/A\")\n            else:\n                values = var.values\n                colors = colors[: -1]\n            return data, CategoricalColorMap(colors, values)\n        elif var.is_continuous:\n            cmap = GradientColorMap(colors[:-1])\n            return data, cmap\n        else:\n            raise TypeError\n\n    def update_column_annotations(self):\n        widget = self.scene.widget\n        if self.data is not None and widget is not None:\n            widget.setColumnLabelsPosition(self._column_label_pos)\n\n    def __adjust_font_size(self, diff):\n        widget = self.scene.widget\n        if widget is None:\n            return\n        curr = widget.font().pointSizeF()\n        new = curr + diff\n\n        self.__font_dec.setEnabled(new > 1.0)\n        self.__font_inc.setEnabled(new <= 32)\n        if new > 1.0:\n            font = QFont()\n            font.setPointSizeF(new)\n            widget.setFont(font)\n\n    def _on_view_context_menu(self, pos):\n        widget = self.scene.widget\n        if widget is None:\n            return\n        assert isinstance(widget, HeatmapGridWidget)\n        menu = QMenu(self.view.viewport())\n        menu.setAttribute(Qt.WA_DeleteOnClose)\n        menu.addActions(self.view.actions())\n        menu.addSeparator()\n        menu.addActions([self.__font_inc, self.__font_dec])\n        menu.addSeparator()\n        a = QAction(\"Keep aspect ratio\", menu, checkable=True)\n        a.setChecked(self.keep_aspect)\n\n        def ontoggled(state):\n            self.keep_aspect = state\n            self.__aspect_mode_changed()\n        a.toggled.connect(ontoggled)\n        menu.addAction(a)\n        menu.popup(self.view.viewport().mapToGlobal(pos))\n\n    def on_selection_finished(self):\n        if self.scene.widget is not None:\n            self.selected_rows = list(self.scene.widget.selectedRows())\n        else:\n            self.selected_rows = []\n        self.commit()\n\n    def commit(self):\n        data = None\n        indices = None\n        if self.merge_kmeans:\n            merge_indices = self.merge_indices\n        else:\n            merge_indices = None\n\n        if self.input_data is not None and self.selected_rows:\n            indices = self.selected_rows\n            if merge_indices is not None:\n                # expand merged indices\n                indices = np.hstack([merge_indices[i] for i in indices])\n\n            data = self.input_data[indices]\n\n        summary = len(data) if data else self.info.NoOutput\n        details = format_summary_details(data) if data else \"\"\n        self.info.set_output_summary(summary, details)\n        self.Outputs.selected_data.send(data)\n        self.Outputs.annotated_data.send(create_annotated_table(self.input_data, indices))\n\n    def onDeleteWidget(self):\n        self.clear()\n        super().onDeleteWidget()\n\n    def send_report(self):\n        self.report_items((\n            (\"Columns:\", \"Clustering\" if self.col_clustering else \"No sorting\"),\n            (\"Rows:\", \"Clustering\" if self.row_clustering else \"No sorting\"),\n            (\"Split:\",\n             self.split_by_var is not None and self.split_by_var.name),\n            (\"Row annotation\",\n             self.annotation_var is not None and self.annotation_var.name),\n        ))\n        self.report_plot()\n\n    @classmethod\n    def migrate_settings(cls, settings, version):\n        if version is not None and version < 3:\n            def st2cl(state: bool) -> Clustering:\n                return Clustering.OrderedClustering if state else \\\n                    Clustering.None_\n\n            rc = settings.pop(\"row_clustering\", False)\n            cc = settings.pop(\"col_clustering\", False)\n            settings[\"row_clustering_method\"] = st2cl(rc).name\n            settings[\"col_clustering_method\"] = st2cl(cc).name",
                                "ast_type_str": "ClassDef",
                                "start_lineno": 132,
                                "end_lineno": 1138,
                                "parent": null
                            }
                        },
                        {
                            "file_name": "Orange/widgets/visualize/owheatmap.py",
                            "type": "class",
                            "name": "OWHeatMap",
                            "content": "class OWHeatMap(widget.OWWidget):\n    name = \"Heat Map\"\n    description = \"Plot a data matrix heatmap.\"\n    icon = \"icons/Heatmap.svg\"\n    priority = 260\n    keywords = []\n\n    class Inputs:\n        data = Input(\"Data\", Table)\n\n    class Outputs:\n        selected_data = Output(\"Selected Data\", Table, default=True)\n        annotated_data = Output(ANNOTATED_DATA_SIGNAL_NAME, Table)\n\n    settings_version = 3\n\n    settingsHandler = settings.DomainContextHandler()\n\n    # Disable clustering for inputs bigger than this\n    MaxClustering = 25000\n    # Disable cluster leaf ordering for inputs bigger than this\n    MaxOrderedClustering = 1000\n\n    threshold_low = settings.Setting(0.0)\n    threshold_high = settings.Setting(1.0)\n\n    merge_kmeans = settings.Setting(False)\n    merge_kmeans_k = settings.Setting(50)\n\n    # Display column with averages\n    averages: bool = settings.Setting(True)\n    # Display legend\n    legend: bool = settings.Setting(True)\n    # Annotations\n    #: text row annotation (row names)\n    annotation_var = settings.ContextSetting(None)\n    #: color row annotation\n    annotation_color_var = settings.ContextSetting(None)\n    # Discrete variable used to split that data/heatmaps (vertically)\n    split_by_var = settings.ContextSetting(None)\n    # Selected row/column clustering method (name)\n    col_clustering_method: str = settings.Setting(Clustering.None_.name)\n    row_clustering_method: str = settings.Setting(Clustering.None_.name)\n\n    palette_name = settings.Setting(colorpalettes.DefaultContinuousPaletteName)\n    column_label_pos: int = settings.Setting(1)\n    selected_rows: List[int] = settings.Setting(None, schema_only=True)\n\n    auto_commit = settings.Setting(True)\n\n    graph_name = \"scene\"\n\n    left_side_scrolling = True\n\n    class Information(widget.OWWidget.Information):\n        sampled = Msg(\"Data has been sampled\")\n        discrete_ignored = Msg(\"{} categorical feature{} ignored\")\n        row_clust = Msg(\"{}\")\n        col_clust = Msg(\"{}\")\n        sparse_densified = Msg(\"Showing this data may require a lot of memory\")\n\n    class Error(widget.OWWidget.Error):\n        no_continuous = Msg(\"No numeric features\")\n        not_enough_features = Msg(\"Not enough features for column clustering\")\n        not_enough_instances = Msg(\"Not enough instances for clustering\")\n        not_enough_instances_k_means = Msg(\n            \"Not enough instances for k-means merging\")\n        not_enough_memory = Msg(\"Not enough memory to show this data\")\n\n    class Warning(widget.OWWidget.Warning):\n        empty_clusters = Msg(\"Empty clusters were removed\")\n        missing_split_values = Msg(\n            \"Instances with undefined split variable values are not shown\")\n\n    def __init__(self):\n        super().__init__()\n        self.__pending_selection = self.selected_rows\n\n        # A kingdom for a save_state/restore_state\n        self.col_clustering = enum_get(\n            Clustering, self.col_clustering_method, Clustering.None_)\n        self.row_clustering = enum_get(\n            Clustering, self.row_clustering_method, Clustering.None_)\n\n        @self.settingsAboutToBePacked.connect\n        def _():\n            self.col_clustering_method = self.col_clustering.name\n            self.row_clustering_method = self.row_clustering.name\n\n        self.keep_aspect = False\n\n        #: The original data with all features (retained to\n        #: preserve the domain on the output)\n        self.input_data = None\n        #: The effective data striped of discrete features, and often\n        #: merged using k-means\n        self.data = None\n        self.effective_data = None\n        #: kmeans model used to merge rows of input_data\n        self.kmeans_model = None\n        #: merge indices derived from kmeans\n        #: a list (len==k) of int ndarray where the i-th item contains\n        #: the indices which merge the input_data into the heatmap row i\n        self.merge_indices = None\n        self.parts: Optional[Parts] = None\n        self.__rows_cache = {}\n        self.__columns_cache = {}\n\n        self.info.set_input_summary(self.info.NoInput)\n        self.info.set_output_summary(self.info.NoOutput)\n\n        # GUI definition\n        colorbox = gui.vBox(self.controlArea, \"Color\")\n        self.color_cb = gui.palette_combo_box(self.palette_name)\n        self.color_cb.currentIndexChanged.connect(self.update_color_schema)\n        colorbox.layout().addWidget(self.color_cb)\n\n        form = QFormLayout(\n            formAlignment=Qt.AlignLeft,\n            labelAlignment=Qt.AlignLeft,\n            fieldGrowthPolicy=QFormLayout.AllNonFixedFieldsGrow\n        )\n        lowslider = gui.hSlider(\n            colorbox, self, \"threshold_low\", minValue=0.0, maxValue=1.0,\n            step=0.05, ticks=True, intOnly=False,\n            createLabel=False, callback=self.update_lowslider)\n        highslider = gui.hSlider(\n            colorbox, self, \"threshold_high\", minValue=0.0, maxValue=1.0,\n            step=0.05, ticks=True, intOnly=False,\n            createLabel=False, callback=self.update_highslider)\n\n        form.addRow(\"Low:\", lowslider)\n        form.addRow(\"High:\", highslider)\n\n        colorbox.layout().addLayout(form)\n\n        mergebox = gui.vBox(self.controlArea, \"Merge\",)\n        gui.checkBox(mergebox, self, \"merge_kmeans\", \"Merge by k-means\",\n                     callback=self.__update_row_clustering)\n        ibox = gui.indentedBox(mergebox)\n        gui.spin(ibox, self, \"merge_kmeans_k\", minv=5, maxv=500,\n                 label=\"Clusters:\", keyboardTracking=False,\n                 callbackOnReturn=True, callback=self.update_merge)\n\n        cluster_box = gui.vBox(self.controlArea, \"Clustering\")\n        # Row clustering\n        self.row_cluster_cb = cb = ComboBox()\n        cb.setModel(create_list_model(ClusteringModelData, self))\n        cbselect(cb, self.row_clustering, ClusteringRole)\n        self.connect_control(\n            \"row_clustering\",\n            lambda value, cb=cb: cbselect(cb, value, ClusteringRole)\n        )\n        @cb.activated.connect\n        def _(idx, cb=cb):\n            self.set_row_clustering(cb.itemData(idx, ClusteringRole))\n\n        # Column clustering\n        self.col_cluster_cb = cb = ComboBox()\n        cb.setModel(create_list_model(ClusteringModelData, self))\n        cbselect(cb, self.col_clustering, ClusteringRole)\n        self.connect_control(\n            \"col_clustering\",\n            lambda value, cb=cb: cbselect(cb, value, ClusteringRole)\n        )\n        @cb.activated.connect\n        def _(idx, cb=cb):\n            self.set_col_clustering(cb.itemData(idx, ClusteringRole))\n\n        form = QFormLayout(\n            labelAlignment=Qt.AlignLeft, formAlignment=Qt.AlignLeft,\n            fieldGrowthPolicy=QFormLayout.AllNonFixedFieldsGrow,\n        )\n        form.addRow(\"Rows:\", self.row_cluster_cb)\n        form.addRow(\"Columns:\", self.col_cluster_cb)\n        cluster_box.layout().addLayout(form)\n        box = gui.vBox(self.controlArea, \"Split By\")\n\n        self.row_split_model = DomainModel(\n            placeholder=\"(None)\",\n            valid_types=(Orange.data.DiscreteVariable,),\n            parent=self,\n        )\n        self.row_split_cb = cb = ComboBoxSearch(\n            enabled=not self.merge_kmeans,\n            sizeAdjustPolicy=ComboBox.AdjustToMinimumContentsLengthWithIcon,\n            minimumContentsLength=14,\n            toolTip=\"Split the heatmap vertically by a categorical column\"\n        )\n        self.row_split_cb.setModel(self.row_split_model)\n        self.connect_control(\n            \"split_by_var\", lambda value, cb=cb: cbselect(cb, value)\n        )\n        self.connect_control(\n            \"merge_kmeans\", self.row_split_cb.setDisabled\n        )\n        self.split_by_var = None\n\n        self.row_split_cb.activated.connect(\n            self.__on_split_rows_activated\n        )\n        box.layout().addWidget(self.row_split_cb)\n\n        box = gui.vBox(self.controlArea, 'Annotation && Legends')\n\n        gui.checkBox(box, self, 'legend', 'Show legend',\n                     callback=self.update_legend)\n\n        gui.checkBox(box, self, 'averages', 'Stripes with averages',\n                     callback=self.update_averages_stripe)\n        annotbox = QGroupBox(\"Row Annotations\", flat=True)\n        form = QFormLayout(\n            annotbox,\n            formAlignment=Qt.AlignLeft,\n            labelAlignment=Qt.AlignLeft,\n            fieldGrowthPolicy=QFormLayout.AllNonFixedFieldsGrow\n        )\n        self.annotation_model = DomainModel(placeholder=\"(None)\")\n        self.annotation_text_cb = ComboBoxSearch(\n            minimumContentsLength=12,\n            sizeAdjustPolicy=QComboBox.AdjustToMinimumContentsLength\n        )\n        self.annotation_text_cb.setModel(self.annotation_model)\n        self.annotation_text_cb.activated.connect(self.set_annotation_var)\n        self.connect_control(\"annotation_var\", self.annotation_var_changed)\n\n        self.row_side_color_model = DomainModel(\n            order=(DomainModel.CLASSES, DomainModel.Separator,\n                   DomainModel.METAS),\n            placeholder=\"(None)\", valid_types=DomainModel.PRIMITIVE,\n            flags=Qt.ItemIsSelectable | Qt.ItemIsEnabled,\n            parent=self,\n        )\n        self.row_side_color_cb = ComboBoxSearch(\n            sizeAdjustPolicy=QComboBox.AdjustToMinimumContentsLength,\n            minimumContentsLength=12\n        )\n        self.row_side_color_cb.setModel(self.row_side_color_model)\n        self.row_side_color_cb.activated.connect(self.set_annotation_color_var)\n        self.connect_control(\"annotation_color_var\", self.annotation_color_var_changed)\n        form.addRow(\"Text\", self.annotation_text_cb)\n        form.addRow(\"Color\", self.row_side_color_cb)\n        box.layout().addWidget(annotbox)\n        posbox = gui.vBox(box, \"Column Labels Position\", addSpace=False)\n        posbox.setFlat(True)\n        cb = gui.comboBox(\n            posbox, self, \"column_label_pos\",\n            callback=self.update_column_annotations)\n        cb.setModel(create_list_model(ColumnLabelsPosData, parent=self))\n        cb.setCurrentIndex(self.column_label_pos)\n        gui.checkBox(self.controlArea, self, \"keep_aspect\",\n                     \"Keep aspect ratio\", box=\"Resize\",\n                     callback=self.__aspect_mode_changed)\n\n        gui.rubber(self.controlArea)\n        gui.auto_send(self.controlArea, self, \"auto_commit\")\n\n        # Scene with heatmap\n        class HeatmapScene(QGraphicsScene):\n            widget: Optional[HeatmapGridWidget] = None\n\n        self.scene = self.scene = HeatmapScene(parent=self)\n        self.view = GraphicsView(\n            self.scene,\n            verticalScrollBarPolicy=Qt.ScrollBarAlwaysOn,\n            horizontalScrollBarPolicy=Qt.ScrollBarAlwaysOn,\n            viewportUpdateMode=QGraphicsView.FullViewportUpdate,\n            widgetResizable=True,\n        )\n        self.view.setContextMenuPolicy(Qt.CustomContextMenu)\n        self.view.customContextMenuRequested.connect(\n            self._on_view_context_menu\n        )\n        self.mainArea.layout().addWidget(self.view)\n        self.selected_rows = []\n        self.__font_inc = QAction(\n            \"Increase Font\", self, shortcut=QKeySequence(\"ctrl+>\"))\n        self.__font_dec = QAction(\n            \"Decrease Font\", self, shortcut=QKeySequence(\"ctrl+<\"))\n        self.__font_inc.triggered.connect(lambda: self.__adjust_font_size(1))\n        self.__font_dec.triggered.connect(lambda: self.__adjust_font_size(-1))\n        if hasattr(QAction, \"setShortcutVisibleInContextMenu\"):\n            apply_all(\n                [self.__font_inc, self.__font_dec],\n                lambda a: a.setShortcutVisibleInContextMenu(True)\n            )\n        self.addActions([self.__font_inc, self.__font_dec])\n\n    @property\n    def center_palette(self):\n        palette = self.color_cb.currentData()\n        return bool(palette.flags & palette.Diverging)\n\n    @property\n    def _column_label_pos(self) -> HeatmapGridWidget.Position:\n        return ColumnLabelsPosData[self.column_label_pos][Qt.UserRole]\n\n    def annotation_color_var_changed(self, value):\n        cbselect(self.row_side_color_cb, value, Qt.EditRole)\n\n    def annotation_var_changed(self, value):\n        cbselect(self.annotation_text_cb, value, Qt.EditRole)\n\n    def set_row_clustering(self, method: Clustering) -> None:\n        assert isinstance(method, Clustering)\n        if self.row_clustering != method:\n            self.row_clustering = method\n            cbselect(self.row_cluster_cb, method, ClusteringRole)\n            self.__update_row_clustering()\n\n    def set_col_clustering(self, method: Clustering) -> None:\n        assert isinstance(method, Clustering)\n        if self.col_clustering != method:\n            self.col_clustering = method\n            cbselect(self.col_cluster_cb, method, ClusteringRole)\n            self.__update_column_clustering()\n\n    def sizeHint(self) -> QSize:\n        return super().sizeHint().expandedTo(QSize(900, 700))\n\n    def color_palette(self):\n        return self.color_cb.currentData().lookup_table()\n\n    def color_map(self) -> GradientColorMap:\n        return GradientColorMap(\n            self.color_palette(), (self.threshold_low, self.threshold_high),\n            0 if self.center_palette else None\n        )\n\n    def clear(self):\n        self.data = None\n        self.input_data = None\n        self.effective_data = None\n        self.kmeans_model = None\n        self.merge_indices = None\n        self.annotation_model.set_domain(None)\n        self.annotation_var = None\n        self.row_side_color_model.set_domain(None)\n        self.annotation_color_var = None\n        self.row_split_model.set_domain(None)\n        self.split_by_var = None\n        self.parts = None\n        self.clear_scene()\n        self.selected_rows = []\n        self.__columns_cache.clear()\n        self.__rows_cache.clear()\n        self.__update_clustering_enable_state(None)\n\n    def clear_scene(self):\n        if self.scene.widget is not None:\n            self.scene.widget.layoutDidActivate.disconnect(\n                self.__on_layout_activate\n            )\n            self.scene.widget.selectionFinished.disconnect(\n                self.on_selection_finished\n            )\n        self.scene.widget = None\n        self.scene.clear()\n\n        self.view.setSceneRect(QRectF())\n        self.view.setHeaderSceneRect(QRectF())\n        self.view.setFooterSceneRect(QRectF())\n\n    @Inputs.data\n    def set_dataset(self, data=None):\n        \"\"\"Set the input dataset to display.\"\"\"\n        self.closeContext()\n        self.clear()\n        self.clear_messages()\n        self._set_input_summary(data)\n\n        if isinstance(data, SqlTable):\n            if data.approx_len() < 4000:\n                data = Table(data)\n            else:\n                self.Information.sampled()\n                data_sample = data.sample_time(1, no_cache=True)\n                data_sample.download_data(2000, partial=True)\n                data = Table(data_sample)\n\n        if data is not None and not len(data):\n            data = None\n\n        if data is not None and sp.issparse(data.X):\n            try:\n                data = data.to_dense()\n            except MemoryError:\n                data = None\n                self.Error.not_enough_memory()\n            else:\n                self.Information.sparse_densified()\n\n        input_data = data\n\n        # Data contains no attributes or meta attributes only\n        if data is not None and len(data.domain.attributes) == 0:\n            self.Error.no_continuous()\n            input_data = data = None\n\n        # Data contains some discrete attributes which must be filtered\n        if data is not None and \\\n                any(var.is_discrete for var in data.domain.attributes):\n            ndisc = sum(var.is_discrete for var in data.domain.attributes)\n            data = data.transform(\n                Domain([var for var in data.domain.attributes\n                        if var.is_continuous],\n                       data.domain.class_vars,\n                       data.domain.metas))\n            if not data.domain.attributes:\n                self.Error.no_continuous()\n                input_data = data = None\n            else:\n                self.Information.discrete_ignored(\n                    ndisc, \"s\" if ndisc > 1 else \"\")\n\n        self.data = data\n        self.input_data = input_data\n\n        if data is not None:\n            self.annotation_model.set_domain(self.input_data.domain)\n            self.row_side_color_model.set_domain(self.input_data.domain)\n            self.annotation_var = None\n            self.annotation_color_var = None\n            self.row_split_model.set_domain(data.domain)\n            if data.domain.has_discrete_class:\n                self.split_by_var = data.domain.class_var\n            else:\n                self.split_by_var = None\n            self.openContext(self.input_data)\n            if self.split_by_var not in self.row_split_model:\n                self.split_by_var = None\n\n        self.update_heatmaps()\n        if data is not None and self.__pending_selection is not None:\n            assert self.scene.widget is not None\n            self.scene.widget.selectRows(self.__pending_selection)\n            self.selected_rows = self.__pending_selection\n            self.__pending_selection = None\n\n        self.unconditional_commit()\n\n    def _set_input_summary(self, data):\n        summary = len(data) if data else self.info.NoInput\n        details = format_summary_details(data) if data else \"\"\n        self.info.set_input_summary(summary, details)\n\n    def __on_split_rows_activated(self):\n        self.set_split_variable(self.row_split_cb.currentData(Qt.EditRole))\n\n    def set_split_variable(self, var):\n        if var != self.split_by_var:\n            self.split_by_var = var\n            self.update_heatmaps()\n\n    def update_heatmaps(self):\n        if self.data is not None:\n            self.clear_scene()\n            self.clear_messages()\n            if self.col_clustering != Clustering.None_ and \\\n                    len(self.data.domain.attributes) < 2:\n                self.Error.not_enough_features()\n            elif (self.col_clustering != Clustering.None_ or\n                  self.row_clustering != Clustering.None_) and \\\n                    len(self.data) < 2:\n                self.Error.not_enough_instances()\n            elif self.merge_kmeans and len(self.data) < 3:\n                self.Error.not_enough_instances_k_means()\n            else:\n                parts = self.construct_heatmaps(self.data, self.split_by_var)\n                self.construct_heatmaps_scene(parts, self.effective_data)\n                self.selected_rows = []\n        else:\n            self.clear()\n        if self.parts is not None and self.effective_data is not None:\n            nrows = sum(len(p.indices) for p in self.parts.rows)\n            self.Warning.missing_split_values(\n                shown=nrows < len(self.effective_data))\n        else:\n            self.Warning.missing_split_values.clear()\n\n    def update_merge(self):\n        self.kmeans_model = None\n        self.merge_indices = None\n        if self.data is not None and self.merge_kmeans:\n            self.update_heatmaps()\n            self.commit()\n\n    def _make_parts(self, data, group_var=None):\n        \"\"\"\n        Make initial `Parts` for data, split by group_var, group_key\n        \"\"\"\n        if group_var is not None:\n            assert group_var.is_discrete\n            _col_data = table_column_data(data, group_var)\n            row_indices = [np.flatnonzero(_col_data == i)\n                           for i in range(len(group_var.values))]\n            row_groups = [RowPart(title=name, indices=ind,\n                                  cluster=None, cluster_ordered=None)\n                          for name, ind in zip(group_var.values, row_indices)]\n        else:\n            row_groups = [RowPart(title=None, indices=range(0, len(data)),\n                                  cluster=None, cluster_ordered=None)]\n\n        col_groups = [\n            ColumnPart(\n                title=None, indices=range(0, len(data.domain.attributes)),\n                domain=data.domain, cluster=None, cluster_ordered=None)\n        ]\n\n        minv, maxv = np.nanmin(data.X), np.nanmax(data.X)\n        return Parts(row_groups, col_groups, span=(minv, maxv))\n\n    def cluster_rows(self, data: Table, parts: 'Parts', ordered=False) -> 'Parts':\n        row_groups = []\n        for row in parts.rows:\n            if row.cluster is not None:\n                cluster = row.cluster\n            else:\n                cluster = None\n            if row.cluster_ordered is not None:\n                cluster_ord = row.cluster_ordered\n            else:\n                cluster_ord = None\n\n            if row.can_cluster:\n                matrix = None\n                need_dist = cluster is None or (ordered and cluster_ord is None)\n                if need_dist:\n                    subset = data[row.indices]\n                    matrix = Orange.distance.Euclidean(subset)\n\n                if cluster is None:\n                    assert len(matrix) < self.MaxClustering\n                    cluster = hierarchical.dist_matrix_clustering(\n                        matrix, linkage=hierarchical.WARD\n                    )\n                if ordered and cluster_ord is None:\n                    assert len(matrix) < self.MaxOrderedClustering\n                    cluster_ord = hierarchical.optimal_leaf_ordering(\n                        cluster, matrix,\n                    )\n            row_groups.append(row._replace(cluster=cluster, cluster_ordered=cluster_ord))\n\n        return parts._replace(rows=row_groups)\n\n    def cluster_columns(self, data, parts, ordered=False):\n        assert len(parts.columns) == 1, \"columns split is no longer supported\"\n        assert all(var.is_continuous for var in data.domain.attributes)\n\n        col0 = parts.columns[0]\n        if col0.cluster is not None:\n            cluster = col0.cluster\n        else:\n            cluster = None\n        if col0.cluster_ordered is not None:\n            cluster_ord = col0.cluster_ordered\n        else:\n            cluster_ord = None\n        need_dist = cluster is None or (ordered and cluster_ord is None)\n        matrix = None\n        if need_dist:\n            data = Orange.distance._preprocess(data)\n            matrix = np.asarray(Orange.distance.PearsonR(data, axis=0))\n            # nan values break clustering below\n            matrix = np.nan_to_num(matrix)\n\n        if cluster is None:\n            assert matrix is not None\n            assert len(matrix) < self.MaxClustering\n            cluster = hierarchical.dist_matrix_clustering(\n                matrix, linkage=hierarchical.WARD\n            )\n        if ordered and cluster_ord is None:\n            assert len(matrix) < self.MaxOrderedClustering\n            cluster_ord = hierarchical.optimal_leaf_ordering(cluster, matrix)\n\n        col_groups = [col._replace(cluster=cluster, cluster_ordered=cluster_ord)\n                      for col in parts.columns]\n        return parts._replace(columns=col_groups)\n\n    def construct_heatmaps(self, data, group_var=None) -> 'Parts':\n        if self.merge_kmeans:\n            if self.kmeans_model is None:\n                effective_data = self.input_data.transform(\n                    Orange.data.Domain(\n                        [var for var in self.input_data.domain.attributes\n                         if var.is_continuous],\n                        self.input_data.domain.class_vars,\n                        self.input_data.domain.metas))\n                nclust = min(self.merge_kmeans_k, len(effective_data) - 1)\n                self.kmeans_model = kmeans_compress(effective_data, k=nclust)\n                effective_data.domain = self.kmeans_model.domain\n                merge_indices = [np.flatnonzero(self.kmeans_model.labels == ind)\n                                 for ind in range(nclust)]\n                not_empty_indices = [i for i, x in enumerate(merge_indices)\n                                     if len(x) > 0]\n                self.merge_indices = \\\n                    [merge_indices[i] for i in not_empty_indices]\n                if len(merge_indices) != len(self.merge_indices):\n                    self.Warning.empty_clusters()\n                effective_data = Orange.data.Table(\n                    Orange.data.Domain(effective_data.domain.attributes),\n                    self.kmeans_model.centroids[not_empty_indices]\n                )\n            else:\n                effective_data = self.effective_data\n\n            group_var = None\n        else:\n            self.kmeans_model = None\n            self.merge_indices = None\n            effective_data = data\n\n        self.effective_data = effective_data\n\n        self.__update_clustering_enable_state(effective_data)\n\n        parts = self._make_parts(effective_data, group_var)\n        # Restore/update the row/columns items descriptions from cache if\n        # available\n        rows_cache_key = (group_var,\n                          self.merge_kmeans_k if self.merge_kmeans else None)\n        if rows_cache_key in self.__rows_cache:\n            parts = parts._replace(rows=self.__rows_cache[rows_cache_key].rows)\n\n        if self.row_clustering != Clustering.None_:\n            parts = self.cluster_rows(\n                effective_data, parts,\n                ordered=self.row_clustering == Clustering.OrderedClustering\n            )\n        if self.col_clustering != Clustering.None_:\n            parts = self.cluster_columns(\n                effective_data, parts,\n                ordered=self.col_clustering == Clustering.OrderedClustering\n            )\n\n        # Cache the updated parts\n        self.__rows_cache[rows_cache_key] = parts\n        return parts\n\n    def construct_heatmaps_scene(self, parts: 'Parts', data: Table) -> None:\n        _T = TypeVar(\"_T\", bound=Union[RowPart, ColumnPart])\n\n        def select_cluster(clustering: Clustering, item: _T) -> _T:\n            if clustering == Clustering.None_:\n                return item._replace(cluster=None, cluster_ordered=None)\n            elif clustering == Clustering.Clustering:\n                return item._replace(cluster=item.cluster, cluster_ordered=None)\n            elif clustering == Clustering.OrderedClustering:\n                return item._replace(cluster=item.cluster_ordered, cluster_ordered=None)\n            else:  # pragma: no cover\n                raise TypeError()\n\n        rows = [select_cluster(self.row_clustering, rowitem)\n                for rowitem in parts.rows]\n        cols = [select_cluster(self.col_clustering, colitem)\n                for colitem in parts.columns]\n        parts = Parts(columns=cols, rows=rows, span=parts.span)\n\n        self.setup_scene(parts, data)\n\n    def setup_scene(self, parts, data):\n        # type: (Parts, Table) -> None\n        widget = HeatmapGridWidget()\n        widget.setColorMap(self.color_map())\n        self.scene.addItem(widget)\n        self.scene.widget = widget\n        columns = [v.name for v in data.domain.attributes]\n        parts = HeatmapGridWidget.Parts(\n            rows=[\n                HeatmapGridWidget.RowItem(r.title, r.indices, r.cluster)\n                for r in parts.rows\n            ],\n            columns=[\n                HeatmapGridWidget.ColumnItem(c.title, c.indices, c.cluster)\n                for c in parts.columns\n            ],\n            data=data.X,\n            span=parts.span,\n            row_names=None,\n            col_names=columns,\n        )\n        widget.setHeatmaps(parts)\n        side = self.row_side_colors()\n        if side is not None:\n            widget.setRowSideColorAnnotations(side[0], side[1], name=side[2].name)\n        widget.setColumnLabelsPosition(self._column_label_pos)\n        widget.setAspectRatioMode(\n            Qt.KeepAspectRatio if self.keep_aspect else Qt.IgnoreAspectRatio\n        )\n        widget.setShowAverages(self.averages)\n        widget.setLegendVisible(self.legend)\n\n        widget.layoutDidActivate.connect(self.__on_layout_activate)\n        widget.selectionFinished.connect(self.on_selection_finished)\n\n        self.update_annotations()\n        self.view.setCentralWidget(widget)\n        self.parts = parts\n\n    def __update_scene_rects(self):\n        widget = self.scene.widget\n        if widget is None:\n            return\n        rect = widget.geometry()\n        self.scene.setSceneRect(rect)\n        self.view.setSceneRect(rect)\n        self.view.setHeaderSceneRect(widget.headerGeometry())\n        self.view.setFooterSceneRect(widget.footerGeometry())\n\n    def __on_layout_activate(self):\n        self.__update_scene_rects()\n\n    def __aspect_mode_changed(self):\n        widget = self.scene.widget\n        if widget is None:\n            return\n        widget.setAspectRatioMode(\n            Qt.KeepAspectRatio if self.keep_aspect else Qt.IgnoreAspectRatio\n        )\n        # when aspect fixed the vertical sh is fixex, when not, it can\n        # shrink vertically\n        sp = widget.sizePolicy()\n        if self.keep_aspect:\n            sp.setVerticalPolicy(QSizePolicy.Fixed)\n        else:\n            sp.setVerticalPolicy(QSizePolicy.Preferred)\n        widget.setSizePolicy(sp)\n\n    def __update_clustering_enable_state(self, data):\n        if data is not None:\n            N = len(data)\n            M = len(data.domain.attributes)\n        else:\n            N = M = 0\n\n        rc_enabled = N <= self.MaxClustering\n        rco_enabled = N <= self.MaxOrderedClustering\n        cc_enabled = M <= self.MaxClustering\n        cco_enabled = M <= self.MaxOrderedClustering\n        row_clust, col_clust = self.row_clustering, self.col_clustering\n\n        row_clust_msg = \"\"\n        col_clust_msg = \"\"\n\n        if not rco_enabled and row_clust == Clustering.OrderedClustering:\n            row_clust = Clustering.Clustering\n            row_clust_msg = \"Row cluster ordering was disabled due to the \" \\\n                            \"input matrix being to big\"\n        if not rc_enabled and row_clust == Clustering.Clustering:\n            row_clust = Clustering.None_\n            row_clust_msg = \"Row clustering was was disabled due to the \" \\\n                            \"input matrix being to big\"\n\n        if not cco_enabled and col_clust == Clustering.OrderedClustering:\n            col_clust = Clustering.Clustering\n            col_clust_msg = \"Column cluster ordering was disabled due to \" \\\n                            \"the input matrix being to big\"\n        if not cc_enabled and col_clust == Clustering.Clustering:\n            col_clust = Clustering.None_\n            col_clust_msg = \"Column clustering was disabled due to the \" \\\n                            \"input matrix being to big\"\n\n        self.col_clustering = col_clust\n        self.row_clustering = row_clust\n\n        self.Information.row_clust(row_clust_msg, shown=bool(row_clust_msg))\n        self.Information.col_clust(col_clust_msg, shown=bool(col_clust_msg))\n\n        # Disable/enable the combobox items for the clustering methods\n        def setenabled(cb: QComboBox, clu: bool, clu_op: bool):\n            model = cb.model()\n            assert isinstance(model, QStandardItemModel)\n            idx = cb.findData(Clustering.OrderedClustering, ClusteringRole)\n            assert idx != -1\n            model.item(idx).setEnabled(clu_op)\n            idx = cb.findData(Clustering.Clustering, ClusteringRole)\n            assert idx != -1\n            model.item(idx).setEnabled(clu)\n\n        setenabled(self.row_cluster_cb, rc_enabled, rco_enabled)\n        setenabled(self.col_cluster_cb, cc_enabled, cco_enabled)\n\n    def update_averages_stripe(self):\n        \"\"\"Update the visibility of the averages stripe.\n        \"\"\"\n        widget = self.scene.widget\n        if widget is not None:\n            widget.setShowAverages(self.averages)\n\n    def update_lowslider(self):\n        low, high = self.controls.threshold_low, self.controls.threshold_high\n        if low.value() >= high.value():\n            low.setSliderPosition(high.value() - 1)\n        self.update_color_schema()\n\n    def update_highslider(self):\n        low, high = self.controls.threshold_low, self.controls.threshold_high\n        if low.value() >= high.value():\n            high.setSliderPosition(low.value() + 1)\n        self.update_color_schema()\n\n    def update_color_schema(self):\n        self.palette_name = self.color_cb.currentData().name\n        w = self.scene.widget\n        if w is not None:\n            w.setColorMap(self.color_map())\n\n    def __update_column_clustering(self):\n        self.update_heatmaps()\n        self.commit()\n\n    def __update_row_clustering(self):\n        self.update_heatmaps()\n        self.commit()\n\n    def update_legend(self):\n        widget = self.scene.widget\n        if widget is not None:\n            widget.setLegendVisible(self.legend)\n\n    def row_annotation_var(self):\n        return self.annotation_var\n\n    def row_annotation_data(self):\n        var = self.row_annotation_var()\n        if var is None:\n            return None\n        return column_str_from_table(self.input_data, var)\n\n    def _merge_row_indices(self):\n        if self.merge_kmeans and self.kmeans_model is not None:\n            return self.merge_indices\n        else:\n            return None\n\n    def set_annotation_var(self, var: Union[None, Variable, int]):\n        if isinstance(var, int):\n            var = self.annotation_model[var]\n        if self.annotation_var != var:\n            self.annotation_var = var\n            self.update_annotations()\n\n    def update_annotations(self):\n        widget = self.scene.widget\n        if widget is not None:\n            annot_col = self.row_annotation_data()\n            merge_indices = self._merge_row_indices()\n            if merge_indices is not None and annot_col is not None:\n                join = lambda _1: join_elided(\", \", 42, _1, \" ({} more)\")\n                annot_col = aggregate_apply(join, annot_col, merge_indices)\n            if annot_col is not None:\n                widget.setRowLabels(annot_col)\n                widget.setRowLabelsVisible(True)\n            else:\n                widget.setRowLabelsVisible(False)\n                widget.setRowLabels(None)\n\n    def row_side_colors(self):\n        var = self.annotation_color_var\n        if var is None:\n            return None\n        column_data = column_data_from_table(self.input_data, var)\n        span = (np.nanmin(column_data), np.nanmax(column_data))\n        merges = self._merge_row_indices()\n        if merges is not None:\n            column_data = aggregate(var, column_data, merges)\n        data, colormap = self._colorize(var, column_data)\n        if var.is_continuous:\n            colormap.span = span\n        return data, colormap, var\n\n    def set_annotation_color_var(self, var: Union[None, Variable, int]):\n        \"\"\"Set the current side color annotation variable.\"\"\"\n        if isinstance(var, int):\n            var = self.row_side_color_model[var]\n        if self.annotation_color_var != var:\n            self.annotation_color_var = var\n            self.update_row_side_colors()\n\n    def update_row_side_colors(self):\n        widget = self.scene.widget\n        if widget is None:\n            return\n        colors = self.row_side_colors()\n        if colors is None:\n            widget.setRowSideColorAnnotations(None)\n        else:\n            widget.setRowSideColorAnnotations(colors[0], colors[1], colors[2].name)\n\n    def _colorize(self, var: Variable, data: np.ndarray) -> Tuple[np.ndarray, ColorMap]:\n        palette = var.palette  # type: Palette\n        colors = np.array(\n            [[c.red(), c.green(), c.blue()] for c in palette.qcolors_w_nan],\n            dtype=np.uint8,\n        )\n        if var.is_discrete:\n            mask = np.isnan(data)\n            data[mask] = -1\n            data = data.astype(int)\n            if mask.any():\n                values = (*var.values, \"N/A\")\n            else:\n                values = var.values\n                colors = colors[: -1]\n            return data, CategoricalColorMap(colors, values)\n        elif var.is_continuous:\n            cmap = GradientColorMap(colors[:-1])\n            return data, cmap\n        else:\n            raise TypeError\n\n    def update_column_annotations(self):\n        widget = self.scene.widget\n        if self.data is not None and widget is not None:\n            widget.setColumnLabelsPosition(self._column_label_pos)\n\n    def __adjust_font_size(self, diff):\n        widget = self.scene.widget\n        if widget is None:\n            return\n        curr = widget.font().pointSizeF()\n        new = curr + diff\n\n        self.__font_dec.setEnabled(new > 1.0)\n        self.__font_inc.setEnabled(new <= 32)\n        if new > 1.0:\n            font = QFont()\n            font.setPointSizeF(new)\n            widget.setFont(font)\n\n    def _on_view_context_menu(self, pos):\n        widget = self.scene.widget\n        if widget is None:\n            return\n        assert isinstance(widget, HeatmapGridWidget)\n        menu = QMenu(self.view.viewport())\n        menu.setAttribute(Qt.WA_DeleteOnClose)\n        menu.addActions(self.view.actions())\n        menu.addSeparator()\n        menu.addActions([self.__font_inc, self.__font_dec])\n        menu.addSeparator()\n        a = QAction(\"Keep aspect ratio\", menu, checkable=True)\n        a.setChecked(self.keep_aspect)\n\n        def ontoggled(state):\n            self.keep_aspect = state\n            self.__aspect_mode_changed()\n        a.toggled.connect(ontoggled)\n        menu.addAction(a)\n        menu.popup(self.view.viewport().mapToGlobal(pos))\n\n    def on_selection_finished(self):\n        if self.scene.widget is not None:\n            self.selected_rows = list(self.scene.widget.selectedRows())\n        else:\n            self.selected_rows = []\n        self.commit()\n\n    def commit(self):\n        data = None\n        indices = None\n        if self.merge_kmeans:\n            merge_indices = self.merge_indices\n        else:\n            merge_indices = None\n\n        if self.input_data is not None and self.selected_rows:\n            indices = self.selected_rows\n            if merge_indices is not None:\n                # expand merged indices\n                indices = np.hstack([merge_indices[i] for i in indices])\n\n            data = self.input_data[indices]\n\n        summary = len(data) if data else self.info.NoOutput\n        details = format_summary_details(data) if data else \"\"\n        self.info.set_output_summary(summary, details)\n        self.Outputs.selected_data.send(data)\n        self.Outputs.annotated_data.send(create_annotated_table(self.input_data, indices))\n\n    def onDeleteWidget(self):\n        self.clear()\n        super().onDeleteWidget()\n\n    def send_report(self):\n        self.report_items((\n            (\"Columns:\", \"Clustering\" if self.col_clustering else \"No sorting\"),\n            (\"Rows:\", \"Clustering\" if self.row_clustering else \"No sorting\"),\n            (\"Split:\",\n             self.split_by_var is not None and self.split_by_var.name),\n            (\"Row annotation\",\n             self.annotation_var is not None and self.annotation_var.name),\n        ))\n        self.report_plot()\n\n    @classmethod\n    def migrate_settings(cls, settings, version):\n        if version is not None and version < 3:\n            def st2cl(state: bool) -> Clustering:\n                return Clustering.OrderedClustering if state else \\\n                    Clustering.None_\n\n            rc = settings.pop(\"row_clustering\", False)\n            cc = settings.pop(\"col_clustering\", False)\n            settings[\"row_clustering_method\"] = st2cl(rc).name\n            settings[\"col_clustering_method\"] = st2cl(cc).name",
                            "ast_type_str": "ClassDef",
                            "start_lineno": 132,
                            "end_lineno": 1138,
                            "parent": null
                        }
                    ],
                    "added_entities": [],
                    "deleted_entities": []
                },
                {
                    "descriptor": {
                        "old_range": {
                            "start": 625,
                            "length": 9
                        },
                        "new_range": {
                            "start": 617,
                            "length": 15
                        },
                        "section": "class OWHeatMap(widget.OWWidget):"
                    },
                    "line_group": {
                        "all_lines": [
                            {
                                "content": "            _col_data = table_column_data(data, group_var)",
                                "type": "context"
                            },
                            {
                                "content": "            row_indices = [np.flatnonzero(_col_data == i)",
                                "type": "context"
                            },
                            {
                                "content": "                           for i in range(len(group_var.values))]",
                                "type": "context"
                            },
                            {
                                "content": "",
                                "type": "added"
                            },
                            {
                                "content": "            row_groups = [RowPart(title=name, indices=ind,",
                                "type": "context"
                            },
                            {
                                "content": "                                  cluster=None, cluster_ordered=None)",
                                "type": "context"
                            },
                            {
                                "content": "                          for name, ind in zip(group_var.values, row_indices)]",
                                "type": "context"
                            },
                            {
                                "content": "            if np.any(_col_data.mask):",
                                "type": "added"
                            },
                            {
                                "content": "                row_groups.append(RowPart(",
                                "type": "added"
                            },
                            {
                                "content": "                    title=\"N/A\", indices=np.flatnonzero(_col_data.mask),",
                                "type": "added"
                            },
                            {
                                "content": "                    cluster=None, cluster_ordered=None",
                                "type": "added"
                            },
                            {
                                "content": "                ))",
                                "type": "added"
                            },
                            {
                                "content": "        else:",
                                "type": "context"
                            },
                            {
                                "content": "            row_groups = [RowPart(title=None, indices=range(0, len(data)),",
                                "type": "context"
                            },
                            {
                                "content": "                                  cluster=None, cluster_ordered=None)]",
                                "type": "context"
                            }
                        ]
                    },
                    "modified_entities": [
                        {
                            "file_name": "Orange/widgets/visualize/owheatmap.py",
                            "type": "method",
                            "name": "OWHeatMap._make_parts",
                            "content": "class OWHeatMap(widget.OWWidget):\n    name = \"Heat Map\"\n    description = \"Plot a data matrix heatmap.\"\n    icon = \"icons/Heatmap.svg\"\n    priority = 260\n    keywords = []\n\n    class Inputs:\n        data = Input(\"Data\", Table)\n\n    class Outputs:\n        selected_data = Output(\"Selected Data\", Table, default=True)\n        annotated_data = Output(ANNOTATED_DATA_SIGNAL_NAME, Table)\n\n    settings_version = 3\n\n    settingsHandler = settings.DomainContextHandler()\n\n    # Disable clustering for inputs bigger than this\n    MaxClustering = 25000\n    # Disable cluster leaf ordering for inputs bigger than this\n    MaxOrderedClustering = 1000\n\n    threshold_low = settings.Setting(0.0)\n    threshold_high = settings.Setting(1.0)\n\n    merge_kmeans = settings.Setting(False)\n    merge_kmeans_k = settings.Setting(50)\n\n    # Display column with averages\n    averages: bool = settings.Setting(True)\n    # Display legend\n    legend: bool = settings.Setting(True)\n    # Annotations\n    #: text row annotation (row names)\n    annotation_var = settings.ContextSetting(None)\n    #: color row annotation\n    annotation_color_var = settings.ContextSetting(None)\n    # Discrete variable used to split that data/heatmaps (vertically)\n    split_by_var = settings.ContextSetting(None)\n    # Selected row/column clustering method (name)\n    col_clustering_method: str = settings.Setting(Clustering.None_.name)\n    row_clustering_method: str = settings.Setting(Clustering.None_.name)\n\n    palette_name = settings.Setting(colorpalettes.DefaultContinuousPaletteName)\n    column_label_pos: int = settings.Setting(1)\n    selected_rows: List[int] = settings.Setting(None, schema_only=True)\n\n    auto_commit = settings.Setting(True)\n\n    graph_name = \"scene\"\n\n    left_side_scrolling = True\n\n    class Information(widget.OWWidget.Information):\n        sampled = Msg(\"Data has been sampled\")\n        discrete_ignored = Msg(\"{} categorical feature{} ignored\")\n        row_clust = Msg(\"{}\")\n        col_clust = Msg(\"{}\")\n        sparse_densified = Msg(\"Showing this data may require a lot of memory\")\n\n    class Error(widget.OWWidget.Error):\n        no_continuous = Msg(\"No numeric features\")\n        not_enough_features = Msg(\"Not enough features for column clustering\")\n        not_enough_instances = Msg(\"Not enough instances for clustering\")\n        not_enough_instances_k_means = Msg(\n            \"Not enough instances for k-means merging\")\n        not_enough_memory = Msg(\"Not enough memory to show this data\")\n\n    class Warning(widget.OWWidget.Warning):\n        empty_clusters = Msg(\"Empty clusters were removed\")\n\n    def __init__(self):\n        super().__init__()\n        self.__pending_selection = self.selected_rows\n\n        # A kingdom for a save_state/restore_state\n        self.col_clustering = enum_get(\n            Clustering, self.col_clustering_method, Clustering.None_)\n        self.row_clustering = enum_get(\n            Clustering, self.row_clustering_method, Clustering.None_)\n\n        @self.settingsAboutToBePacked.connect\n        def _():\n            self.col_clustering_method = self.col_clustering.name\n            self.row_clustering_method = self.row_clustering.name\n\n        self.keep_aspect = False\n\n        #: The original data with all features (retained to\n        #: preserve the domain on the output)\n        self.input_data = None\n        #: The effective data striped of discrete features, and often\n        #: merged using k-means\n        self.data = None\n        self.effective_data = None\n        #: kmeans model used to merge rows of input_data\n        self.kmeans_model = None\n        #: merge indices derived from kmeans\n        #: a list (len==k) of int ndarray where the i-th item contains\n        #: the indices which merge the input_data into the heatmap row i\n        self.merge_indices = None\n        self.parts: Optional[Parts] = None\n        self.__rows_cache = {}\n        self.__columns_cache = {}\n\n        self.info.set_input_summary(self.info.NoInput)\n        self.info.set_output_summary(self.info.NoOutput)\n\n        # GUI definition\n        colorbox = gui.vBox(self.controlArea, \"Color\")\n        self.color_cb = gui.palette_combo_box(self.palette_name)\n        self.color_cb.currentIndexChanged.connect(self.update_color_schema)\n        colorbox.layout().addWidget(self.color_cb)\n\n        form = QFormLayout(\n            formAlignment=Qt.AlignLeft,\n            labelAlignment=Qt.AlignLeft,\n            fieldGrowthPolicy=QFormLayout.AllNonFixedFieldsGrow\n        )\n        lowslider = gui.hSlider(\n            colorbox, self, \"threshold_low\", minValue=0.0, maxValue=1.0,\n            step=0.05, ticks=True, intOnly=False,\n            createLabel=False, callback=self.update_lowslider)\n        highslider = gui.hSlider(\n            colorbox, self, \"threshold_high\", minValue=0.0, maxValue=1.0,\n            step=0.05, ticks=True, intOnly=False,\n            createLabel=False, callback=self.update_highslider)\n\n        form.addRow(\"Low:\", lowslider)\n        form.addRow(\"High:\", highslider)\n\n        colorbox.layout().addLayout(form)\n\n        mergebox = gui.vBox(self.controlArea, \"Merge\",)\n        gui.checkBox(mergebox, self, \"merge_kmeans\", \"Merge by k-means\",\n                     callback=self.__update_row_clustering)\n        ibox = gui.indentedBox(mergebox)\n        gui.spin(ibox, self, \"merge_kmeans_k\", minv=5, maxv=500,\n                 label=\"Clusters:\", keyboardTracking=False,\n                 callbackOnReturn=True, callback=self.update_merge)\n\n        cluster_box = gui.vBox(self.controlArea, \"Clustering\")\n        # Row clustering\n        self.row_cluster_cb = cb = ComboBox()\n        cb.setModel(create_list_model(ClusteringModelData, self))\n        cbselect(cb, self.row_clustering, ClusteringRole)\n        self.connect_control(\n            \"row_clustering\",\n            lambda value, cb=cb: cbselect(cb, value, ClusteringRole)\n        )\n        @cb.activated.connect\n        def _(idx, cb=cb):\n            self.set_row_clustering(cb.itemData(idx, ClusteringRole))\n\n        # Column clustering\n        self.col_cluster_cb = cb = ComboBox()\n        cb.setModel(create_list_model(ClusteringModelData, self))\n        cbselect(cb, self.col_clustering, ClusteringRole)\n        self.connect_control(\n            \"col_clustering\",\n            lambda value, cb=cb: cbselect(cb, value, ClusteringRole)\n        )\n        @cb.activated.connect\n        def _(idx, cb=cb):\n            self.set_col_clustering(cb.itemData(idx, ClusteringRole))\n\n        form = QFormLayout(\n            labelAlignment=Qt.AlignLeft, formAlignment=Qt.AlignLeft,\n            fieldGrowthPolicy=QFormLayout.AllNonFixedFieldsGrow,\n        )\n        form.addRow(\"Rows:\", self.row_cluster_cb)\n        form.addRow(\"Columns:\", self.col_cluster_cb)\n        cluster_box.layout().addLayout(form)\n        box = gui.vBox(self.controlArea, \"Split By\")\n\n        self.row_split_model = DomainModel(\n            placeholder=\"(None)\",\n            valid_types=(Orange.data.DiscreteVariable,),\n            parent=self,\n        )\n        self.row_split_cb = cb = ComboBoxSearch(\n            enabled=not self.merge_kmeans,\n            sizeAdjustPolicy=ComboBox.AdjustToMinimumContentsLengthWithIcon,\n            minimumContentsLength=14,\n            toolTip=\"Split the heatmap vertically by a categorical column\"\n        )\n        self.row_split_cb.setModel(self.row_split_model)\n        self.connect_control(\n            \"split_by_var\", lambda value, cb=cb: cbselect(cb, value)\n        )\n        self.connect_control(\n            \"merge_kmeans\", self.row_split_cb.setDisabled\n        )\n        self.split_by_var = None\n\n        self.row_split_cb.activated.connect(\n            self.__on_split_rows_activated\n        )\n        box.layout().addWidget(self.row_split_cb)\n\n        box = gui.vBox(self.controlArea, 'Annotation && Legends')\n\n        gui.checkBox(box, self, 'legend', 'Show legend',\n                     callback=self.update_legend)\n\n        gui.checkBox(box, self, 'averages', 'Stripes with averages',\n                     callback=self.update_averages_stripe)\n        annotbox = QGroupBox(\"Row Annotations\", flat=True)\n        form = QFormLayout(\n            annotbox,\n            formAlignment=Qt.AlignLeft,\n            labelAlignment=Qt.AlignLeft,\n            fieldGrowthPolicy=QFormLayout.AllNonFixedFieldsGrow\n        )\n        self.annotation_model = DomainModel(placeholder=\"(None)\")\n        self.annotation_text_cb = ComboBoxSearch(\n            minimumContentsLength=12,\n            sizeAdjustPolicy=QComboBox.AdjustToMinimumContentsLength\n        )\n        self.annotation_text_cb.setModel(self.annotation_model)\n        self.annotation_text_cb.activated.connect(self.set_annotation_var)\n        self.connect_control(\"annotation_var\", self.annotation_var_changed)\n\n        self.row_side_color_model = DomainModel(\n            order=(DomainModel.CLASSES, DomainModel.Separator,\n                   DomainModel.METAS),\n            placeholder=\"(None)\", valid_types=DomainModel.PRIMITIVE,\n            flags=Qt.ItemIsSelectable | Qt.ItemIsEnabled,\n            parent=self,\n        )\n        self.row_side_color_cb = ComboBoxSearch(\n            sizeAdjustPolicy=QComboBox.AdjustToMinimumContentsLength,\n            minimumContentsLength=12\n        )\n        self.row_side_color_cb.setModel(self.row_side_color_model)\n        self.row_side_color_cb.activated.connect(self.set_annotation_color_var)\n        self.connect_control(\"annotation_color_var\", self.annotation_color_var_changed)\n        form.addRow(\"Text\", self.annotation_text_cb)\n        form.addRow(\"Color\", self.row_side_color_cb)\n        box.layout().addWidget(annotbox)\n        posbox = gui.vBox(box, \"Column Labels Position\", addSpace=False)\n        posbox.setFlat(True)\n        cb = gui.comboBox(\n            posbox, self, \"column_label_pos\",\n            callback=self.update_column_annotations)\n        cb.setModel(create_list_model(ColumnLabelsPosData, parent=self))\n        cb.setCurrentIndex(self.column_label_pos)\n        gui.checkBox(self.controlArea, self, \"keep_aspect\",\n                     \"Keep aspect ratio\", box=\"Resize\",\n                     callback=self.__aspect_mode_changed)\n\n        gui.rubber(self.controlArea)\n        gui.auto_send(self.controlArea, self, \"auto_commit\")\n\n        # Scene with heatmap\n        class HeatmapScene(QGraphicsScene):\n            widget: Optional[HeatmapGridWidget] = None\n\n        self.scene = self.scene = HeatmapScene(parent=self)\n        self.view = GraphicsView(\n            self.scene,\n            verticalScrollBarPolicy=Qt.ScrollBarAlwaysOn,\n            horizontalScrollBarPolicy=Qt.ScrollBarAlwaysOn,\n            viewportUpdateMode=QGraphicsView.FullViewportUpdate,\n            widgetResizable=True,\n        )\n        self.view.setContextMenuPolicy(Qt.CustomContextMenu)\n        self.view.customContextMenuRequested.connect(\n            self._on_view_context_menu\n        )\n        self.mainArea.layout().addWidget(self.view)\n        self.selected_rows = []\n        self.__font_inc = QAction(\n            \"Increase Font\", self, shortcut=QKeySequence(\"ctrl+>\"))\n        self.__font_dec = QAction(\n            \"Decrease Font\", self, shortcut=QKeySequence(\"ctrl+<\"))\n        self.__font_inc.triggered.connect(lambda: self.__adjust_font_size(1))\n        self.__font_dec.triggered.connect(lambda: self.__adjust_font_size(-1))\n        if hasattr(QAction, \"setShortcutVisibleInContextMenu\"):\n            apply_all(\n                [self.__font_inc, self.__font_dec],\n                lambda a: a.setShortcutVisibleInContextMenu(True)\n            )\n        self.addActions([self.__font_inc, self.__font_dec])\n\n    @property\n    def center_palette(self):\n        palette = self.color_cb.currentData()\n        return bool(palette.flags & palette.Diverging)\n\n    @property\n    def _column_label_pos(self) -> HeatmapGridWidget.Position:\n        return ColumnLabelsPosData[self.column_label_pos][Qt.UserRole]\n\n    def annotation_color_var_changed(self, value):\n        cbselect(self.row_side_color_cb, value, Qt.EditRole)\n\n    def annotation_var_changed(self, value):\n        cbselect(self.annotation_text_cb, value, Qt.EditRole)\n\n    def set_row_clustering(self, method: Clustering) -> None:\n        assert isinstance(method, Clustering)\n        if self.row_clustering != method:\n            self.row_clustering = method\n            cbselect(self.row_cluster_cb, method, ClusteringRole)\n            self.__update_row_clustering()\n\n    def set_col_clustering(self, method: Clustering) -> None:\n        assert isinstance(method, Clustering)\n        if self.col_clustering != method:\n            self.col_clustering = method\n            cbselect(self.col_cluster_cb, method, ClusteringRole)\n            self.__update_column_clustering()\n\n    def sizeHint(self) -> QSize:\n        return super().sizeHint().expandedTo(QSize(900, 700))\n\n    def color_palette(self):\n        return self.color_cb.currentData().lookup_table()\n\n    def color_map(self) -> GradientColorMap:\n        return GradientColorMap(\n            self.color_palette(), (self.threshold_low, self.threshold_high),\n            0 if self.center_palette else None\n        )\n\n    def clear(self):\n        self.data = None\n        self.input_data = None\n        self.effective_data = None\n        self.kmeans_model = None\n        self.merge_indices = None\n        self.annotation_model.set_domain(None)\n        self.annotation_var = None\n        self.row_side_color_model.set_domain(None)\n        self.annotation_color_var = None\n        self.row_split_model.set_domain(None)\n        self.split_by_var = None\n        self.parts = None\n        self.clear_scene()\n        self.selected_rows = []\n        self.__columns_cache.clear()\n        self.__rows_cache.clear()\n        self.__update_clustering_enable_state(None)\n\n    def clear_scene(self):\n        if self.scene.widget is not None:\n            self.scene.widget.layoutDidActivate.disconnect(\n                self.__on_layout_activate\n            )\n            self.scene.widget.selectionFinished.disconnect(\n                self.on_selection_finished\n            )\n        self.scene.widget = None\n        self.scene.clear()\n\n        self.view.setSceneRect(QRectF())\n        self.view.setHeaderSceneRect(QRectF())\n        self.view.setFooterSceneRect(QRectF())\n\n    @Inputs.data\n    def set_dataset(self, data=None):\n        \"\"\"Set the input dataset to display.\"\"\"\n        self.closeContext()\n        self.clear()\n        self.clear_messages()\n        self._set_input_summary(data)\n\n        if isinstance(data, SqlTable):\n            if data.approx_len() < 4000:\n                data = Table(data)\n            else:\n                self.Information.sampled()\n                data_sample = data.sample_time(1, no_cache=True)\n                data_sample.download_data(2000, partial=True)\n                data = Table(data_sample)\n\n        if data is not None and not len(data):\n            data = None\n\n        if data is not None and sp.issparse(data.X):\n            try:\n                data = data.to_dense()\n            except MemoryError:\n                data = None\n                self.Error.not_enough_memory()\n            else:\n                self.Information.sparse_densified()\n\n        input_data = data\n\n        # Data contains no attributes or meta attributes only\n        if data is not None and len(data.domain.attributes) == 0:\n            self.Error.no_continuous()\n            input_data = data = None\n\n        # Data contains some discrete attributes which must be filtered\n        if data is not None and \\\n                any(var.is_discrete for var in data.domain.attributes):\n            ndisc = sum(var.is_discrete for var in data.domain.attributes)\n            data = data.transform(\n                Domain([var for var in data.domain.attributes\n                        if var.is_continuous],\n                       data.domain.class_vars,\n                       data.domain.metas))\n            if not data.domain.attributes:\n                self.Error.no_continuous()\n                input_data = data = None\n            else:\n                self.Information.discrete_ignored(\n                    ndisc, \"s\" if ndisc > 1 else \"\")\n\n        self.data = data\n        self.input_data = input_data\n\n        if data is not None:\n            self.annotation_model.set_domain(self.input_data.domain)\n            self.row_side_color_model.set_domain(self.input_data.domain)\n            self.annotation_var = None\n            self.annotation_color_var = None\n            self.row_split_model.set_domain(data.domain)\n            if data.domain.has_discrete_class:\n                self.split_by_var = data.domain.class_var\n            else:\n                self.split_by_var = None\n            self.openContext(self.input_data)\n            if self.split_by_var not in self.row_split_model:\n                self.split_by_var = None\n\n        self.update_heatmaps()\n        if data is not None and self.__pending_selection is not None:\n            assert self.scene.widget is not None\n            self.scene.widget.selectRows(self.__pending_selection)\n            self.selected_rows = self.__pending_selection\n            self.__pending_selection = None\n\n        self.unconditional_commit()\n\n    def _set_input_summary(self, data):\n        summary = len(data) if data else self.info.NoInput\n        details = format_summary_details(data) if data else \"\"\n        self.info.set_input_summary(summary, details)\n\n    def __on_split_rows_activated(self):\n        self.set_split_variable(self.row_split_cb.currentData(Qt.EditRole))\n\n    def set_split_variable(self, var):\n        if var != self.split_by_var:\n            self.split_by_var = var\n            self.update_heatmaps()\n\n    def update_heatmaps(self):\n        if self.data is not None:\n            self.clear_scene()\n            self.clear_messages()\n            if self.col_clustering != Clustering.None_ and \\\n                    len(self.data.domain.attributes) < 2:\n                self.Error.not_enough_features()\n            elif (self.col_clustering != Clustering.None_ or\n                  self.row_clustering != Clustering.None_) and \\\n                    len(self.data) < 2:\n                self.Error.not_enough_instances()\n            elif self.merge_kmeans and len(self.data) < 3:\n                self.Error.not_enough_instances_k_means()\n            else:\n                parts = self.construct_heatmaps(self.data, self.split_by_var)\n                self.construct_heatmaps_scene(parts, self.effective_data)\n                self.selected_rows = []\n        else:\n            self.clear()\n\n    def update_merge(self):\n        self.kmeans_model = None\n        self.merge_indices = None\n        if self.data is not None and self.merge_kmeans:\n            self.update_heatmaps()\n            self.commit()\n\n    def _make_parts(self, data, group_var=None):\n        \"\"\"\n        Make initial `Parts` for data, split by group_var, group_key\n        \"\"\"\n        if group_var is not None:\n            assert group_var.is_discrete\n            _col_data = table_column_data(data, group_var)\n            row_indices = [np.flatnonzero(_col_data == i)\n                           for i in range(len(group_var.values))]\n\n            row_groups = [RowPart(title=name, indices=ind,\n                                  cluster=None, cluster_ordered=None)\n                          for name, ind in zip(group_var.values, row_indices)]\n            if np.any(_col_data.mask):\n                row_groups.append(RowPart(\n                    title=\"N/A\", indices=np.flatnonzero(_col_data.mask),\n                    cluster=None, cluster_ordered=None\n                ))\n        else:\n            row_groups = [RowPart(title=None, indices=range(0, len(data)),\n                                  cluster=None, cluster_ordered=None)]\n\n        col_groups = [\n            ColumnPart(\n                title=None, indices=range(0, len(data.domain.attributes)),\n                domain=data.domain, cluster=None, cluster_ordered=None)\n        ]\n\n        minv, maxv = np.nanmin(data.X), np.nanmax(data.X)\n        return Parts(row_groups, col_groups, span=(minv, maxv))\n\n    def cluster_rows(self, data: Table, parts: 'Parts', ordered=False) -> 'Parts':\n        row_groups = []\n        for row in parts.rows:\n            if row.cluster is not None:\n                cluster = row.cluster\n            else:\n                cluster = None\n            if row.cluster_ordered is not None:\n                cluster_ord = row.cluster_ordered\n            else:\n                cluster_ord = None\n\n            if row.can_cluster:\n                matrix = None\n                need_dist = cluster is None or (ordered and cluster_ord is None)\n                if need_dist:\n                    subset = data[row.indices]\n                    matrix = Orange.distance.Euclidean(subset)\n\n                if cluster is None:\n                    assert len(matrix) < self.MaxClustering\n                    cluster = hierarchical.dist_matrix_clustering(\n                        matrix, linkage=hierarchical.WARD\n                    )\n                if ordered and cluster_ord is None:\n                    assert len(matrix) < self.MaxOrderedClustering\n                    cluster_ord = hierarchical.optimal_leaf_ordering(\n                        cluster, matrix,\n                    )\n            row_groups.append(row._replace(cluster=cluster, cluster_ordered=cluster_ord))\n\n        return parts._replace(rows=row_groups)\n\n    def cluster_columns(self, data, parts, ordered=False):\n        assert len(parts.columns) == 1, \"columns split is no longer supported\"\n        assert all(var.is_continuous for var in data.domain.attributes)\n\n        col0 = parts.columns[0]\n        if col0.cluster is not None:\n            cluster = col0.cluster\n        else:\n            cluster = None\n        if col0.cluster_ordered is not None:\n            cluster_ord = col0.cluster_ordered\n        else:\n            cluster_ord = None\n        need_dist = cluster is None or (ordered and cluster_ord is None)\n        matrix = None\n        if need_dist:\n            data = Orange.distance._preprocess(data)\n            matrix = np.asarray(Orange.distance.PearsonR(data, axis=0))\n            # nan values break clustering below\n            matrix = np.nan_to_num(matrix)\n\n        if cluster is None:\n            assert matrix is not None\n            assert len(matrix) < self.MaxClustering\n            cluster = hierarchical.dist_matrix_clustering(\n                matrix, linkage=hierarchical.WARD\n            )\n        if ordered and cluster_ord is None:\n            assert len(matrix) < self.MaxOrderedClustering\n            cluster_ord = hierarchical.optimal_leaf_ordering(cluster, matrix)\n\n        col_groups = [col._replace(cluster=cluster, cluster_ordered=cluster_ord)\n                      for col in parts.columns]\n        return parts._replace(columns=col_groups)\n\n    def construct_heatmaps(self, data, group_var=None) -> 'Parts':\n        if self.merge_kmeans:\n            if self.kmeans_model is None:\n                effective_data = self.input_data.transform(\n                    Orange.data.Domain(\n                        [var for var in self.input_data.domain.attributes\n                         if var.is_continuous],\n                        self.input_data.domain.class_vars,\n                        self.input_data.domain.metas))\n                nclust = min(self.merge_kmeans_k, len(effective_data) - 1)\n                self.kmeans_model = kmeans_compress(effective_data, k=nclust)\n                effective_data.domain = self.kmeans_model.domain\n                merge_indices = [np.flatnonzero(self.kmeans_model.labels == ind)\n                                 for ind in range(nclust)]\n                not_empty_indices = [i for i, x in enumerate(merge_indices)\n                                     if len(x) > 0]\n                self.merge_indices = \\\n                    [merge_indices[i] for i in not_empty_indices]\n                if len(merge_indices) != len(self.merge_indices):\n                    self.Warning.empty_clusters()\n                effective_data = Orange.data.Table(\n                    Orange.data.Domain(effective_data.domain.attributes),\n                    self.kmeans_model.centroids[not_empty_indices]\n                )\n            else:\n                effective_data = self.effective_data\n\n            group_var = None\n        else:\n            self.kmeans_model = None\n            self.merge_indices = None\n            effective_data = data\n\n        self.effective_data = effective_data\n\n        self.__update_clustering_enable_state(effective_data)\n\n        parts = self._make_parts(effective_data, group_var)\n        # Restore/update the row/columns items descriptions from cache if\n        # available\n        rows_cache_key = (group_var,\n                          self.merge_kmeans_k if self.merge_kmeans else None)\n        if rows_cache_key in self.__rows_cache:\n            parts = parts._replace(rows=self.__rows_cache[rows_cache_key].rows)\n\n        if self.row_clustering != Clustering.None_:\n            parts = self.cluster_rows(\n                effective_data, parts,\n                ordered=self.row_clustering == Clustering.OrderedClustering\n            )\n        if self.col_clustering != Clustering.None_:\n            parts = self.cluster_columns(\n                effective_data, parts,\n                ordered=self.col_clustering == Clustering.OrderedClustering\n            )\n\n        # Cache the updated parts\n        self.__rows_cache[rows_cache_key] = parts\n        return parts\n\n    def construct_heatmaps_scene(self, parts: 'Parts', data: Table) -> None:\n        _T = TypeVar(\"_T\", bound=Union[RowPart, ColumnPart])\n\n        def select_cluster(clustering: Clustering, item: _T) -> _T:\n            if clustering == Clustering.None_:\n                return item._replace(cluster=None, cluster_ordered=None)\n            elif clustering == Clustering.Clustering:\n                return item._replace(cluster=item.cluster, cluster_ordered=None)\n            elif clustering == Clustering.OrderedClustering:\n                return item._replace(cluster=item.cluster_ordered, cluster_ordered=None)\n            else:  # pragma: no cover\n                raise TypeError()\n\n        rows = [select_cluster(self.row_clustering, rowitem)\n                for rowitem in parts.rows]\n        cols = [select_cluster(self.col_clustering, colitem)\n                for colitem in parts.columns]\n        parts = Parts(columns=cols, rows=rows, span=parts.span)\n\n        self.setup_scene(parts, data)\n\n    def setup_scene(self, parts, data):\n        # type: (Parts, Table) -> None\n        widget = HeatmapGridWidget()\n        widget.setColorMap(self.color_map())\n        self.scene.addItem(widget)\n        self.scene.widget = widget\n        columns = [v.name for v in data.domain.attributes]\n        parts = HeatmapGridWidget.Parts(\n            rows=[\n                HeatmapGridWidget.RowItem(r.title, r.indices, r.cluster)\n                for r in parts.rows\n            ],\n            columns=[\n                HeatmapGridWidget.ColumnItem(c.title, c.indices, c.cluster)\n                for c in parts.columns\n            ],\n            data=data.X,\n            span=parts.span,\n            row_names=None,\n            col_names=columns,\n        )\n        widget.setHeatmaps(parts)\n        side = self.row_side_colors()\n        if side is not None:\n            widget.setRowSideColorAnnotations(side[0], side[1], name=side[2].name)\n        widget.setColumnLabelsPosition(self._column_label_pos)\n        widget.setAspectRatioMode(\n            Qt.KeepAspectRatio if self.keep_aspect else Qt.IgnoreAspectRatio\n        )\n        widget.setShowAverages(self.averages)\n        widget.setLegendVisible(self.legend)\n\n        widget.layoutDidActivate.connect(self.__on_layout_activate)\n        widget.selectionFinished.connect(self.on_selection_finished)\n\n        self.update_annotations()\n        self.view.setCentralWidget(widget)\n        self.parts = parts\n\n    def __update_scene_rects(self):\n        widget = self.scene.widget\n        if widget is None:\n            return\n        rect = widget.geometry()\n        self.scene.setSceneRect(rect)\n        self.view.setSceneRect(rect)\n        self.view.setHeaderSceneRect(widget.headerGeometry())\n        self.view.setFooterSceneRect(widget.footerGeometry())\n\n    def __on_layout_activate(self):\n        self.__update_scene_rects()\n\n    def __aspect_mode_changed(self):\n        widget = self.scene.widget\n        if widget is None:\n            return\n        widget.setAspectRatioMode(\n            Qt.KeepAspectRatio if self.keep_aspect else Qt.IgnoreAspectRatio\n        )\n        # when aspect fixed the vertical sh is fixex, when not, it can\n        # shrink vertically\n        sp = widget.sizePolicy()\n        if self.keep_aspect:\n            sp.setVerticalPolicy(QSizePolicy.Fixed)\n        else:\n            sp.setVerticalPolicy(QSizePolicy.Preferred)\n        widget.setSizePolicy(sp)\n\n    def __update_clustering_enable_state(self, data):\n        if data is not None:\n            N = len(data)\n            M = len(data.domain.attributes)\n        else:\n            N = M = 0\n\n        rc_enabled = N <= self.MaxClustering\n        rco_enabled = N <= self.MaxOrderedClustering\n        cc_enabled = M <= self.MaxClustering\n        cco_enabled = M <= self.MaxOrderedClustering\n        row_clust, col_clust = self.row_clustering, self.col_clustering\n\n        row_clust_msg = \"\"\n        col_clust_msg = \"\"\n\n        if not rco_enabled and row_clust == Clustering.OrderedClustering:\n            row_clust = Clustering.Clustering\n            row_clust_msg = \"Row cluster ordering was disabled due to the \" \\\n                            \"input matrix being to big\"\n        if not rc_enabled and row_clust == Clustering.Clustering:\n            row_clust = Clustering.None_\n            row_clust_msg = \"Row clustering was was disabled due to the \" \\\n                            \"input matrix being to big\"\n\n        if not cco_enabled and col_clust == Clustering.OrderedClustering:\n            col_clust = Clustering.Clustering\n            col_clust_msg = \"Column cluster ordering was disabled due to \" \\\n                            \"the input matrix being to big\"\n        if not cc_enabled and col_clust == Clustering.Clustering:\n            col_clust = Clustering.None_\n            col_clust_msg = \"Column clustering was disabled due to the \" \\\n                            \"input matrix being to big\"\n\n        self.col_clustering = col_clust\n        self.row_clustering = row_clust\n\n        self.Information.row_clust(row_clust_msg, shown=bool(row_clust_msg))\n        self.Information.col_clust(col_clust_msg, shown=bool(col_clust_msg))\n\n        # Disable/enable the combobox items for the clustering methods\n        def setenabled(cb: QComboBox, clu: bool, clu_op: bool):\n            model = cb.model()\n            assert isinstance(model, QStandardItemModel)\n            idx = cb.findData(Clustering.OrderedClustering, ClusteringRole)\n            assert idx != -1\n            model.item(idx).setEnabled(clu_op)\n            idx = cb.findData(Clustering.Clustering, ClusteringRole)\n            assert idx != -1\n            model.item(idx).setEnabled(clu)\n\n        setenabled(self.row_cluster_cb, rc_enabled, rco_enabled)\n        setenabled(self.col_cluster_cb, cc_enabled, cco_enabled)\n\n    def update_averages_stripe(self):\n        \"\"\"Update the visibility of the averages stripe.\n        \"\"\"\n        widget = self.scene.widget\n        if widget is not None:\n            widget.setShowAverages(self.averages)\n\n    def update_lowslider(self):\n        low, high = self.controls.threshold_low, self.controls.threshold_high\n        if low.value() >= high.value():\n            low.setSliderPosition(high.value() - 1)\n        self.update_color_schema()\n\n    def update_highslider(self):\n        low, high = self.controls.threshold_low, self.controls.threshold_high\n        if low.value() >= high.value():\n            high.setSliderPosition(low.value() + 1)\n        self.update_color_schema()\n\n    def update_color_schema(self):\n        self.palette_name = self.color_cb.currentData().name\n        w = self.scene.widget\n        if w is not None:\n            w.setColorMap(self.color_map())\n\n    def __update_column_clustering(self):\n        self.update_heatmaps()\n        self.commit()\n\n    def __update_row_clustering(self):\n        self.update_heatmaps()\n        self.commit()\n\n    def update_legend(self):\n        widget = self.scene.widget\n        if widget is not None:\n            widget.setLegendVisible(self.legend)\n\n    def row_annotation_var(self):\n        return self.annotation_var\n\n    def row_annotation_data(self):\n        var = self.row_annotation_var()\n        if var is None:\n            return None\n        return column_str_from_table(self.input_data, var)\n\n    def _merge_row_indices(self):\n        if self.merge_kmeans and self.kmeans_model is not None:\n            return self.merge_indices\n        else:\n            return None\n\n    def set_annotation_var(self, var: Union[None, Variable, int]):\n        if isinstance(var, int):\n            var = self.annotation_model[var]\n        if self.annotation_var != var:\n            self.annotation_var = var\n            self.update_annotations()\n\n    def update_annotations(self):\n        widget = self.scene.widget\n        if widget is not None:\n            annot_col = self.row_annotation_data()\n            merge_indices = self._merge_row_indices()\n            if merge_indices is not None and annot_col is not None:\n                join = lambda _1: join_elided(\", \", 42, _1, \" ({} more)\")\n                annot_col = aggregate_apply(join, annot_col, merge_indices)\n            if annot_col is not None:\n                widget.setRowLabels(annot_col)\n                widget.setRowLabelsVisible(True)\n            else:\n                widget.setRowLabelsVisible(False)\n                widget.setRowLabels(None)\n\n    def row_side_colors(self):\n        var = self.annotation_color_var\n        if var is None:\n            return None\n        column_data = column_data_from_table(self.input_data, var)\n        span = (np.nanmin(column_data), np.nanmax(column_data))\n        merges = self._merge_row_indices()\n        if merges is not None:\n            column_data = aggregate(var, column_data, merges)\n        data, colormap = self._colorize(var, column_data)\n        if var.is_continuous:\n            colormap.span = span\n        return data, colormap, var\n\n    def set_annotation_color_var(self, var: Union[None, Variable, int]):\n        \"\"\"Set the current side color annotation variable.\"\"\"\n        if isinstance(var, int):\n            var = self.row_side_color_model[var]\n        if self.annotation_color_var != var:\n            self.annotation_color_var = var\n            self.update_row_side_colors()\n\n    def update_row_side_colors(self):\n        widget = self.scene.widget\n        if widget is None:\n            return\n        colors = self.row_side_colors()\n        if colors is None:\n            widget.setRowSideColorAnnotations(None)\n        else:\n            widget.setRowSideColorAnnotations(colors[0], colors[1], colors[2].name)\n\n    def _colorize(self, var: Variable, data: np.ndarray) -> Tuple[np.ndarray, ColorMap]:\n        palette = var.palette  # type: Palette\n        colors = np.array(\n            [[c.red(), c.green(), c.blue()] for c in palette.qcolors_w_nan],\n            dtype=np.uint8,\n        )\n        if var.is_discrete:\n            mask = np.isnan(data)\n            data[mask] = -1\n            data = data.astype(int)\n            if mask.any():\n                values = (*var.values, \"N/A\")\n            else:\n                values = var.values\n                colors = colors[: -1]\n            return data, CategoricalColorMap(colors, values)\n        elif var.is_continuous:\n            cmap = GradientColorMap(colors[:-1])\n            return data, cmap\n        else:\n            raise TypeError\n\n    def update_column_annotations(self):\n        widget = self.scene.widget\n        if self.data is not None and widget is not None:\n            widget.setColumnLabelsPosition(self._column_label_pos)\n\n    def __adjust_font_size(self, diff):\n        widget = self.scene.widget\n        if widget is None:\n            return\n        curr = widget.font().pointSizeF()\n        new = curr + diff\n\n        self.__font_dec.setEnabled(new > 1.0)\n        self.__font_inc.setEnabled(new <= 32)\n        if new > 1.0:\n            font = QFont()\n            font.setPointSizeF(new)\n            widget.setFont(font)\n\n    def _on_view_context_menu(self, pos):\n        widget = self.scene.widget\n        if widget is None:\n            return\n        assert isinstance(widget, HeatmapGridWidget)\n        menu = QMenu(self.view.viewport())\n        menu.setAttribute(Qt.WA_DeleteOnClose)\n        menu.addActions(self.view.actions())\n        menu.addSeparator()\n        menu.addActions([self.__font_inc, self.__font_dec])\n        menu.addSeparator()\n        a = QAction(\"Keep aspect ratio\", menu, checkable=True)\n        a.setChecked(self.keep_aspect)\n\n        def ontoggled(state):\n            self.keep_aspect = state\n            self.__aspect_mode_changed()\n        a.toggled.connect(ontoggled)\n        menu.addAction(a)\n        menu.popup(self.view.viewport().mapToGlobal(pos))\n\n    def on_selection_finished(self):\n        if self.scene.widget is not None:\n            self.selected_rows = list(self.scene.widget.selectedRows())\n        else:\n            self.selected_rows = []\n        self.commit()\n\n    def commit(self):\n        data = None\n        indices = None\n        if self.merge_kmeans:\n            merge_indices = self.merge_indices\n        else:\n            merge_indices = None\n\n        if self.input_data is not None and self.selected_rows:\n            indices = self.selected_rows\n            if merge_indices is not None:\n                # expand merged indices\n                indices = np.hstack([merge_indices[i] for i in indices])\n\n            data = self.input_data[indices]\n\n        summary = len(data) if data else self.info.NoOutput\n        details = format_summary_details(data) if data else \"\"\n        self.info.set_output_summary(summary, details)\n        self.Outputs.selected_data.send(data)\n        self.Outputs.annotated_data.send(create_annotated_table(self.input_data, indices))\n\n    def onDeleteWidget(self):\n        self.clear()\n        super().onDeleteWidget()\n\n    def send_report(self):\n        self.report_items((\n            (\"Columns:\", \"Clustering\" if self.col_clustering else \"No sorting\"),\n            (\"Rows:\", \"Clustering\" if self.row_clustering else \"No sorting\"),\n            (\"Split:\",\n             self.split_by_var is not None and self.split_by_var.name),\n            (\"Row annotation\",\n             self.annotation_var is not None and self.annotation_var.name),\n        ))\n        self.report_plot()\n\n    @classmethod\n    def migrate_settings(cls, settings, version):\n        if version is not None and version < 3:\n            def st2cl(state: bool) -> Clustering:\n                return Clustering.OrderedClustering if state else \\\n                    Clustering.None_\n\n            rc = settings.pop(\"row_clustering\", False)\n            cc = settings.pop(\"col_clustering\", False)\n            settings[\"row_clustering_method\"] = st2cl(rc).name\n            settings[\"col_clustering_method\"] = st2cl(cc).name",
                            "ast_type_str": "ClassDef",
                            "start_lineno": 611,
                            "end_lineno": 640,
                            "parent": {
                                "file_name": "Orange/widgets/visualize/owheatmap.py",
                                "type": "class",
                                "name": "OWHeatMap",
                                "content": "class OWHeatMap(widget.OWWidget):\n    name = \"Heat Map\"\n    description = \"Plot a data matrix heatmap.\"\n    icon = \"icons/Heatmap.svg\"\n    priority = 260\n    keywords = []\n\n    class Inputs:\n        data = Input(\"Data\", Table)\n\n    class Outputs:\n        selected_data = Output(\"Selected Data\", Table, default=True)\n        annotated_data = Output(ANNOTATED_DATA_SIGNAL_NAME, Table)\n\n    settings_version = 3\n\n    settingsHandler = settings.DomainContextHandler()\n\n    # Disable clustering for inputs bigger than this\n    MaxClustering = 25000\n    # Disable cluster leaf ordering for inputs bigger than this\n    MaxOrderedClustering = 1000\n\n    threshold_low = settings.Setting(0.0)\n    threshold_high = settings.Setting(1.0)\n\n    merge_kmeans = settings.Setting(False)\n    merge_kmeans_k = settings.Setting(50)\n\n    # Display column with averages\n    averages: bool = settings.Setting(True)\n    # Display legend\n    legend: bool = settings.Setting(True)\n    # Annotations\n    #: text row annotation (row names)\n    annotation_var = settings.ContextSetting(None)\n    #: color row annotation\n    annotation_color_var = settings.ContextSetting(None)\n    # Discrete variable used to split that data/heatmaps (vertically)\n    split_by_var = settings.ContextSetting(None)\n    # Selected row/column clustering method (name)\n    col_clustering_method: str = settings.Setting(Clustering.None_.name)\n    row_clustering_method: str = settings.Setting(Clustering.None_.name)\n\n    palette_name = settings.Setting(colorpalettes.DefaultContinuousPaletteName)\n    column_label_pos: int = settings.Setting(1)\n    selected_rows: List[int] = settings.Setting(None, schema_only=True)\n\n    auto_commit = settings.Setting(True)\n\n    graph_name = \"scene\"\n\n    left_side_scrolling = True\n\n    class Information(widget.OWWidget.Information):\n        sampled = Msg(\"Data has been sampled\")\n        discrete_ignored = Msg(\"{} categorical feature{} ignored\")\n        row_clust = Msg(\"{}\")\n        col_clust = Msg(\"{}\")\n        sparse_densified = Msg(\"Showing this data may require a lot of memory\")\n\n    class Error(widget.OWWidget.Error):\n        no_continuous = Msg(\"No numeric features\")\n        not_enough_features = Msg(\"Not enough features for column clustering\")\n        not_enough_instances = Msg(\"Not enough instances for clustering\")\n        not_enough_instances_k_means = Msg(\n            \"Not enough instances for k-means merging\")\n        not_enough_memory = Msg(\"Not enough memory to show this data\")\n\n    class Warning(widget.OWWidget.Warning):\n        empty_clusters = Msg(\"Empty clusters were removed\")\n\n    def __init__(self):\n        super().__init__()\n        self.__pending_selection = self.selected_rows\n\n        # A kingdom for a save_state/restore_state\n        self.col_clustering = enum_get(\n            Clustering, self.col_clustering_method, Clustering.None_)\n        self.row_clustering = enum_get(\n            Clustering, self.row_clustering_method, Clustering.None_)\n\n        @self.settingsAboutToBePacked.connect\n        def _():\n            self.col_clustering_method = self.col_clustering.name\n            self.row_clustering_method = self.row_clustering.name\n\n        self.keep_aspect = False\n\n        #: The original data with all features (retained to\n        #: preserve the domain on the output)\n        self.input_data = None\n        #: The effective data striped of discrete features, and often\n        #: merged using k-means\n        self.data = None\n        self.effective_data = None\n        #: kmeans model used to merge rows of input_data\n        self.kmeans_model = None\n        #: merge indices derived from kmeans\n        #: a list (len==k) of int ndarray where the i-th item contains\n        #: the indices which merge the input_data into the heatmap row i\n        self.merge_indices = None\n        self.parts: Optional[Parts] = None\n        self.__rows_cache = {}\n        self.__columns_cache = {}\n\n        self.info.set_input_summary(self.info.NoInput)\n        self.info.set_output_summary(self.info.NoOutput)\n\n        # GUI definition\n        colorbox = gui.vBox(self.controlArea, \"Color\")\n        self.color_cb = gui.palette_combo_box(self.palette_name)\n        self.color_cb.currentIndexChanged.connect(self.update_color_schema)\n        colorbox.layout().addWidget(self.color_cb)\n\n        form = QFormLayout(\n            formAlignment=Qt.AlignLeft,\n            labelAlignment=Qt.AlignLeft,\n            fieldGrowthPolicy=QFormLayout.AllNonFixedFieldsGrow\n        )\n        lowslider = gui.hSlider(\n            colorbox, self, \"threshold_low\", minValue=0.0, maxValue=1.0,\n            step=0.05, ticks=True, intOnly=False,\n            createLabel=False, callback=self.update_lowslider)\n        highslider = gui.hSlider(\n            colorbox, self, \"threshold_high\", minValue=0.0, maxValue=1.0,\n            step=0.05, ticks=True, intOnly=False,\n            createLabel=False, callback=self.update_highslider)\n\n        form.addRow(\"Low:\", lowslider)\n        form.addRow(\"High:\", highslider)\n\n        colorbox.layout().addLayout(form)\n\n        mergebox = gui.vBox(self.controlArea, \"Merge\",)\n        gui.checkBox(mergebox, self, \"merge_kmeans\", \"Merge by k-means\",\n                     callback=self.__update_row_clustering)\n        ibox = gui.indentedBox(mergebox)\n        gui.spin(ibox, self, \"merge_kmeans_k\", minv=5, maxv=500,\n                 label=\"Clusters:\", keyboardTracking=False,\n                 callbackOnReturn=True, callback=self.update_merge)\n\n        cluster_box = gui.vBox(self.controlArea, \"Clustering\")\n        # Row clustering\n        self.row_cluster_cb = cb = ComboBox()\n        cb.setModel(create_list_model(ClusteringModelData, self))\n        cbselect(cb, self.row_clustering, ClusteringRole)\n        self.connect_control(\n            \"row_clustering\",\n            lambda value, cb=cb: cbselect(cb, value, ClusteringRole)\n        )\n        @cb.activated.connect\n        def _(idx, cb=cb):\n            self.set_row_clustering(cb.itemData(idx, ClusteringRole))\n\n        # Column clustering\n        self.col_cluster_cb = cb = ComboBox()\n        cb.setModel(create_list_model(ClusteringModelData, self))\n        cbselect(cb, self.col_clustering, ClusteringRole)\n        self.connect_control(\n            \"col_clustering\",\n            lambda value, cb=cb: cbselect(cb, value, ClusteringRole)\n        )\n        @cb.activated.connect\n        def _(idx, cb=cb):\n            self.set_col_clustering(cb.itemData(idx, ClusteringRole))\n\n        form = QFormLayout(\n            labelAlignment=Qt.AlignLeft, formAlignment=Qt.AlignLeft,\n            fieldGrowthPolicy=QFormLayout.AllNonFixedFieldsGrow,\n        )\n        form.addRow(\"Rows:\", self.row_cluster_cb)\n        form.addRow(\"Columns:\", self.col_cluster_cb)\n        cluster_box.layout().addLayout(form)\n        box = gui.vBox(self.controlArea, \"Split By\")\n\n        self.row_split_model = DomainModel(\n            placeholder=\"(None)\",\n            valid_types=(Orange.data.DiscreteVariable,),\n            parent=self,\n        )\n        self.row_split_cb = cb = ComboBoxSearch(\n            enabled=not self.merge_kmeans,\n            sizeAdjustPolicy=ComboBox.AdjustToMinimumContentsLengthWithIcon,\n            minimumContentsLength=14,\n            toolTip=\"Split the heatmap vertically by a categorical column\"\n        )\n        self.row_split_cb.setModel(self.row_split_model)\n        self.connect_control(\n            \"split_by_var\", lambda value, cb=cb: cbselect(cb, value)\n        )\n        self.connect_control(\n            \"merge_kmeans\", self.row_split_cb.setDisabled\n        )\n        self.split_by_var = None\n\n        self.row_split_cb.activated.connect(\n            self.__on_split_rows_activated\n        )\n        box.layout().addWidget(self.row_split_cb)\n\n        box = gui.vBox(self.controlArea, 'Annotation && Legends')\n\n        gui.checkBox(box, self, 'legend', 'Show legend',\n                     callback=self.update_legend)\n\n        gui.checkBox(box, self, 'averages', 'Stripes with averages',\n                     callback=self.update_averages_stripe)\n        annotbox = QGroupBox(\"Row Annotations\", flat=True)\n        form = QFormLayout(\n            annotbox,\n            formAlignment=Qt.AlignLeft,\n            labelAlignment=Qt.AlignLeft,\n            fieldGrowthPolicy=QFormLayout.AllNonFixedFieldsGrow\n        )\n        self.annotation_model = DomainModel(placeholder=\"(None)\")\n        self.annotation_text_cb = ComboBoxSearch(\n            minimumContentsLength=12,\n            sizeAdjustPolicy=QComboBox.AdjustToMinimumContentsLength\n        )\n        self.annotation_text_cb.setModel(self.annotation_model)\n        self.annotation_text_cb.activated.connect(self.set_annotation_var)\n        self.connect_control(\"annotation_var\", self.annotation_var_changed)\n\n        self.row_side_color_model = DomainModel(\n            order=(DomainModel.CLASSES, DomainModel.Separator,\n                   DomainModel.METAS),\n            placeholder=\"(None)\", valid_types=DomainModel.PRIMITIVE,\n            flags=Qt.ItemIsSelectable | Qt.ItemIsEnabled,\n            parent=self,\n        )\n        self.row_side_color_cb = ComboBoxSearch(\n            sizeAdjustPolicy=QComboBox.AdjustToMinimumContentsLength,\n            minimumContentsLength=12\n        )\n        self.row_side_color_cb.setModel(self.row_side_color_model)\n        self.row_side_color_cb.activated.connect(self.set_annotation_color_var)\n        self.connect_control(\"annotation_color_var\", self.annotation_color_var_changed)\n        form.addRow(\"Text\", self.annotation_text_cb)\n        form.addRow(\"Color\", self.row_side_color_cb)\n        box.layout().addWidget(annotbox)\n        posbox = gui.vBox(box, \"Column Labels Position\", addSpace=False)\n        posbox.setFlat(True)\n        cb = gui.comboBox(\n            posbox, self, \"column_label_pos\",\n            callback=self.update_column_annotations)\n        cb.setModel(create_list_model(ColumnLabelsPosData, parent=self))\n        cb.setCurrentIndex(self.column_label_pos)\n        gui.checkBox(self.controlArea, self, \"keep_aspect\",\n                     \"Keep aspect ratio\", box=\"Resize\",\n                     callback=self.__aspect_mode_changed)\n\n        gui.rubber(self.controlArea)\n        gui.auto_send(self.controlArea, self, \"auto_commit\")\n\n        # Scene with heatmap\n        class HeatmapScene(QGraphicsScene):\n            widget: Optional[HeatmapGridWidget] = None\n\n        self.scene = self.scene = HeatmapScene(parent=self)\n        self.view = GraphicsView(\n            self.scene,\n            verticalScrollBarPolicy=Qt.ScrollBarAlwaysOn,\n            horizontalScrollBarPolicy=Qt.ScrollBarAlwaysOn,\n            viewportUpdateMode=QGraphicsView.FullViewportUpdate,\n            widgetResizable=True,\n        )\n        self.view.setContextMenuPolicy(Qt.CustomContextMenu)\n        self.view.customContextMenuRequested.connect(\n            self._on_view_context_menu\n        )\n        self.mainArea.layout().addWidget(self.view)\n        self.selected_rows = []\n        self.__font_inc = QAction(\n            \"Increase Font\", self, shortcut=QKeySequence(\"ctrl+>\"))\n        self.__font_dec = QAction(\n            \"Decrease Font\", self, shortcut=QKeySequence(\"ctrl+<\"))\n        self.__font_inc.triggered.connect(lambda: self.__adjust_font_size(1))\n        self.__font_dec.triggered.connect(lambda: self.__adjust_font_size(-1))\n        if hasattr(QAction, \"setShortcutVisibleInContextMenu\"):\n            apply_all(\n                [self.__font_inc, self.__font_dec],\n                lambda a: a.setShortcutVisibleInContextMenu(True)\n            )\n        self.addActions([self.__font_inc, self.__font_dec])\n\n    @property\n    def center_palette(self):\n        palette = self.color_cb.currentData()\n        return bool(palette.flags & palette.Diverging)\n\n    @property\n    def _column_label_pos(self) -> HeatmapGridWidget.Position:\n        return ColumnLabelsPosData[self.column_label_pos][Qt.UserRole]\n\n    def annotation_color_var_changed(self, value):\n        cbselect(self.row_side_color_cb, value, Qt.EditRole)\n\n    def annotation_var_changed(self, value):\n        cbselect(self.annotation_text_cb, value, Qt.EditRole)\n\n    def set_row_clustering(self, method: Clustering) -> None:\n        assert isinstance(method, Clustering)\n        if self.row_clustering != method:\n            self.row_clustering = method\n            cbselect(self.row_cluster_cb, method, ClusteringRole)\n            self.__update_row_clustering()\n\n    def set_col_clustering(self, method: Clustering) -> None:\n        assert isinstance(method, Clustering)\n        if self.col_clustering != method:\n            self.col_clustering = method\n            cbselect(self.col_cluster_cb, method, ClusteringRole)\n            self.__update_column_clustering()\n\n    def sizeHint(self) -> QSize:\n        return super().sizeHint().expandedTo(QSize(900, 700))\n\n    def color_palette(self):\n        return self.color_cb.currentData().lookup_table()\n\n    def color_map(self) -> GradientColorMap:\n        return GradientColorMap(\n            self.color_palette(), (self.threshold_low, self.threshold_high),\n            0 if self.center_palette else None\n        )\n\n    def clear(self):\n        self.data = None\n        self.input_data = None\n        self.effective_data = None\n        self.kmeans_model = None\n        self.merge_indices = None\n        self.annotation_model.set_domain(None)\n        self.annotation_var = None\n        self.row_side_color_model.set_domain(None)\n        self.annotation_color_var = None\n        self.row_split_model.set_domain(None)\n        self.split_by_var = None\n        self.parts = None\n        self.clear_scene()\n        self.selected_rows = []\n        self.__columns_cache.clear()\n        self.__rows_cache.clear()\n        self.__update_clustering_enable_state(None)\n\n    def clear_scene(self):\n        if self.scene.widget is not None:\n            self.scene.widget.layoutDidActivate.disconnect(\n                self.__on_layout_activate\n            )\n            self.scene.widget.selectionFinished.disconnect(\n                self.on_selection_finished\n            )\n        self.scene.widget = None\n        self.scene.clear()\n\n        self.view.setSceneRect(QRectF())\n        self.view.setHeaderSceneRect(QRectF())\n        self.view.setFooterSceneRect(QRectF())\n\n    @Inputs.data\n    def set_dataset(self, data=None):\n        \"\"\"Set the input dataset to display.\"\"\"\n        self.closeContext()\n        self.clear()\n        self.clear_messages()\n        self._set_input_summary(data)\n\n        if isinstance(data, SqlTable):\n            if data.approx_len() < 4000:\n                data = Table(data)\n            else:\n                self.Information.sampled()\n                data_sample = data.sample_time(1, no_cache=True)\n                data_sample.download_data(2000, partial=True)\n                data = Table(data_sample)\n\n        if data is not None and not len(data):\n            data = None\n\n        if data is not None and sp.issparse(data.X):\n            try:\n                data = data.to_dense()\n            except MemoryError:\n                data = None\n                self.Error.not_enough_memory()\n            else:\n                self.Information.sparse_densified()\n\n        input_data = data\n\n        # Data contains no attributes or meta attributes only\n        if data is not None and len(data.domain.attributes) == 0:\n            self.Error.no_continuous()\n            input_data = data = None\n\n        # Data contains some discrete attributes which must be filtered\n        if data is not None and \\\n                any(var.is_discrete for var in data.domain.attributes):\n            ndisc = sum(var.is_discrete for var in data.domain.attributes)\n            data = data.transform(\n                Domain([var for var in data.domain.attributes\n                        if var.is_continuous],\n                       data.domain.class_vars,\n                       data.domain.metas))\n            if not data.domain.attributes:\n                self.Error.no_continuous()\n                input_data = data = None\n            else:\n                self.Information.discrete_ignored(\n                    ndisc, \"s\" if ndisc > 1 else \"\")\n\n        self.data = data\n        self.input_data = input_data\n\n        if data is not None:\n            self.annotation_model.set_domain(self.input_data.domain)\n            self.row_side_color_model.set_domain(self.input_data.domain)\n            self.annotation_var = None\n            self.annotation_color_var = None\n            self.row_split_model.set_domain(data.domain)\n            if data.domain.has_discrete_class:\n                self.split_by_var = data.domain.class_var\n            else:\n                self.split_by_var = None\n            self.openContext(self.input_data)\n            if self.split_by_var not in self.row_split_model:\n                self.split_by_var = None\n\n        self.update_heatmaps()\n        if data is not None and self.__pending_selection is not None:\n            assert self.scene.widget is not None\n            self.scene.widget.selectRows(self.__pending_selection)\n            self.selected_rows = self.__pending_selection\n            self.__pending_selection = None\n\n        self.unconditional_commit()\n\n    def _set_input_summary(self, data):\n        summary = len(data) if data else self.info.NoInput\n        details = format_summary_details(data) if data else \"\"\n        self.info.set_input_summary(summary, details)\n\n    def __on_split_rows_activated(self):\n        self.set_split_variable(self.row_split_cb.currentData(Qt.EditRole))\n\n    def set_split_variable(self, var):\n        if var != self.split_by_var:\n            self.split_by_var = var\n            self.update_heatmaps()\n\n    def update_heatmaps(self):\n        if self.data is not None:\n            self.clear_scene()\n            self.clear_messages()\n            if self.col_clustering != Clustering.None_ and \\\n                    len(self.data.domain.attributes) < 2:\n                self.Error.not_enough_features()\n            elif (self.col_clustering != Clustering.None_ or\n                  self.row_clustering != Clustering.None_) and \\\n                    len(self.data) < 2:\n                self.Error.not_enough_instances()\n            elif self.merge_kmeans and len(self.data) < 3:\n                self.Error.not_enough_instances_k_means()\n            else:\n                parts = self.construct_heatmaps(self.data, self.split_by_var)\n                self.construct_heatmaps_scene(parts, self.effective_data)\n                self.selected_rows = []\n        else:\n            self.clear()\n\n    def update_merge(self):\n        self.kmeans_model = None\n        self.merge_indices = None\n        if self.data is not None and self.merge_kmeans:\n            self.update_heatmaps()\n            self.commit()\n\n    def _make_parts(self, data, group_var=None):\n        \"\"\"\n        Make initial `Parts` for data, split by group_var, group_key\n        \"\"\"\n        if group_var is not None:\n            assert group_var.is_discrete\n            _col_data = table_column_data(data, group_var)\n            row_indices = [np.flatnonzero(_col_data == i)\n                           for i in range(len(group_var.values))]\n\n            row_groups = [RowPart(title=name, indices=ind,\n                                  cluster=None, cluster_ordered=None)\n                          for name, ind in zip(group_var.values, row_indices)]\n            if np.any(_col_data.mask):\n                row_groups.append(RowPart(\n                    title=\"N/A\", indices=np.flatnonzero(_col_data.mask),\n                    cluster=None, cluster_ordered=None\n                ))\n        else:\n            row_groups = [RowPart(title=None, indices=range(0, len(data)),\n                                  cluster=None, cluster_ordered=None)]\n\n        col_groups = [\n            ColumnPart(\n                title=None, indices=range(0, len(data.domain.attributes)),\n                domain=data.domain, cluster=None, cluster_ordered=None)\n        ]\n\n        minv, maxv = np.nanmin(data.X), np.nanmax(data.X)\n        return Parts(row_groups, col_groups, span=(minv, maxv))\n\n    def cluster_rows(self, data: Table, parts: 'Parts', ordered=False) -> 'Parts':\n        row_groups = []\n        for row in parts.rows:\n            if row.cluster is not None:\n                cluster = row.cluster\n            else:\n                cluster = None\n            if row.cluster_ordered is not None:\n                cluster_ord = row.cluster_ordered\n            else:\n                cluster_ord = None\n\n            if row.can_cluster:\n                matrix = None\n                need_dist = cluster is None or (ordered and cluster_ord is None)\n                if need_dist:\n                    subset = data[row.indices]\n                    matrix = Orange.distance.Euclidean(subset)\n\n                if cluster is None:\n                    assert len(matrix) < self.MaxClustering\n                    cluster = hierarchical.dist_matrix_clustering(\n                        matrix, linkage=hierarchical.WARD\n                    )\n                if ordered and cluster_ord is None:\n                    assert len(matrix) < self.MaxOrderedClustering\n                    cluster_ord = hierarchical.optimal_leaf_ordering(\n                        cluster, matrix,\n                    )\n            row_groups.append(row._replace(cluster=cluster, cluster_ordered=cluster_ord))\n\n        return parts._replace(rows=row_groups)\n\n    def cluster_columns(self, data, parts, ordered=False):\n        assert len(parts.columns) == 1, \"columns split is no longer supported\"\n        assert all(var.is_continuous for var in data.domain.attributes)\n\n        col0 = parts.columns[0]\n        if col0.cluster is not None:\n            cluster = col0.cluster\n        else:\n            cluster = None\n        if col0.cluster_ordered is not None:\n            cluster_ord = col0.cluster_ordered\n        else:\n            cluster_ord = None\n        need_dist = cluster is None or (ordered and cluster_ord is None)\n        matrix = None\n        if need_dist:\n            data = Orange.distance._preprocess(data)\n            matrix = np.asarray(Orange.distance.PearsonR(data, axis=0))\n            # nan values break clustering below\n            matrix = np.nan_to_num(matrix)\n\n        if cluster is None:\n            assert matrix is not None\n            assert len(matrix) < self.MaxClustering\n            cluster = hierarchical.dist_matrix_clustering(\n                matrix, linkage=hierarchical.WARD\n            )\n        if ordered and cluster_ord is None:\n            assert len(matrix) < self.MaxOrderedClustering\n            cluster_ord = hierarchical.optimal_leaf_ordering(cluster, matrix)\n\n        col_groups = [col._replace(cluster=cluster, cluster_ordered=cluster_ord)\n                      for col in parts.columns]\n        return parts._replace(columns=col_groups)\n\n    def construct_heatmaps(self, data, group_var=None) -> 'Parts':\n        if self.merge_kmeans:\n            if self.kmeans_model is None:\n                effective_data = self.input_data.transform(\n                    Orange.data.Domain(\n                        [var for var in self.input_data.domain.attributes\n                         if var.is_continuous],\n                        self.input_data.domain.class_vars,\n                        self.input_data.domain.metas))\n                nclust = min(self.merge_kmeans_k, len(effective_data) - 1)\n                self.kmeans_model = kmeans_compress(effective_data, k=nclust)\n                effective_data.domain = self.kmeans_model.domain\n                merge_indices = [np.flatnonzero(self.kmeans_model.labels == ind)\n                                 for ind in range(nclust)]\n                not_empty_indices = [i for i, x in enumerate(merge_indices)\n                                     if len(x) > 0]\n                self.merge_indices = \\\n                    [merge_indices[i] for i in not_empty_indices]\n                if len(merge_indices) != len(self.merge_indices):\n                    self.Warning.empty_clusters()\n                effective_data = Orange.data.Table(\n                    Orange.data.Domain(effective_data.domain.attributes),\n                    self.kmeans_model.centroids[not_empty_indices]\n                )\n            else:\n                effective_data = self.effective_data\n\n            group_var = None\n        else:\n            self.kmeans_model = None\n            self.merge_indices = None\n            effective_data = data\n\n        self.effective_data = effective_data\n\n        self.__update_clustering_enable_state(effective_data)\n\n        parts = self._make_parts(effective_data, group_var)\n        # Restore/update the row/columns items descriptions from cache if\n        # available\n        rows_cache_key = (group_var,\n                          self.merge_kmeans_k if self.merge_kmeans else None)\n        if rows_cache_key in self.__rows_cache:\n            parts = parts._replace(rows=self.__rows_cache[rows_cache_key].rows)\n\n        if self.row_clustering != Clustering.None_:\n            parts = self.cluster_rows(\n                effective_data, parts,\n                ordered=self.row_clustering == Clustering.OrderedClustering\n            )\n        if self.col_clustering != Clustering.None_:\n            parts = self.cluster_columns(\n                effective_data, parts,\n                ordered=self.col_clustering == Clustering.OrderedClustering\n            )\n\n        # Cache the updated parts\n        self.__rows_cache[rows_cache_key] = parts\n        return parts\n\n    def construct_heatmaps_scene(self, parts: 'Parts', data: Table) -> None:\n        _T = TypeVar(\"_T\", bound=Union[RowPart, ColumnPart])\n\n        def select_cluster(clustering: Clustering, item: _T) -> _T:\n            if clustering == Clustering.None_:\n                return item._replace(cluster=None, cluster_ordered=None)\n            elif clustering == Clustering.Clustering:\n                return item._replace(cluster=item.cluster, cluster_ordered=None)\n            elif clustering == Clustering.OrderedClustering:\n                return item._replace(cluster=item.cluster_ordered, cluster_ordered=None)\n            else:  # pragma: no cover\n                raise TypeError()\n\n        rows = [select_cluster(self.row_clustering, rowitem)\n                for rowitem in parts.rows]\n        cols = [select_cluster(self.col_clustering, colitem)\n                for colitem in parts.columns]\n        parts = Parts(columns=cols, rows=rows, span=parts.span)\n\n        self.setup_scene(parts, data)\n\n    def setup_scene(self, parts, data):\n        # type: (Parts, Table) -> None\n        widget = HeatmapGridWidget()\n        widget.setColorMap(self.color_map())\n        self.scene.addItem(widget)\n        self.scene.widget = widget\n        columns = [v.name for v in data.domain.attributes]\n        parts = HeatmapGridWidget.Parts(\n            rows=[\n                HeatmapGridWidget.RowItem(r.title, r.indices, r.cluster)\n                for r in parts.rows\n            ],\n            columns=[\n                HeatmapGridWidget.ColumnItem(c.title, c.indices, c.cluster)\n                for c in parts.columns\n            ],\n            data=data.X,\n            span=parts.span,\n            row_names=None,\n            col_names=columns,\n        )\n        widget.setHeatmaps(parts)\n        side = self.row_side_colors()\n        if side is not None:\n            widget.setRowSideColorAnnotations(side[0], side[1], name=side[2].name)\n        widget.setColumnLabelsPosition(self._column_label_pos)\n        widget.setAspectRatioMode(\n            Qt.KeepAspectRatio if self.keep_aspect else Qt.IgnoreAspectRatio\n        )\n        widget.setShowAverages(self.averages)\n        widget.setLegendVisible(self.legend)\n\n        widget.layoutDidActivate.connect(self.__on_layout_activate)\n        widget.selectionFinished.connect(self.on_selection_finished)\n\n        self.update_annotations()\n        self.view.setCentralWidget(widget)\n        self.parts = parts\n\n    def __update_scene_rects(self):\n        widget = self.scene.widget\n        if widget is None:\n            return\n        rect = widget.geometry()\n        self.scene.setSceneRect(rect)\n        self.view.setSceneRect(rect)\n        self.view.setHeaderSceneRect(widget.headerGeometry())\n        self.view.setFooterSceneRect(widget.footerGeometry())\n\n    def __on_layout_activate(self):\n        self.__update_scene_rects()\n\n    def __aspect_mode_changed(self):\n        widget = self.scene.widget\n        if widget is None:\n            return\n        widget.setAspectRatioMode(\n            Qt.KeepAspectRatio if self.keep_aspect else Qt.IgnoreAspectRatio\n        )\n        # when aspect fixed the vertical sh is fixex, when not, it can\n        # shrink vertically\n        sp = widget.sizePolicy()\n        if self.keep_aspect:\n            sp.setVerticalPolicy(QSizePolicy.Fixed)\n        else:\n            sp.setVerticalPolicy(QSizePolicy.Preferred)\n        widget.setSizePolicy(sp)\n\n    def __update_clustering_enable_state(self, data):\n        if data is not None:\n            N = len(data)\n            M = len(data.domain.attributes)\n        else:\n            N = M = 0\n\n        rc_enabled = N <= self.MaxClustering\n        rco_enabled = N <= self.MaxOrderedClustering\n        cc_enabled = M <= self.MaxClustering\n        cco_enabled = M <= self.MaxOrderedClustering\n        row_clust, col_clust = self.row_clustering, self.col_clustering\n\n        row_clust_msg = \"\"\n        col_clust_msg = \"\"\n\n        if not rco_enabled and row_clust == Clustering.OrderedClustering:\n            row_clust = Clustering.Clustering\n            row_clust_msg = \"Row cluster ordering was disabled due to the \" \\\n                            \"input matrix being to big\"\n        if not rc_enabled and row_clust == Clustering.Clustering:\n            row_clust = Clustering.None_\n            row_clust_msg = \"Row clustering was was disabled due to the \" \\\n                            \"input matrix being to big\"\n\n        if not cco_enabled and col_clust == Clustering.OrderedClustering:\n            col_clust = Clustering.Clustering\n            col_clust_msg = \"Column cluster ordering was disabled due to \" \\\n                            \"the input matrix being to big\"\n        if not cc_enabled and col_clust == Clustering.Clustering:\n            col_clust = Clustering.None_\n            col_clust_msg = \"Column clustering was disabled due to the \" \\\n                            \"input matrix being to big\"\n\n        self.col_clustering = col_clust\n        self.row_clustering = row_clust\n\n        self.Information.row_clust(row_clust_msg, shown=bool(row_clust_msg))\n        self.Information.col_clust(col_clust_msg, shown=bool(col_clust_msg))\n\n        # Disable/enable the combobox items for the clustering methods\n        def setenabled(cb: QComboBox, clu: bool, clu_op: bool):\n            model = cb.model()\n            assert isinstance(model, QStandardItemModel)\n            idx = cb.findData(Clustering.OrderedClustering, ClusteringRole)\n            assert idx != -1\n            model.item(idx).setEnabled(clu_op)\n            idx = cb.findData(Clustering.Clustering, ClusteringRole)\n            assert idx != -1\n            model.item(idx).setEnabled(clu)\n\n        setenabled(self.row_cluster_cb, rc_enabled, rco_enabled)\n        setenabled(self.col_cluster_cb, cc_enabled, cco_enabled)\n\n    def update_averages_stripe(self):\n        \"\"\"Update the visibility of the averages stripe.\n        \"\"\"\n        widget = self.scene.widget\n        if widget is not None:\n            widget.setShowAverages(self.averages)\n\n    def update_lowslider(self):\n        low, high = self.controls.threshold_low, self.controls.threshold_high\n        if low.value() >= high.value():\n            low.setSliderPosition(high.value() - 1)\n        self.update_color_schema()\n\n    def update_highslider(self):\n        low, high = self.controls.threshold_low, self.controls.threshold_high\n        if low.value() >= high.value():\n            high.setSliderPosition(low.value() + 1)\n        self.update_color_schema()\n\n    def update_color_schema(self):\n        self.palette_name = self.color_cb.currentData().name\n        w = self.scene.widget\n        if w is not None:\n            w.setColorMap(self.color_map())\n\n    def __update_column_clustering(self):\n        self.update_heatmaps()\n        self.commit()\n\n    def __update_row_clustering(self):\n        self.update_heatmaps()\n        self.commit()\n\n    def update_legend(self):\n        widget = self.scene.widget\n        if widget is not None:\n            widget.setLegendVisible(self.legend)\n\n    def row_annotation_var(self):\n        return self.annotation_var\n\n    def row_annotation_data(self):\n        var = self.row_annotation_var()\n        if var is None:\n            return None\n        return column_str_from_table(self.input_data, var)\n\n    def _merge_row_indices(self):\n        if self.merge_kmeans and self.kmeans_model is not None:\n            return self.merge_indices\n        else:\n            return None\n\n    def set_annotation_var(self, var: Union[None, Variable, int]):\n        if isinstance(var, int):\n            var = self.annotation_model[var]\n        if self.annotation_var != var:\n            self.annotation_var = var\n            self.update_annotations()\n\n    def update_annotations(self):\n        widget = self.scene.widget\n        if widget is not None:\n            annot_col = self.row_annotation_data()\n            merge_indices = self._merge_row_indices()\n            if merge_indices is not None and annot_col is not None:\n                join = lambda _1: join_elided(\", \", 42, _1, \" ({} more)\")\n                annot_col = aggregate_apply(join, annot_col, merge_indices)\n            if annot_col is not None:\n                widget.setRowLabels(annot_col)\n                widget.setRowLabelsVisible(True)\n            else:\n                widget.setRowLabelsVisible(False)\n                widget.setRowLabels(None)\n\n    def row_side_colors(self):\n        var = self.annotation_color_var\n        if var is None:\n            return None\n        column_data = column_data_from_table(self.input_data, var)\n        span = (np.nanmin(column_data), np.nanmax(column_data))\n        merges = self._merge_row_indices()\n        if merges is not None:\n            column_data = aggregate(var, column_data, merges)\n        data, colormap = self._colorize(var, column_data)\n        if var.is_continuous:\n            colormap.span = span\n        return data, colormap, var\n\n    def set_annotation_color_var(self, var: Union[None, Variable, int]):\n        \"\"\"Set the current side color annotation variable.\"\"\"\n        if isinstance(var, int):\n            var = self.row_side_color_model[var]\n        if self.annotation_color_var != var:\n            self.annotation_color_var = var\n            self.update_row_side_colors()\n\n    def update_row_side_colors(self):\n        widget = self.scene.widget\n        if widget is None:\n            return\n        colors = self.row_side_colors()\n        if colors is None:\n            widget.setRowSideColorAnnotations(None)\n        else:\n            widget.setRowSideColorAnnotations(colors[0], colors[1], colors[2].name)\n\n    def _colorize(self, var: Variable, data: np.ndarray) -> Tuple[np.ndarray, ColorMap]:\n        palette = var.palette  # type: Palette\n        colors = np.array(\n            [[c.red(), c.green(), c.blue()] for c in palette.qcolors_w_nan],\n            dtype=np.uint8,\n        )\n        if var.is_discrete:\n            mask = np.isnan(data)\n            data[mask] = -1\n            data = data.astype(int)\n            if mask.any():\n                values = (*var.values, \"N/A\")\n            else:\n                values = var.values\n                colors = colors[: -1]\n            return data, CategoricalColorMap(colors, values)\n        elif var.is_continuous:\n            cmap = GradientColorMap(colors[:-1])\n            return data, cmap\n        else:\n            raise TypeError\n\n    def update_column_annotations(self):\n        widget = self.scene.widget\n        if self.data is not None and widget is not None:\n            widget.setColumnLabelsPosition(self._column_label_pos)\n\n    def __adjust_font_size(self, diff):\n        widget = self.scene.widget\n        if widget is None:\n            return\n        curr = widget.font().pointSizeF()\n        new = curr + diff\n\n        self.__font_dec.setEnabled(new > 1.0)\n        self.__font_inc.setEnabled(new <= 32)\n        if new > 1.0:\n            font = QFont()\n            font.setPointSizeF(new)\n            widget.setFont(font)\n\n    def _on_view_context_menu(self, pos):\n        widget = self.scene.widget\n        if widget is None:\n            return\n        assert isinstance(widget, HeatmapGridWidget)\n        menu = QMenu(self.view.viewport())\n        menu.setAttribute(Qt.WA_DeleteOnClose)\n        menu.addActions(self.view.actions())\n        menu.addSeparator()\n        menu.addActions([self.__font_inc, self.__font_dec])\n        menu.addSeparator()\n        a = QAction(\"Keep aspect ratio\", menu, checkable=True)\n        a.setChecked(self.keep_aspect)\n\n        def ontoggled(state):\n            self.keep_aspect = state\n            self.__aspect_mode_changed()\n        a.toggled.connect(ontoggled)\n        menu.addAction(a)\n        menu.popup(self.view.viewport().mapToGlobal(pos))\n\n    def on_selection_finished(self):\n        if self.scene.widget is not None:\n            self.selected_rows = list(self.scene.widget.selectedRows())\n        else:\n            self.selected_rows = []\n        self.commit()\n\n    def commit(self):\n        data = None\n        indices = None\n        if self.merge_kmeans:\n            merge_indices = self.merge_indices\n        else:\n            merge_indices = None\n\n        if self.input_data is not None and self.selected_rows:\n            indices = self.selected_rows\n            if merge_indices is not None:\n                # expand merged indices\n                indices = np.hstack([merge_indices[i] for i in indices])\n\n            data = self.input_data[indices]\n\n        summary = len(data) if data else self.info.NoOutput\n        details = format_summary_details(data) if data else \"\"\n        self.info.set_output_summary(summary, details)\n        self.Outputs.selected_data.send(data)\n        self.Outputs.annotated_data.send(create_annotated_table(self.input_data, indices))\n\n    def onDeleteWidget(self):\n        self.clear()\n        super().onDeleteWidget()\n\n    def send_report(self):\n        self.report_items((\n            (\"Columns:\", \"Clustering\" if self.col_clustering else \"No sorting\"),\n            (\"Rows:\", \"Clustering\" if self.row_clustering else \"No sorting\"),\n            (\"Split:\",\n             self.split_by_var is not None and self.split_by_var.name),\n            (\"Row annotation\",\n             self.annotation_var is not None and self.annotation_var.name),\n        ))\n        self.report_plot()\n\n    @classmethod\n    def migrate_settings(cls, settings, version):\n        if version is not None and version < 3:\n            def st2cl(state: bool) -> Clustering:\n                return Clustering.OrderedClustering if state else \\\n                    Clustering.None_\n\n            rc = settings.pop(\"row_clustering\", False)\n            cc = settings.pop(\"col_clustering\", False)\n            settings[\"row_clustering_method\"] = st2cl(rc).name\n            settings[\"col_clustering_method\"] = st2cl(cc).name",
                                "ast_type_str": "ClassDef",
                                "start_lineno": 132,
                                "end_lineno": 1136,
                                "parent": null
                            }
                        },
                        {
                            "file_name": "Orange/widgets/visualize/owheatmap.py",
                            "type": "class",
                            "name": "OWHeatMap",
                            "content": "class OWHeatMap(widget.OWWidget):\n    name = \"Heat Map\"\n    description = \"Plot a data matrix heatmap.\"\n    icon = \"icons/Heatmap.svg\"\n    priority = 260\n    keywords = []\n\n    class Inputs:\n        data = Input(\"Data\", Table)\n\n    class Outputs:\n        selected_data = Output(\"Selected Data\", Table, default=True)\n        annotated_data = Output(ANNOTATED_DATA_SIGNAL_NAME, Table)\n\n    settings_version = 3\n\n    settingsHandler = settings.DomainContextHandler()\n\n    # Disable clustering for inputs bigger than this\n    MaxClustering = 25000\n    # Disable cluster leaf ordering for inputs bigger than this\n    MaxOrderedClustering = 1000\n\n    threshold_low = settings.Setting(0.0)\n    threshold_high = settings.Setting(1.0)\n\n    merge_kmeans = settings.Setting(False)\n    merge_kmeans_k = settings.Setting(50)\n\n    # Display column with averages\n    averages: bool = settings.Setting(True)\n    # Display legend\n    legend: bool = settings.Setting(True)\n    # Annotations\n    #: text row annotation (row names)\n    annotation_var = settings.ContextSetting(None)\n    #: color row annotation\n    annotation_color_var = settings.ContextSetting(None)\n    # Discrete variable used to split that data/heatmaps (vertically)\n    split_by_var = settings.ContextSetting(None)\n    # Selected row/column clustering method (name)\n    col_clustering_method: str = settings.Setting(Clustering.None_.name)\n    row_clustering_method: str = settings.Setting(Clustering.None_.name)\n\n    palette_name = settings.Setting(colorpalettes.DefaultContinuousPaletteName)\n    column_label_pos: int = settings.Setting(1)\n    selected_rows: List[int] = settings.Setting(None, schema_only=True)\n\n    auto_commit = settings.Setting(True)\n\n    graph_name = \"scene\"\n\n    left_side_scrolling = True\n\n    class Information(widget.OWWidget.Information):\n        sampled = Msg(\"Data has been sampled\")\n        discrete_ignored = Msg(\"{} categorical feature{} ignored\")\n        row_clust = Msg(\"{}\")\n        col_clust = Msg(\"{}\")\n        sparse_densified = Msg(\"Showing this data may require a lot of memory\")\n\n    class Error(widget.OWWidget.Error):\n        no_continuous = Msg(\"No numeric features\")\n        not_enough_features = Msg(\"Not enough features for column clustering\")\n        not_enough_instances = Msg(\"Not enough instances for clustering\")\n        not_enough_instances_k_means = Msg(\n            \"Not enough instances for k-means merging\")\n        not_enough_memory = Msg(\"Not enough memory to show this data\")\n\n    class Warning(widget.OWWidget.Warning):\n        empty_clusters = Msg(\"Empty clusters were removed\")\n\n    def __init__(self):\n        super().__init__()\n        self.__pending_selection = self.selected_rows\n\n        # A kingdom for a save_state/restore_state\n        self.col_clustering = enum_get(\n            Clustering, self.col_clustering_method, Clustering.None_)\n        self.row_clustering = enum_get(\n            Clustering, self.row_clustering_method, Clustering.None_)\n\n        @self.settingsAboutToBePacked.connect\n        def _():\n            self.col_clustering_method = self.col_clustering.name\n            self.row_clustering_method = self.row_clustering.name\n\n        self.keep_aspect = False\n\n        #: The original data with all features (retained to\n        #: preserve the domain on the output)\n        self.input_data = None\n        #: The effective data striped of discrete features, and often\n        #: merged using k-means\n        self.data = None\n        self.effective_data = None\n        #: kmeans model used to merge rows of input_data\n        self.kmeans_model = None\n        #: merge indices derived from kmeans\n        #: a list (len==k) of int ndarray where the i-th item contains\n        #: the indices which merge the input_data into the heatmap row i\n        self.merge_indices = None\n        self.parts: Optional[Parts] = None\n        self.__rows_cache = {}\n        self.__columns_cache = {}\n\n        self.info.set_input_summary(self.info.NoInput)\n        self.info.set_output_summary(self.info.NoOutput)\n\n        # GUI definition\n        colorbox = gui.vBox(self.controlArea, \"Color\")\n        self.color_cb = gui.palette_combo_box(self.palette_name)\n        self.color_cb.currentIndexChanged.connect(self.update_color_schema)\n        colorbox.layout().addWidget(self.color_cb)\n\n        form = QFormLayout(\n            formAlignment=Qt.AlignLeft,\n            labelAlignment=Qt.AlignLeft,\n            fieldGrowthPolicy=QFormLayout.AllNonFixedFieldsGrow\n        )\n        lowslider = gui.hSlider(\n            colorbox, self, \"threshold_low\", minValue=0.0, maxValue=1.0,\n            step=0.05, ticks=True, intOnly=False,\n            createLabel=False, callback=self.update_lowslider)\n        highslider = gui.hSlider(\n            colorbox, self, \"threshold_high\", minValue=0.0, maxValue=1.0,\n            step=0.05, ticks=True, intOnly=False,\n            createLabel=False, callback=self.update_highslider)\n\n        form.addRow(\"Low:\", lowslider)\n        form.addRow(\"High:\", highslider)\n\n        colorbox.layout().addLayout(form)\n\n        mergebox = gui.vBox(self.controlArea, \"Merge\",)\n        gui.checkBox(mergebox, self, \"merge_kmeans\", \"Merge by k-means\",\n                     callback=self.__update_row_clustering)\n        ibox = gui.indentedBox(mergebox)\n        gui.spin(ibox, self, \"merge_kmeans_k\", minv=5, maxv=500,\n                 label=\"Clusters:\", keyboardTracking=False,\n                 callbackOnReturn=True, callback=self.update_merge)\n\n        cluster_box = gui.vBox(self.controlArea, \"Clustering\")\n        # Row clustering\n        self.row_cluster_cb = cb = ComboBox()\n        cb.setModel(create_list_model(ClusteringModelData, self))\n        cbselect(cb, self.row_clustering, ClusteringRole)\n        self.connect_control(\n            \"row_clustering\",\n            lambda value, cb=cb: cbselect(cb, value, ClusteringRole)\n        )\n        @cb.activated.connect\n        def _(idx, cb=cb):\n            self.set_row_clustering(cb.itemData(idx, ClusteringRole))\n\n        # Column clustering\n        self.col_cluster_cb = cb = ComboBox()\n        cb.setModel(create_list_model(ClusteringModelData, self))\n        cbselect(cb, self.col_clustering, ClusteringRole)\n        self.connect_control(\n            \"col_clustering\",\n            lambda value, cb=cb: cbselect(cb, value, ClusteringRole)\n        )\n        @cb.activated.connect\n        def _(idx, cb=cb):\n            self.set_col_clustering(cb.itemData(idx, ClusteringRole))\n\n        form = QFormLayout(\n            labelAlignment=Qt.AlignLeft, formAlignment=Qt.AlignLeft,\n            fieldGrowthPolicy=QFormLayout.AllNonFixedFieldsGrow,\n        )\n        form.addRow(\"Rows:\", self.row_cluster_cb)\n        form.addRow(\"Columns:\", self.col_cluster_cb)\n        cluster_box.layout().addLayout(form)\n        box = gui.vBox(self.controlArea, \"Split By\")\n\n        self.row_split_model = DomainModel(\n            placeholder=\"(None)\",\n            valid_types=(Orange.data.DiscreteVariable,),\n            parent=self,\n        )\n        self.row_split_cb = cb = ComboBoxSearch(\n            enabled=not self.merge_kmeans,\n            sizeAdjustPolicy=ComboBox.AdjustToMinimumContentsLengthWithIcon,\n            minimumContentsLength=14,\n            toolTip=\"Split the heatmap vertically by a categorical column\"\n        )\n        self.row_split_cb.setModel(self.row_split_model)\n        self.connect_control(\n            \"split_by_var\", lambda value, cb=cb: cbselect(cb, value)\n        )\n        self.connect_control(\n            \"merge_kmeans\", self.row_split_cb.setDisabled\n        )\n        self.split_by_var = None\n\n        self.row_split_cb.activated.connect(\n            self.__on_split_rows_activated\n        )\n        box.layout().addWidget(self.row_split_cb)\n\n        box = gui.vBox(self.controlArea, 'Annotation && Legends')\n\n        gui.checkBox(box, self, 'legend', 'Show legend',\n                     callback=self.update_legend)\n\n        gui.checkBox(box, self, 'averages', 'Stripes with averages',\n                     callback=self.update_averages_stripe)\n        annotbox = QGroupBox(\"Row Annotations\", flat=True)\n        form = QFormLayout(\n            annotbox,\n            formAlignment=Qt.AlignLeft,\n            labelAlignment=Qt.AlignLeft,\n            fieldGrowthPolicy=QFormLayout.AllNonFixedFieldsGrow\n        )\n        self.annotation_model = DomainModel(placeholder=\"(None)\")\n        self.annotation_text_cb = ComboBoxSearch(\n            minimumContentsLength=12,\n            sizeAdjustPolicy=QComboBox.AdjustToMinimumContentsLength\n        )\n        self.annotation_text_cb.setModel(self.annotation_model)\n        self.annotation_text_cb.activated.connect(self.set_annotation_var)\n        self.connect_control(\"annotation_var\", self.annotation_var_changed)\n\n        self.row_side_color_model = DomainModel(\n            order=(DomainModel.CLASSES, DomainModel.Separator,\n                   DomainModel.METAS),\n            placeholder=\"(None)\", valid_types=DomainModel.PRIMITIVE,\n            flags=Qt.ItemIsSelectable | Qt.ItemIsEnabled,\n            parent=self,\n        )\n        self.row_side_color_cb = ComboBoxSearch(\n            sizeAdjustPolicy=QComboBox.AdjustToMinimumContentsLength,\n            minimumContentsLength=12\n        )\n        self.row_side_color_cb.setModel(self.row_side_color_model)\n        self.row_side_color_cb.activated.connect(self.set_annotation_color_var)\n        self.connect_control(\"annotation_color_var\", self.annotation_color_var_changed)\n        form.addRow(\"Text\", self.annotation_text_cb)\n        form.addRow(\"Color\", self.row_side_color_cb)\n        box.layout().addWidget(annotbox)\n        posbox = gui.vBox(box, \"Column Labels Position\", addSpace=False)\n        posbox.setFlat(True)\n        cb = gui.comboBox(\n            posbox, self, \"column_label_pos\",\n            callback=self.update_column_annotations)\n        cb.setModel(create_list_model(ColumnLabelsPosData, parent=self))\n        cb.setCurrentIndex(self.column_label_pos)\n        gui.checkBox(self.controlArea, self, \"keep_aspect\",\n                     \"Keep aspect ratio\", box=\"Resize\",\n                     callback=self.__aspect_mode_changed)\n\n        gui.rubber(self.controlArea)\n        gui.auto_send(self.controlArea, self, \"auto_commit\")\n\n        # Scene with heatmap\n        class HeatmapScene(QGraphicsScene):\n            widget: Optional[HeatmapGridWidget] = None\n\n        self.scene = self.scene = HeatmapScene(parent=self)\n        self.view = GraphicsView(\n            self.scene,\n            verticalScrollBarPolicy=Qt.ScrollBarAlwaysOn,\n            horizontalScrollBarPolicy=Qt.ScrollBarAlwaysOn,\n            viewportUpdateMode=QGraphicsView.FullViewportUpdate,\n            widgetResizable=True,\n        )\n        self.view.setContextMenuPolicy(Qt.CustomContextMenu)\n        self.view.customContextMenuRequested.connect(\n            self._on_view_context_menu\n        )\n        self.mainArea.layout().addWidget(self.view)\n        self.selected_rows = []\n        self.__font_inc = QAction(\n            \"Increase Font\", self, shortcut=QKeySequence(\"ctrl+>\"))\n        self.__font_dec = QAction(\n            \"Decrease Font\", self, shortcut=QKeySequence(\"ctrl+<\"))\n        self.__font_inc.triggered.connect(lambda: self.__adjust_font_size(1))\n        self.__font_dec.triggered.connect(lambda: self.__adjust_font_size(-1))\n        if hasattr(QAction, \"setShortcutVisibleInContextMenu\"):\n            apply_all(\n                [self.__font_inc, self.__font_dec],\n                lambda a: a.setShortcutVisibleInContextMenu(True)\n            )\n        self.addActions([self.__font_inc, self.__font_dec])\n\n    @property\n    def center_palette(self):\n        palette = self.color_cb.currentData()\n        return bool(palette.flags & palette.Diverging)\n\n    @property\n    def _column_label_pos(self) -> HeatmapGridWidget.Position:\n        return ColumnLabelsPosData[self.column_label_pos][Qt.UserRole]\n\n    def annotation_color_var_changed(self, value):\n        cbselect(self.row_side_color_cb, value, Qt.EditRole)\n\n    def annotation_var_changed(self, value):\n        cbselect(self.annotation_text_cb, value, Qt.EditRole)\n\n    def set_row_clustering(self, method: Clustering) -> None:\n        assert isinstance(method, Clustering)\n        if self.row_clustering != method:\n            self.row_clustering = method\n            cbselect(self.row_cluster_cb, method, ClusteringRole)\n            self.__update_row_clustering()\n\n    def set_col_clustering(self, method: Clustering) -> None:\n        assert isinstance(method, Clustering)\n        if self.col_clustering != method:\n            self.col_clustering = method\n            cbselect(self.col_cluster_cb, method, ClusteringRole)\n            self.__update_column_clustering()\n\n    def sizeHint(self) -> QSize:\n        return super().sizeHint().expandedTo(QSize(900, 700))\n\n    def color_palette(self):\n        return self.color_cb.currentData().lookup_table()\n\n    def color_map(self) -> GradientColorMap:\n        return GradientColorMap(\n            self.color_palette(), (self.threshold_low, self.threshold_high),\n            0 if self.center_palette else None\n        )\n\n    def clear(self):\n        self.data = None\n        self.input_data = None\n        self.effective_data = None\n        self.kmeans_model = None\n        self.merge_indices = None\n        self.annotation_model.set_domain(None)\n        self.annotation_var = None\n        self.row_side_color_model.set_domain(None)\n        self.annotation_color_var = None\n        self.row_split_model.set_domain(None)\n        self.split_by_var = None\n        self.parts = None\n        self.clear_scene()\n        self.selected_rows = []\n        self.__columns_cache.clear()\n        self.__rows_cache.clear()\n        self.__update_clustering_enable_state(None)\n\n    def clear_scene(self):\n        if self.scene.widget is not None:\n            self.scene.widget.layoutDidActivate.disconnect(\n                self.__on_layout_activate\n            )\n            self.scene.widget.selectionFinished.disconnect(\n                self.on_selection_finished\n            )\n        self.scene.widget = None\n        self.scene.clear()\n\n        self.view.setSceneRect(QRectF())\n        self.view.setHeaderSceneRect(QRectF())\n        self.view.setFooterSceneRect(QRectF())\n\n    @Inputs.data\n    def set_dataset(self, data=None):\n        \"\"\"Set the input dataset to display.\"\"\"\n        self.closeContext()\n        self.clear()\n        self.clear_messages()\n        self._set_input_summary(data)\n\n        if isinstance(data, SqlTable):\n            if data.approx_len() < 4000:\n                data = Table(data)\n            else:\n                self.Information.sampled()\n                data_sample = data.sample_time(1, no_cache=True)\n                data_sample.download_data(2000, partial=True)\n                data = Table(data_sample)\n\n        if data is not None and not len(data):\n            data = None\n\n        if data is not None and sp.issparse(data.X):\n            try:\n                data = data.to_dense()\n            except MemoryError:\n                data = None\n                self.Error.not_enough_memory()\n            else:\n                self.Information.sparse_densified()\n\n        input_data = data\n\n        # Data contains no attributes or meta attributes only\n        if data is not None and len(data.domain.attributes) == 0:\n            self.Error.no_continuous()\n            input_data = data = None\n\n        # Data contains some discrete attributes which must be filtered\n        if data is not None and \\\n                any(var.is_discrete for var in data.domain.attributes):\n            ndisc = sum(var.is_discrete for var in data.domain.attributes)\n            data = data.transform(\n                Domain([var for var in data.domain.attributes\n                        if var.is_continuous],\n                       data.domain.class_vars,\n                       data.domain.metas))\n            if not data.domain.attributes:\n                self.Error.no_continuous()\n                input_data = data = None\n            else:\n                self.Information.discrete_ignored(\n                    ndisc, \"s\" if ndisc > 1 else \"\")\n\n        self.data = data\n        self.input_data = input_data\n\n        if data is not None:\n            self.annotation_model.set_domain(self.input_data.domain)\n            self.row_side_color_model.set_domain(self.input_data.domain)\n            self.annotation_var = None\n            self.annotation_color_var = None\n            self.row_split_model.set_domain(data.domain)\n            if data.domain.has_discrete_class:\n                self.split_by_var = data.domain.class_var\n            else:\n                self.split_by_var = None\n            self.openContext(self.input_data)\n            if self.split_by_var not in self.row_split_model:\n                self.split_by_var = None\n\n        self.update_heatmaps()\n        if data is not None and self.__pending_selection is not None:\n            assert self.scene.widget is not None\n            self.scene.widget.selectRows(self.__pending_selection)\n            self.selected_rows = self.__pending_selection\n            self.__pending_selection = None\n\n        self.unconditional_commit()\n\n    def _set_input_summary(self, data):\n        summary = len(data) if data else self.info.NoInput\n        details = format_summary_details(data) if data else \"\"\n        self.info.set_input_summary(summary, details)\n\n    def __on_split_rows_activated(self):\n        self.set_split_variable(self.row_split_cb.currentData(Qt.EditRole))\n\n    def set_split_variable(self, var):\n        if var != self.split_by_var:\n            self.split_by_var = var\n            self.update_heatmaps()\n\n    def update_heatmaps(self):\n        if self.data is not None:\n            self.clear_scene()\n            self.clear_messages()\n            if self.col_clustering != Clustering.None_ and \\\n                    len(self.data.domain.attributes) < 2:\n                self.Error.not_enough_features()\n            elif (self.col_clustering != Clustering.None_ or\n                  self.row_clustering != Clustering.None_) and \\\n                    len(self.data) < 2:\n                self.Error.not_enough_instances()\n            elif self.merge_kmeans and len(self.data) < 3:\n                self.Error.not_enough_instances_k_means()\n            else:\n                parts = self.construct_heatmaps(self.data, self.split_by_var)\n                self.construct_heatmaps_scene(parts, self.effective_data)\n                self.selected_rows = []\n        else:\n            self.clear()\n\n    def update_merge(self):\n        self.kmeans_model = None\n        self.merge_indices = None\n        if self.data is not None and self.merge_kmeans:\n            self.update_heatmaps()\n            self.commit()\n\n    def _make_parts(self, data, group_var=None):\n        \"\"\"\n        Make initial `Parts` for data, split by group_var, group_key\n        \"\"\"\n        if group_var is not None:\n            assert group_var.is_discrete\n            _col_data = table_column_data(data, group_var)\n            row_indices = [np.flatnonzero(_col_data == i)\n                           for i in range(len(group_var.values))]\n\n            row_groups = [RowPart(title=name, indices=ind,\n                                  cluster=None, cluster_ordered=None)\n                          for name, ind in zip(group_var.values, row_indices)]\n            if np.any(_col_data.mask):\n                row_groups.append(RowPart(\n                    title=\"N/A\", indices=np.flatnonzero(_col_data.mask),\n                    cluster=None, cluster_ordered=None\n                ))\n        else:\n            row_groups = [RowPart(title=None, indices=range(0, len(data)),\n                                  cluster=None, cluster_ordered=None)]\n\n        col_groups = [\n            ColumnPart(\n                title=None, indices=range(0, len(data.domain.attributes)),\n                domain=data.domain, cluster=None, cluster_ordered=None)\n        ]\n\n        minv, maxv = np.nanmin(data.X), np.nanmax(data.X)\n        return Parts(row_groups, col_groups, span=(minv, maxv))\n\n    def cluster_rows(self, data: Table, parts: 'Parts', ordered=False) -> 'Parts':\n        row_groups = []\n        for row in parts.rows:\n            if row.cluster is not None:\n                cluster = row.cluster\n            else:\n                cluster = None\n            if row.cluster_ordered is not None:\n                cluster_ord = row.cluster_ordered\n            else:\n                cluster_ord = None\n\n            if row.can_cluster:\n                matrix = None\n                need_dist = cluster is None or (ordered and cluster_ord is None)\n                if need_dist:\n                    subset = data[row.indices]\n                    matrix = Orange.distance.Euclidean(subset)\n\n                if cluster is None:\n                    assert len(matrix) < self.MaxClustering\n                    cluster = hierarchical.dist_matrix_clustering(\n                        matrix, linkage=hierarchical.WARD\n                    )\n                if ordered and cluster_ord is None:\n                    assert len(matrix) < self.MaxOrderedClustering\n                    cluster_ord = hierarchical.optimal_leaf_ordering(\n                        cluster, matrix,\n                    )\n            row_groups.append(row._replace(cluster=cluster, cluster_ordered=cluster_ord))\n\n        return parts._replace(rows=row_groups)\n\n    def cluster_columns(self, data, parts, ordered=False):\n        assert len(parts.columns) == 1, \"columns split is no longer supported\"\n        assert all(var.is_continuous for var in data.domain.attributes)\n\n        col0 = parts.columns[0]\n        if col0.cluster is not None:\n            cluster = col0.cluster\n        else:\n            cluster = None\n        if col0.cluster_ordered is not None:\n            cluster_ord = col0.cluster_ordered\n        else:\n            cluster_ord = None\n        need_dist = cluster is None or (ordered and cluster_ord is None)\n        matrix = None\n        if need_dist:\n            data = Orange.distance._preprocess(data)\n            matrix = np.asarray(Orange.distance.PearsonR(data, axis=0))\n            # nan values break clustering below\n            matrix = np.nan_to_num(matrix)\n\n        if cluster is None:\n            assert matrix is not None\n            assert len(matrix) < self.MaxClustering\n            cluster = hierarchical.dist_matrix_clustering(\n                matrix, linkage=hierarchical.WARD\n            )\n        if ordered and cluster_ord is None:\n            assert len(matrix) < self.MaxOrderedClustering\n            cluster_ord = hierarchical.optimal_leaf_ordering(cluster, matrix)\n\n        col_groups = [col._replace(cluster=cluster, cluster_ordered=cluster_ord)\n                      for col in parts.columns]\n        return parts._replace(columns=col_groups)\n\n    def construct_heatmaps(self, data, group_var=None) -> 'Parts':\n        if self.merge_kmeans:\n            if self.kmeans_model is None:\n                effective_data = self.input_data.transform(\n                    Orange.data.Domain(\n                        [var for var in self.input_data.domain.attributes\n                         if var.is_continuous],\n                        self.input_data.domain.class_vars,\n                        self.input_data.domain.metas))\n                nclust = min(self.merge_kmeans_k, len(effective_data) - 1)\n                self.kmeans_model = kmeans_compress(effective_data, k=nclust)\n                effective_data.domain = self.kmeans_model.domain\n                merge_indices = [np.flatnonzero(self.kmeans_model.labels == ind)\n                                 for ind in range(nclust)]\n                not_empty_indices = [i for i, x in enumerate(merge_indices)\n                                     if len(x) > 0]\n                self.merge_indices = \\\n                    [merge_indices[i] for i in not_empty_indices]\n                if len(merge_indices) != len(self.merge_indices):\n                    self.Warning.empty_clusters()\n                effective_data = Orange.data.Table(\n                    Orange.data.Domain(effective_data.domain.attributes),\n                    self.kmeans_model.centroids[not_empty_indices]\n                )\n            else:\n                effective_data = self.effective_data\n\n            group_var = None\n        else:\n            self.kmeans_model = None\n            self.merge_indices = None\n            effective_data = data\n\n        self.effective_data = effective_data\n\n        self.__update_clustering_enable_state(effective_data)\n\n        parts = self._make_parts(effective_data, group_var)\n        # Restore/update the row/columns items descriptions from cache if\n        # available\n        rows_cache_key = (group_var,\n                          self.merge_kmeans_k if self.merge_kmeans else None)\n        if rows_cache_key in self.__rows_cache:\n            parts = parts._replace(rows=self.__rows_cache[rows_cache_key].rows)\n\n        if self.row_clustering != Clustering.None_:\n            parts = self.cluster_rows(\n                effective_data, parts,\n                ordered=self.row_clustering == Clustering.OrderedClustering\n            )\n        if self.col_clustering != Clustering.None_:\n            parts = self.cluster_columns(\n                effective_data, parts,\n                ordered=self.col_clustering == Clustering.OrderedClustering\n            )\n\n        # Cache the updated parts\n        self.__rows_cache[rows_cache_key] = parts\n        return parts\n\n    def construct_heatmaps_scene(self, parts: 'Parts', data: Table) -> None:\n        _T = TypeVar(\"_T\", bound=Union[RowPart, ColumnPart])\n\n        def select_cluster(clustering: Clustering, item: _T) -> _T:\n            if clustering == Clustering.None_:\n                return item._replace(cluster=None, cluster_ordered=None)\n            elif clustering == Clustering.Clustering:\n                return item._replace(cluster=item.cluster, cluster_ordered=None)\n            elif clustering == Clustering.OrderedClustering:\n                return item._replace(cluster=item.cluster_ordered, cluster_ordered=None)\n            else:  # pragma: no cover\n                raise TypeError()\n\n        rows = [select_cluster(self.row_clustering, rowitem)\n                for rowitem in parts.rows]\n        cols = [select_cluster(self.col_clustering, colitem)\n                for colitem in parts.columns]\n        parts = Parts(columns=cols, rows=rows, span=parts.span)\n\n        self.setup_scene(parts, data)\n\n    def setup_scene(self, parts, data):\n        # type: (Parts, Table) -> None\n        widget = HeatmapGridWidget()\n        widget.setColorMap(self.color_map())\n        self.scene.addItem(widget)\n        self.scene.widget = widget\n        columns = [v.name for v in data.domain.attributes]\n        parts = HeatmapGridWidget.Parts(\n            rows=[\n                HeatmapGridWidget.RowItem(r.title, r.indices, r.cluster)\n                for r in parts.rows\n            ],\n            columns=[\n                HeatmapGridWidget.ColumnItem(c.title, c.indices, c.cluster)\n                for c in parts.columns\n            ],\n            data=data.X,\n            span=parts.span,\n            row_names=None,\n            col_names=columns,\n        )\n        widget.setHeatmaps(parts)\n        side = self.row_side_colors()\n        if side is not None:\n            widget.setRowSideColorAnnotations(side[0], side[1], name=side[2].name)\n        widget.setColumnLabelsPosition(self._column_label_pos)\n        widget.setAspectRatioMode(\n            Qt.KeepAspectRatio if self.keep_aspect else Qt.IgnoreAspectRatio\n        )\n        widget.setShowAverages(self.averages)\n        widget.setLegendVisible(self.legend)\n\n        widget.layoutDidActivate.connect(self.__on_layout_activate)\n        widget.selectionFinished.connect(self.on_selection_finished)\n\n        self.update_annotations()\n        self.view.setCentralWidget(widget)\n        self.parts = parts\n\n    def __update_scene_rects(self):\n        widget = self.scene.widget\n        if widget is None:\n            return\n        rect = widget.geometry()\n        self.scene.setSceneRect(rect)\n        self.view.setSceneRect(rect)\n        self.view.setHeaderSceneRect(widget.headerGeometry())\n        self.view.setFooterSceneRect(widget.footerGeometry())\n\n    def __on_layout_activate(self):\n        self.__update_scene_rects()\n\n    def __aspect_mode_changed(self):\n        widget = self.scene.widget\n        if widget is None:\n            return\n        widget.setAspectRatioMode(\n            Qt.KeepAspectRatio if self.keep_aspect else Qt.IgnoreAspectRatio\n        )\n        # when aspect fixed the vertical sh is fixex, when not, it can\n        # shrink vertically\n        sp = widget.sizePolicy()\n        if self.keep_aspect:\n            sp.setVerticalPolicy(QSizePolicy.Fixed)\n        else:\n            sp.setVerticalPolicy(QSizePolicy.Preferred)\n        widget.setSizePolicy(sp)\n\n    def __update_clustering_enable_state(self, data):\n        if data is not None:\n            N = len(data)\n            M = len(data.domain.attributes)\n        else:\n            N = M = 0\n\n        rc_enabled = N <= self.MaxClustering\n        rco_enabled = N <= self.MaxOrderedClustering\n        cc_enabled = M <= self.MaxClustering\n        cco_enabled = M <= self.MaxOrderedClustering\n        row_clust, col_clust = self.row_clustering, self.col_clustering\n\n        row_clust_msg = \"\"\n        col_clust_msg = \"\"\n\n        if not rco_enabled and row_clust == Clustering.OrderedClustering:\n            row_clust = Clustering.Clustering\n            row_clust_msg = \"Row cluster ordering was disabled due to the \" \\\n                            \"input matrix being to big\"\n        if not rc_enabled and row_clust == Clustering.Clustering:\n            row_clust = Clustering.None_\n            row_clust_msg = \"Row clustering was was disabled due to the \" \\\n                            \"input matrix being to big\"\n\n        if not cco_enabled and col_clust == Clustering.OrderedClustering:\n            col_clust = Clustering.Clustering\n            col_clust_msg = \"Column cluster ordering was disabled due to \" \\\n                            \"the input matrix being to big\"\n        if not cc_enabled and col_clust == Clustering.Clustering:\n            col_clust = Clustering.None_\n            col_clust_msg = \"Column clustering was disabled due to the \" \\\n                            \"input matrix being to big\"\n\n        self.col_clustering = col_clust\n        self.row_clustering = row_clust\n\n        self.Information.row_clust(row_clust_msg, shown=bool(row_clust_msg))\n        self.Information.col_clust(col_clust_msg, shown=bool(col_clust_msg))\n\n        # Disable/enable the combobox items for the clustering methods\n        def setenabled(cb: QComboBox, clu: bool, clu_op: bool):\n            model = cb.model()\n            assert isinstance(model, QStandardItemModel)\n            idx = cb.findData(Clustering.OrderedClustering, ClusteringRole)\n            assert idx != -1\n            model.item(idx).setEnabled(clu_op)\n            idx = cb.findData(Clustering.Clustering, ClusteringRole)\n            assert idx != -1\n            model.item(idx).setEnabled(clu)\n\n        setenabled(self.row_cluster_cb, rc_enabled, rco_enabled)\n        setenabled(self.col_cluster_cb, cc_enabled, cco_enabled)\n\n    def update_averages_stripe(self):\n        \"\"\"Update the visibility of the averages stripe.\n        \"\"\"\n        widget = self.scene.widget\n        if widget is not None:\n            widget.setShowAverages(self.averages)\n\n    def update_lowslider(self):\n        low, high = self.controls.threshold_low, self.controls.threshold_high\n        if low.value() >= high.value():\n            low.setSliderPosition(high.value() - 1)\n        self.update_color_schema()\n\n    def update_highslider(self):\n        low, high = self.controls.threshold_low, self.controls.threshold_high\n        if low.value() >= high.value():\n            high.setSliderPosition(low.value() + 1)\n        self.update_color_schema()\n\n    def update_color_schema(self):\n        self.palette_name = self.color_cb.currentData().name\n        w = self.scene.widget\n        if w is not None:\n            w.setColorMap(self.color_map())\n\n    def __update_column_clustering(self):\n        self.update_heatmaps()\n        self.commit()\n\n    def __update_row_clustering(self):\n        self.update_heatmaps()\n        self.commit()\n\n    def update_legend(self):\n        widget = self.scene.widget\n        if widget is not None:\n            widget.setLegendVisible(self.legend)\n\n    def row_annotation_var(self):\n        return self.annotation_var\n\n    def row_annotation_data(self):\n        var = self.row_annotation_var()\n        if var is None:\n            return None\n        return column_str_from_table(self.input_data, var)\n\n    def _merge_row_indices(self):\n        if self.merge_kmeans and self.kmeans_model is not None:\n            return self.merge_indices\n        else:\n            return None\n\n    def set_annotation_var(self, var: Union[None, Variable, int]):\n        if isinstance(var, int):\n            var = self.annotation_model[var]\n        if self.annotation_var != var:\n            self.annotation_var = var\n            self.update_annotations()\n\n    def update_annotations(self):\n        widget = self.scene.widget\n        if widget is not None:\n            annot_col = self.row_annotation_data()\n            merge_indices = self._merge_row_indices()\n            if merge_indices is not None and annot_col is not None:\n                join = lambda _1: join_elided(\", \", 42, _1, \" ({} more)\")\n                annot_col = aggregate_apply(join, annot_col, merge_indices)\n            if annot_col is not None:\n                widget.setRowLabels(annot_col)\n                widget.setRowLabelsVisible(True)\n            else:\n                widget.setRowLabelsVisible(False)\n                widget.setRowLabels(None)\n\n    def row_side_colors(self):\n        var = self.annotation_color_var\n        if var is None:\n            return None\n        column_data = column_data_from_table(self.input_data, var)\n        span = (np.nanmin(column_data), np.nanmax(column_data))\n        merges = self._merge_row_indices()\n        if merges is not None:\n            column_data = aggregate(var, column_data, merges)\n        data, colormap = self._colorize(var, column_data)\n        if var.is_continuous:\n            colormap.span = span\n        return data, colormap, var\n\n    def set_annotation_color_var(self, var: Union[None, Variable, int]):\n        \"\"\"Set the current side color annotation variable.\"\"\"\n        if isinstance(var, int):\n            var = self.row_side_color_model[var]\n        if self.annotation_color_var != var:\n            self.annotation_color_var = var\n            self.update_row_side_colors()\n\n    def update_row_side_colors(self):\n        widget = self.scene.widget\n        if widget is None:\n            return\n        colors = self.row_side_colors()\n        if colors is None:\n            widget.setRowSideColorAnnotations(None)\n        else:\n            widget.setRowSideColorAnnotations(colors[0], colors[1], colors[2].name)\n\n    def _colorize(self, var: Variable, data: np.ndarray) -> Tuple[np.ndarray, ColorMap]:\n        palette = var.palette  # type: Palette\n        colors = np.array(\n            [[c.red(), c.green(), c.blue()] for c in palette.qcolors_w_nan],\n            dtype=np.uint8,\n        )\n        if var.is_discrete:\n            mask = np.isnan(data)\n            data[mask] = -1\n            data = data.astype(int)\n            if mask.any():\n                values = (*var.values, \"N/A\")\n            else:\n                values = var.values\n                colors = colors[: -1]\n            return data, CategoricalColorMap(colors, values)\n        elif var.is_continuous:\n            cmap = GradientColorMap(colors[:-1])\n            return data, cmap\n        else:\n            raise TypeError\n\n    def update_column_annotations(self):\n        widget = self.scene.widget\n        if self.data is not None and widget is not None:\n            widget.setColumnLabelsPosition(self._column_label_pos)\n\n    def __adjust_font_size(self, diff):\n        widget = self.scene.widget\n        if widget is None:\n            return\n        curr = widget.font().pointSizeF()\n        new = curr + diff\n\n        self.__font_dec.setEnabled(new > 1.0)\n        self.__font_inc.setEnabled(new <= 32)\n        if new > 1.0:\n            font = QFont()\n            font.setPointSizeF(new)\n            widget.setFont(font)\n\n    def _on_view_context_menu(self, pos):\n        widget = self.scene.widget\n        if widget is None:\n            return\n        assert isinstance(widget, HeatmapGridWidget)\n        menu = QMenu(self.view.viewport())\n        menu.setAttribute(Qt.WA_DeleteOnClose)\n        menu.addActions(self.view.actions())\n        menu.addSeparator()\n        menu.addActions([self.__font_inc, self.__font_dec])\n        menu.addSeparator()\n        a = QAction(\"Keep aspect ratio\", menu, checkable=True)\n        a.setChecked(self.keep_aspect)\n\n        def ontoggled(state):\n            self.keep_aspect = state\n            self.__aspect_mode_changed()\n        a.toggled.connect(ontoggled)\n        menu.addAction(a)\n        menu.popup(self.view.viewport().mapToGlobal(pos))\n\n    def on_selection_finished(self):\n        if self.scene.widget is not None:\n            self.selected_rows = list(self.scene.widget.selectedRows())\n        else:\n            self.selected_rows = []\n        self.commit()\n\n    def commit(self):\n        data = None\n        indices = None\n        if self.merge_kmeans:\n            merge_indices = self.merge_indices\n        else:\n            merge_indices = None\n\n        if self.input_data is not None and self.selected_rows:\n            indices = self.selected_rows\n            if merge_indices is not None:\n                # expand merged indices\n                indices = np.hstack([merge_indices[i] for i in indices])\n\n            data = self.input_data[indices]\n\n        summary = len(data) if data else self.info.NoOutput\n        details = format_summary_details(data) if data else \"\"\n        self.info.set_output_summary(summary, details)\n        self.Outputs.selected_data.send(data)\n        self.Outputs.annotated_data.send(create_annotated_table(self.input_data, indices))\n\n    def onDeleteWidget(self):\n        self.clear()\n        super().onDeleteWidget()\n\n    def send_report(self):\n        self.report_items((\n            (\"Columns:\", \"Clustering\" if self.col_clustering else \"No sorting\"),\n            (\"Rows:\", \"Clustering\" if self.row_clustering else \"No sorting\"),\n            (\"Split:\",\n             self.split_by_var is not None and self.split_by_var.name),\n            (\"Row annotation\",\n             self.annotation_var is not None and self.annotation_var.name),\n        ))\n        self.report_plot()\n\n    @classmethod\n    def migrate_settings(cls, settings, version):\n        if version is not None and version < 3:\n            def st2cl(state: bool) -> Clustering:\n                return Clustering.OrderedClustering if state else \\\n                    Clustering.None_\n\n            rc = settings.pop(\"row_clustering\", False)\n            cc = settings.pop(\"col_clustering\", False)\n            settings[\"row_clustering_method\"] = st2cl(rc).name\n            settings[\"col_clustering_method\"] = st2cl(cc).name",
                            "ast_type_str": "ClassDef",
                            "start_lineno": 132,
                            "end_lineno": 1136,
                            "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\nimport unittest\nfrom unittest.mock import patch\n\nimport numpy as np\nfrom sklearn.exceptions import ConvergenceWarning\n\nfrom AnyQt.QtCore import Qt, QModelIndex\n\nfrom Orange.data import Table, Domain, ContinuousVariable, DiscreteVariable\nfrom Orange.preprocess import Continuize\nfrom Orange.widgets.utils import colorpalettes\nfrom Orange.widgets.utils.state_summary import format_summary_details\nfrom Orange.widgets.visualize.owheatmap import OWHeatMap, Clustering\nfrom Orange.widgets.tests.base import WidgetTest, WidgetOutputsTestMixin, datasets\n\n\ndef image_row_colors(image):\n    colors = np.full((image.height(), 3), np.nan)\n    for r in range(image.height()):\n        c = image.pixelColor(0, r)\n        colors[r] = c.red(), c.green(), c.blue()\n    return colors\n\n\nclass TestOWHeatMap(WidgetTest, WidgetOutputsTestMixin):\n    @classmethod\n    def setUpClass(cls):\n        super().setUpClass()\n        WidgetOutputsTestMixin.init(cls)\n\n        cls.housing = Table(\"housing\")\n        cls.titanic = Table(\"titanic\")\n        cls.brown_selected = Table(\"brown-selected\")\n\n        cls.signal_name = \"Data\"\n        cls.signal_data = cls.data\n\n    def setUp(self):\n        self.widget = self.create_widget(OWHeatMap)  # type: OWHeatMap\n\n    def test_input_data(self):\n        \"\"\"Check widget's data with data on the input\"\"\"\n        for data in (self.data, self.housing):\n            self.assertEqual(self.widget.data, None)\n            self.send_signal(self.widget.Inputs.data, data)\n            self.assertEqual(self.widget.data, data)\n            self.assertFalse(self.widget.Error.active)\n            self.assertFalse(self.widget.Warning.active)\n            self.assertFalse(self.widget.Information.active)\n            self.send_signal(self.widget.Inputs.data, None)\n\n        self.send_signal(self.widget.Inputs.data, self.data[:0])\n\n    def test_error_message(self):\n        self.send_signal(self.widget.Inputs.data, self.titanic)\n        self.assertTrue(self.widget.Error.active)\n        self.send_signal(self.widget.Inputs.data, self.data)\n        self.assertFalse(self.widget.Error.active)\n\n    def test_information_message(self):\n        self.widget.set_row_clustering(Clustering.OrderedClustering)\n        continuizer = Continuize()\n        cont_titanic = continuizer(self.titanic)\n        self.widget.MaxClustering = 1000\n        self.send_signal(self.widget.Inputs.data, cont_titanic)\n        self.assertTrue(self.widget.Information.active)\n        self.send_signal(self.widget.Inputs.data, self.data)\n        self.assertFalse(self.widget.Information.active)\n\n    def test_settings_changed(self):\n        self.send_signal(self.widget.Inputs.data, self.data)\n        # check output when \"Sorting Column\" setting changes\n        self._select_data()\n        self.assertIsNotNone(self.get_output(self.widget.Outputs.selected_data))\n        self.widget.set_col_clustering(Clustering.OrderedClustering)\n        self.assertIsNone(self.get_output(self.widget.Outputs.selected_data))\n        # check output when \"Sorting Row\" setting changes\n        self._select_data()\n        self.assertIsNotNone(self.get_output(self.widget.Outputs.selected_data))\n        self.widget.set_row_clustering(Clustering.OrderedClustering)\n        self.assertIsNone(self.get_output(self.widget.Outputs.selected_data))\n        # check output when \"Merge by k-means\" setting changes\n        self._select_data()\n        self.assertIsNotNone(self.get_output(self.widget.Outputs.selected_data))\n        self.widget.controls.merge_kmeans.setChecked(True)\n        self.assertIsNone(self.get_output(self.widget.Outputs.selected_data))\n\n    def _select_data(self):\n        selected_indices = list(range(10, 31))\n        self.widget.scene.widget.selectRows(selected_indices)\n        self.widget.on_selection_finished()\n        return selected_indices\n\n    def test_not_enough_data_settings_changed(self):\n        \"\"\"Check widget for dataset with one feature or for one instance\"\"\"\n        # Test tests that widget handles this exact condition\n        warnings.filterwarnings(\"ignore\", \"Number of distinct clusters\",\n                                ConvergenceWarning)\n\n        msg = self.widget.Error\n        clusterings = (Clustering.None_, Clustering.Clustering,\n                       Clustering.OrderedClustering)\n        for kmeans_checked in (False, True):\n            self.widget.controls.merge_kmeans.setChecked(kmeans_checked)\n            for col_clust in clusterings:\n                self.widget.set_col_clustering(col_clust)\n                self.send_signal(self.widget.Inputs.data, None)\n                self.send_signal(self.widget.Inputs.data, self.data[:, 0])\n                if col_clust != Clustering.None_:\n                    self.assertTrue(msg.not_enough_features.is_shown())\n                for row_clust in clusterings:\n                    self.widget.set_row_clustering(row_clust)\n                    self.send_signal(self.widget.Inputs.data, None)\n                    self.send_signal(self.widget.Inputs.data, self.data[0:1])\n                    if row_clust != Clustering.None_:\n                        self.assertTrue(msg.not_enough_instances.is_shown())\n                    elif kmeans_checked and row_clust != Clustering.None_:\n                        self.assertTrue(msg.not_enough_instances_k_means.is_shown())\n            self.send_signal(self.widget.Inputs.data, None)\n            self.assertFalse(msg.not_enough_features.is_shown())\n            self.assertFalse(msg.not_enough_instances.is_shown())\n            self.assertFalse(msg.not_enough_instances_k_means.is_shown())\n\n    def test_color_low_high(self):\n        \"\"\"\n        Prevent horizontal sliders to set Low >= High.\n        GH-2025\n        \"\"\"\n        self.widget.controls.threshold_low.setValue(4)\n        self.widget.controls.threshold_high.setValue(2)\n        self.assertGreater(self.widget.threshold_high, self.widget.threshold_low)\n\n    def test_data_column_nans(self):\n        \"\"\"\n        Send data with one column with all values set to NaN.\n        ValueError should not be thrown (Invalid number of variable columns)\n        That column is now suppose to be removed in a table array and\n        in a domain as well.\n        GH-2057\n        \"\"\"\n        table = datasets.data_one_column_nans()\n        self.widget.controls.merge_kmeans.setChecked(True)\n        self.send_signal(self.widget.Inputs.data, table)\n\n    def test_cluster_column_on_all_zero_column(self):\n        # Pearson distance used for clustering of columns does not\n        # handle all zero columns well\n        iris = Table(\"iris\")\n        iris[:, 0] = 0\n\n        self.widget.col_clustering = True\n        self.widget.set_dataset(iris)\n\n    def test_empty_clusters(self):\n        \"\"\"Test if empty clusters are not displayed and warning is shown\"\"\"\n        data = np.array([1, 1, 1, 2, 2, 2, 3, 3, 3])\n\n        table = Table.from_numpy(Domain([ContinuousVariable(\"y\")]),\n                                 data.reshape((9, 1)))\n        self.widget.controls.merge_kmeans.setChecked(True)\n\n        # Test tests that widget handles this exact condition\n        warnings.filterwarnings(\"ignore\", \"Number of distinct clusters\",\n                                ConvergenceWarning)\n        self.send_signal(self.widget.Inputs.data, table)\n\n        self.assertTrue(self.widget.Warning.empty_clusters.is_shown())\n        self.assertEqual(len(self.widget.merge_indices), 3)\n\n    def test_use_enough_colors(self):\n        # Before 201906 thresholds modified the palette and decreased\n        # the number of colors used.\n        data = np.arange(1000).reshape(-1, 1)\n        table = Table.from_numpy(Domain([ContinuousVariable(\"y\")]), data)\n        self.send_signal(self.widget.Inputs.data, table)\n        self.widget.threshold_high = 0.05\n        self.widget.update_color_schema()\n        heatmap_widget = self.widget.scene.widget.heatmap_widget_grid[0][0]\n        image = heatmap_widget.pixmap().toImage()\n        colors = image_row_colors(image)\n        unique_colors = len(np.unique(colors, axis=0))\n        self.assertLessEqual(len(data)*self.widget.threshold_low, unique_colors)\n\n    def test_cls_with_single_instance(self):\n        table = Table(Domain([ContinuousVariable(\"c1\")],\n                             [DiscreteVariable(\"c2\", values=(\"a\", \"b\"))]),\n                      np.array([[1], [2], [3]]), np.array([[0], [0], [1]]))\n        self.send_signal(self.widget.Inputs.data, table)\n        self.widget.set_row_clustering(Clustering.Clustering)\n\n    def test_unconditional_commit_on_new_signal(self):\n        with patch.object(self.widget, 'unconditional_commit') as commit:\n            self.widget.auto_commit = False\n            commit.reset_mock()\n            self.send_signal(self.widget.Inputs.data, self.titanic)\n            commit.assert_called()\n\n    def test_saved_selection(self):\n        iris = Table(\"iris\")\n\n        self.send_signal(self.widget.Inputs.data, iris)\n        selected_indices = list(range(10, 31))\n        self.widget.scene.widget.selectRows(selected_indices)\n        self.widget.on_selection_finished()\n        settings = self.widget.settingsHandler.pack_data(self.widget)\n\n        w = self.create_widget(OWHeatMap, stored_settings=settings)\n        self.send_signal(w.Inputs.data, iris, widget=w)\n        self.assertEqual(len(self.get_output(w.Outputs.selected_data)), 21)\n\n    def test_set_split_var(self):\n        data = self.brown_selected[::3]\n        w = self.widget\n        self.send_signal(self.widget.Inputs.data, data, widget=w)\n        self.assertIs(w.split_by_var, data.domain.class_var)\n        self.assertEqual(len(w.parts.rows),\n                         len(data.domain.class_var.values))\n        w.set_split_variable(None)\n        self.assertIs(w.split_by_var, None)\n        self.assertEqual(len(w.parts.rows), 1)\n\n    def test_set_split_var_missing(self):\n        data = self.brown_selected[::3].copy()\n        data.Y[::5] = np.nan\n        w = self.widget\n        self.send_signal(self.widget.Inputs.data, data, widget=w)\n        self.assertIs(w.split_by_var, data.domain.class_var)\n        self.assertEqual(len(w.parts.rows),\n                         len(data.domain.class_var.values))\n        self.assertTrue(w.Warning.missing_split_values.is_shown())\n        w.set_split_variable(None)\n        self.assertFalse(w.Warning.missing_split_values.is_shown())\n\n    def test_palette_centering(self):\n        data = np.arange(2).reshape(-1, 1)\n        table = Table.from_numpy(Domain([ContinuousVariable(\"y\")]), data)\n        self.send_signal(self.widget.Inputs.data, table)\n\n        self.widget.color_palette = lambda: \\\n            colorpalettes.ContinuousPalette.from_colors(\n                (0, 255, 0), (255, 0, 0), (0, 0, 0)).lookup_table()\n\n        desired_uncentered = [[0, 255, 0],\n                              [255, 0, 0]]\n\n        desired_centered = [[0, 0, 0],\n                            [255, 0, 0]]\n\n        for center, desired in [(False, desired_uncentered), (True, desired_centered)]:\n            with patch.object(OWHeatMap, \"center_palette\", center):\n                self.widget.update_color_schema()\n                heatmap_widget = self.widget.scene.widget.heatmap_widget_grid[0][0]\n                image = heatmap_widget.pixmap().toImage()\n                colors = image_row_colors(image)\n                np.testing.assert_almost_equal(colors, desired)\n\n    def test_palette_center(self):\n        widget = self.widget\n        model = widget.color_cb.model()\n        for idx in range(model.rowCount(QModelIndex())):\n            palette = model.data(model.index(idx, 0), Qt.UserRole)\n            if palette is None:\n                continue\n            widget.color_cb.setCurrentIndex(idx)\n            self.assertEqual(widget.center_palette,\n                             bool(palette.flags & palette.Diverging))\n\n    def test_migrate_settings_v3(self):\n        w = self.create_widget(\n            OWHeatMap, stored_settings={\n                \"row_clustering\": False,\n                \"col_clustering\": True,\n            }\n        )\n        self.assertEqual(w.row_clustering, Clustering.None_)\n        self.assertEqual(w.col_clustering, Clustering.OrderedClustering)\n\n    def test_row_color_annotations(self):\n        widget = self.widget\n        data = Table(\"brown-selected\")[::5]\n        self.send_signal(widget.Inputs.data, data, widget=widget)\n        widget.set_annotation_color_var(data.domain[\"function\"])\n        self.assertTrue(widget.scene.widget.right_side_colors[0].isVisible())\n        widget.set_annotation_color_var(None)\n        self.assertFalse(widget.scene.widget.right_side_colors[0].isVisible())\n\n    def test_summary(self):\n        \"\"\"Check if status bar is updated when data is received\"\"\"\n        info = self.widget.info\n        no_input, no_output = \"No data on input\", \"No data on output\"\n\n        data = self.housing\n        self.send_signal(self.widget.Inputs.data, data)\n        summary, details = f\"{len(data)}\", format_summary_details(data)\n        self.assertEqual(info._StateInfo__input_summary.brief, summary)\n        self.assertEqual(info._StateInfo__input_summary.details, details)\n        self.assertEqual(info._StateInfo__output_summary.brief, \"\")\n        self.assertEqual(info._StateInfo__output_summary.details, no_output)\n        self._select_data()\n        output = self.get_output(self.widget.Outputs.selected_data)\n        summary, details = f\"{len(output)}\", format_summary_details(output)\n        self.assertEqual(info._StateInfo__output_summary.brief, summary)\n        self.assertEqual(info._StateInfo__output_summary.details, details)\n\n        self.send_signal(self.widget.Inputs.data, None)\n        self.assertEqual(info._StateInfo__input_summary.brief, \"\")\n        self.assertEqual(info._StateInfo__input_summary.details, no_input)\n        self.assertEqual(info._StateInfo__output_summary.brief, \"\")\n        self.assertEqual(info._StateInfo__output_summary.details, no_output)\n\n\nif __name__ == \"__main__\":\n    unittest.main()\n",
            "new_file_content": "# Test methods with long descriptive names can omit docstrings\n# pylint: disable=missing-docstring, protected-access\nimport warnings\nimport unittest\nfrom unittest.mock import patch\n\nimport numpy as np\nfrom sklearn.exceptions import ConvergenceWarning\n\nfrom AnyQt.QtCore import Qt, QModelIndex\n\nfrom Orange.data import Table, Domain, ContinuousVariable, DiscreteVariable\nfrom Orange.preprocess import Continuize\nfrom Orange.widgets.utils import colorpalettes\nfrom Orange.widgets.utils.state_summary import format_summary_details\nfrom Orange.widgets.visualize.owheatmap import OWHeatMap, Clustering\nfrom Orange.widgets.tests.base import WidgetTest, WidgetOutputsTestMixin, datasets\n\n\ndef image_row_colors(image):\n    colors = np.full((image.height(), 3), np.nan)\n    for r in range(image.height()):\n        c = image.pixelColor(0, r)\n        colors[r] = c.red(), c.green(), c.blue()\n    return colors\n\n\nclass TestOWHeatMap(WidgetTest, WidgetOutputsTestMixin):\n    @classmethod\n    def setUpClass(cls):\n        super().setUpClass()\n        WidgetOutputsTestMixin.init(cls)\n\n        cls.housing = Table(\"housing\")\n        cls.titanic = Table(\"titanic\")\n        cls.brown_selected = Table(\"brown-selected\")\n\n        cls.signal_name = \"Data\"\n        cls.signal_data = cls.data\n\n    def setUp(self):\n        self.widget = self.create_widget(OWHeatMap)  # type: OWHeatMap\n\n    def test_input_data(self):\n        \"\"\"Check widget's data with data on the input\"\"\"\n        for data in (self.data, self.housing):\n            self.assertEqual(self.widget.data, None)\n            self.send_signal(self.widget.Inputs.data, data)\n            self.assertEqual(self.widget.data, data)\n            self.assertFalse(self.widget.Error.active)\n            self.assertFalse(self.widget.Warning.active)\n            self.assertFalse(self.widget.Information.active)\n            self.send_signal(self.widget.Inputs.data, None)\n\n        self.send_signal(self.widget.Inputs.data, self.data[:0])\n\n    def test_error_message(self):\n        self.send_signal(self.widget.Inputs.data, self.titanic)\n        self.assertTrue(self.widget.Error.active)\n        self.send_signal(self.widget.Inputs.data, self.data)\n        self.assertFalse(self.widget.Error.active)\n\n    def test_information_message(self):\n        self.widget.set_row_clustering(Clustering.OrderedClustering)\n        continuizer = Continuize()\n        cont_titanic = continuizer(self.titanic)\n        self.widget.MaxClustering = 1000\n        self.send_signal(self.widget.Inputs.data, cont_titanic)\n        self.assertTrue(self.widget.Information.active)\n        self.send_signal(self.widget.Inputs.data, self.data)\n        self.assertFalse(self.widget.Information.active)\n\n    def test_settings_changed(self):\n        self.send_signal(self.widget.Inputs.data, self.data)\n        # check output when \"Sorting Column\" setting changes\n        self._select_data()\n        self.assertIsNotNone(self.get_output(self.widget.Outputs.selected_data))\n        self.widget.set_col_clustering(Clustering.OrderedClustering)\n        self.assertIsNone(self.get_output(self.widget.Outputs.selected_data))\n        # check output when \"Sorting Row\" setting changes\n        self._select_data()\n        self.assertIsNotNone(self.get_output(self.widget.Outputs.selected_data))\n        self.widget.set_row_clustering(Clustering.OrderedClustering)\n        self.assertIsNone(self.get_output(self.widget.Outputs.selected_data))\n        # check output when \"Merge by k-means\" setting changes\n        self._select_data()\n        self.assertIsNotNone(self.get_output(self.widget.Outputs.selected_data))\n        self.widget.controls.merge_kmeans.setChecked(True)\n        self.assertIsNone(self.get_output(self.widget.Outputs.selected_data))\n\n    def _select_data(self):\n        selected_indices = list(range(10, 31))\n        self.widget.scene.widget.selectRows(selected_indices)\n        self.widget.on_selection_finished()\n        return selected_indices\n\n    def test_not_enough_data_settings_changed(self):\n        \"\"\"Check widget for dataset with one feature or for one instance\"\"\"\n        # Test tests that widget handles this exact condition\n        warnings.filterwarnings(\"ignore\", \"Number of distinct clusters\",\n                                ConvergenceWarning)\n\n        msg = self.widget.Error\n        clusterings = (Clustering.None_, Clustering.Clustering,\n                       Clustering.OrderedClustering)\n        for kmeans_checked in (False, True):\n            self.widget.controls.merge_kmeans.setChecked(kmeans_checked)\n            for col_clust in clusterings:\n                self.widget.set_col_clustering(col_clust)\n                self.send_signal(self.widget.Inputs.data, None)\n                self.send_signal(self.widget.Inputs.data, self.data[:, 0])\n                if col_clust != Clustering.None_:\n                    self.assertTrue(msg.not_enough_features.is_shown())\n                for row_clust in clusterings:\n                    self.widget.set_row_clustering(row_clust)\n                    self.send_signal(self.widget.Inputs.data, None)\n                    self.send_signal(self.widget.Inputs.data, self.data[0:1])\n                    if row_clust != Clustering.None_:\n                        self.assertTrue(msg.not_enough_instances.is_shown())\n                    elif kmeans_checked and row_clust != Clustering.None_:\n                        self.assertTrue(msg.not_enough_instances_k_means.is_shown())\n            self.send_signal(self.widget.Inputs.data, None)\n            self.assertFalse(msg.not_enough_features.is_shown())\n            self.assertFalse(msg.not_enough_instances.is_shown())\n            self.assertFalse(msg.not_enough_instances_k_means.is_shown())\n\n    def test_color_low_high(self):\n        \"\"\"\n        Prevent horizontal sliders to set Low >= High.\n        GH-2025\n        \"\"\"\n        self.widget.controls.threshold_low.setValue(4)\n        self.widget.controls.threshold_high.setValue(2)\n        self.assertGreater(self.widget.threshold_high, self.widget.threshold_low)\n\n    def test_data_column_nans(self):\n        \"\"\"\n        Send data with one column with all values set to NaN.\n        ValueError should not be thrown (Invalid number of variable columns)\n        That column is now suppose to be removed in a table array and\n        in a domain as well.\n        GH-2057\n        \"\"\"\n        table = datasets.data_one_column_nans()\n        self.widget.controls.merge_kmeans.setChecked(True)\n        self.send_signal(self.widget.Inputs.data, table)\n\n    def test_cluster_column_on_all_zero_column(self):\n        # Pearson distance used for clustering of columns does not\n        # handle all zero columns well\n        iris = Table(\"iris\")\n        iris[:, 0] = 0\n\n        self.widget.col_clustering = True\n        self.widget.set_dataset(iris)\n\n    def test_empty_clusters(self):\n        \"\"\"Test if empty clusters are not displayed and warning is shown\"\"\"\n        data = np.array([1, 1, 1, 2, 2, 2, 3, 3, 3])\n\n        table = Table.from_numpy(Domain([ContinuousVariable(\"y\")]),\n                                 data.reshape((9, 1)))\n        self.widget.controls.merge_kmeans.setChecked(True)\n\n        # Test tests that widget handles this exact condition\n        warnings.filterwarnings(\"ignore\", \"Number of distinct clusters\",\n                                ConvergenceWarning)\n        self.send_signal(self.widget.Inputs.data, table)\n\n        self.assertTrue(self.widget.Warning.empty_clusters.is_shown())\n        self.assertEqual(len(self.widget.merge_indices), 3)\n\n    def test_use_enough_colors(self):\n        # Before 201906 thresholds modified the palette and decreased\n        # the number of colors used.\n        data = np.arange(1000).reshape(-1, 1)\n        table = Table.from_numpy(Domain([ContinuousVariable(\"y\")]), data)\n        self.send_signal(self.widget.Inputs.data, table)\n        self.widget.threshold_high = 0.05\n        self.widget.update_color_schema()\n        heatmap_widget = self.widget.scene.widget.heatmap_widget_grid[0][0]\n        image = heatmap_widget.pixmap().toImage()\n        colors = image_row_colors(image)\n        unique_colors = len(np.unique(colors, axis=0))\n        self.assertLessEqual(len(data)*self.widget.threshold_low, unique_colors)\n\n    def test_cls_with_single_instance(self):\n        table = Table(Domain([ContinuousVariable(\"c1\")],\n                             [DiscreteVariable(\"c2\", values=(\"a\", \"b\"))]),\n                      np.array([[1], [2], [3]]), np.array([[0], [0], [1]]))\n        self.send_signal(self.widget.Inputs.data, table)\n        self.widget.set_row_clustering(Clustering.Clustering)\n\n    def test_unconditional_commit_on_new_signal(self):\n        with patch.object(self.widget, 'unconditional_commit') as commit:\n            self.widget.auto_commit = False\n            commit.reset_mock()\n            self.send_signal(self.widget.Inputs.data, self.titanic)\n            commit.assert_called()\n\n    def test_saved_selection(self):\n        iris = Table(\"iris\")\n\n        self.send_signal(self.widget.Inputs.data, iris)\n        selected_indices = list(range(10, 31))\n        self.widget.scene.widget.selectRows(selected_indices)\n        self.widget.on_selection_finished()\n        settings = self.widget.settingsHandler.pack_data(self.widget)\n\n        w = self.create_widget(OWHeatMap, stored_settings=settings)\n        self.send_signal(w.Inputs.data, iris, widget=w)\n        self.assertEqual(len(self.get_output(w.Outputs.selected_data)), 21)\n\n    def test_set_split_var(self):\n        data = self.brown_selected[::3]\n        w = self.widget\n        self.send_signal(self.widget.Inputs.data, data, widget=w)\n        self.assertIs(w.split_by_var, data.domain.class_var)\n        self.assertEqual(len(w.parts.rows),\n                         len(data.domain.class_var.values))\n        w.set_split_variable(None)\n        self.assertIs(w.split_by_var, None)\n        self.assertEqual(len(w.parts.rows), 1)\n\n    def test_set_split_var_missing(self):\n        data = self.brown_selected[::3].copy()\n        data.Y[::5] = np.nan\n        w = self.widget\n        self.send_signal(self.widget.Inputs.data, data, widget=w)\n        self.assertIs(w.split_by_var, data.domain.class_var)\n        self.assertEqual(len(w.parts.rows),\n                         len(data.domain.class_var.values) + 1)\n\n    def test_palette_centering(self):\n        data = np.arange(2).reshape(-1, 1)\n        table = Table.from_numpy(Domain([ContinuousVariable(\"y\")]), data)\n        self.send_signal(self.widget.Inputs.data, table)\n\n        self.widget.color_palette = lambda: \\\n            colorpalettes.ContinuousPalette.from_colors(\n                (0, 255, 0), (255, 0, 0), (0, 0, 0)).lookup_table()\n\n        desired_uncentered = [[0, 255, 0],\n                              [255, 0, 0]]\n\n        desired_centered = [[0, 0, 0],\n                            [255, 0, 0]]\n\n        for center, desired in [(False, desired_uncentered), (True, desired_centered)]:\n            with patch.object(OWHeatMap, \"center_palette\", center):\n                self.widget.update_color_schema()\n                heatmap_widget = self.widget.scene.widget.heatmap_widget_grid[0][0]\n                image = heatmap_widget.pixmap().toImage()\n                colors = image_row_colors(image)\n                np.testing.assert_almost_equal(colors, desired)\n\n    def test_palette_center(self):\n        widget = self.widget\n        model = widget.color_cb.model()\n        for idx in range(model.rowCount(QModelIndex())):\n            palette = model.data(model.index(idx, 0), Qt.UserRole)\n            if palette is None:\n                continue\n            widget.color_cb.setCurrentIndex(idx)\n            self.assertEqual(widget.center_palette,\n                             bool(palette.flags & palette.Diverging))\n\n    def test_migrate_settings_v3(self):\n        w = self.create_widget(\n            OWHeatMap, stored_settings={\n                \"row_clustering\": False,\n                \"col_clustering\": True,\n            }\n        )\n        self.assertEqual(w.row_clustering, Clustering.None_)\n        self.assertEqual(w.col_clustering, Clustering.OrderedClustering)\n\n    def test_row_color_annotations(self):\n        widget = self.widget\n        data = Table(\"brown-selected\")[::5]\n        self.send_signal(widget.Inputs.data, data, widget=widget)\n        widget.set_annotation_color_var(data.domain[\"function\"])\n        self.assertTrue(widget.scene.widget.right_side_colors[0].isVisible())\n        widget.set_annotation_color_var(None)\n        self.assertFalse(widget.scene.widget.right_side_colors[0].isVisible())\n\n    def test_summary(self):\n        \"\"\"Check if status bar is updated when data is received\"\"\"\n        info = self.widget.info\n        no_input, no_output = \"No data on input\", \"No data on output\"\n\n        data = self.housing\n        self.send_signal(self.widget.Inputs.data, data)\n        summary, details = f\"{len(data)}\", format_summary_details(data)\n        self.assertEqual(info._StateInfo__input_summary.brief, summary)\n        self.assertEqual(info._StateInfo__input_summary.details, details)\n        self.assertEqual(info._StateInfo__output_summary.brief, \"\")\n        self.assertEqual(info._StateInfo__output_summary.details, no_output)\n        self._select_data()\n        output = self.get_output(self.widget.Outputs.selected_data)\n        summary, details = f\"{len(output)}\", format_summary_details(output)\n        self.assertEqual(info._StateInfo__output_summary.brief, summary)\n        self.assertEqual(info._StateInfo__output_summary.details, details)\n\n        self.send_signal(self.widget.Inputs.data, None)\n        self.assertEqual(info._StateInfo__input_summary.brief, \"\")\n        self.assertEqual(info._StateInfo__input_summary.details, no_input)\n        self.assertEqual(info._StateInfo__output_summary.brief, \"\")\n        self.assertEqual(info._StateInfo__output_summary.details, no_output)\n\n\nif __name__ == \"__main__\":\n    unittest.main()\n",
            "header": {
                "file": {
                    "path": "Orange/widgets/visualize/tests/test_owheatmap.py"
                },
                "misc_line": null
            },
            "index_line": {
                "old_commit_hash": "98eee49fb",
                "new_commit_hash": "7aba80d85",
                "mode": "100644"
            },
            "is_binary_file": false,
            "binary_line": null,
            "minus_file": {
                "path": "a/Orange/widgets/visualize/tests/test_owheatmap.py"
            },
            "plus_file": {
                "path": "b/Orange/widgets/visualize/tests/test_owheatmap.py"
            },
            "hunks": [
                {
                    "descriptor": {
                        "old_range": {
                            "start": 229,
                            "length": 10
                        },
                        "new_range": {
                            "start": 229,
                            "length": 7
                        },
                        "section": "class TestOWHeatMap(WidgetTest, WidgetOutputsTestMixin):"
                    },
                    "line_group": {
                        "all_lines": [
                            {
                                "content": "        self.send_signal(self.widget.Inputs.data, data, widget=w)",
                                "type": "context"
                            },
                            {
                                "content": "        self.assertIs(w.split_by_var, data.domain.class_var)",
                                "type": "context"
                            },
                            {
                                "content": "        self.assertEqual(len(w.parts.rows),",
                                "type": "context"
                            },
                            {
                                "content": "                         len(data.domain.class_var.values))",
                                "type": "deleted"
                            },
                            {
                                "content": "        self.assertTrue(w.Warning.missing_split_values.is_shown())",
                                "type": "deleted"
                            },
                            {
                                "content": "        w.set_split_variable(None)",
                                "type": "deleted"
                            },
                            {
                                "content": "        self.assertFalse(w.Warning.missing_split_values.is_shown())",
                                "type": "deleted"
                            },
                            {
                                "content": "                         len(data.domain.class_var.values) + 1)",
                                "type": "added"
                            },
                            {
                                "content": "",
                                "type": "context"
                            },
                            {
                                "content": "    def test_palette_centering(self):",
                                "type": "context"
                            },
                            {
                                "content": "        data = np.arange(2).reshape(-1, 1)",
                                "type": "context"
                            },
                            {
                                "content": "",
                                "type": "context"
                            }
                        ]
                    },
                    "modified_entities": [
                        {
                            "file_name": "Orange/widgets/visualize/tests/test_owheatmap.py",
                            "type": "method",
                            "name": "TestOWHeatMap.test_set_split_var_missing",
                            "content": "class TestOWHeatMap(WidgetTest, WidgetOutputsTestMixin):\n    @classmethod\n    def setUpClass(cls):\n        super().setUpClass()\n        WidgetOutputsTestMixin.init(cls)\n\n        cls.housing = Table(\"housing\")\n        cls.titanic = Table(\"titanic\")\n        cls.brown_selected = Table(\"brown-selected\")\n\n        cls.signal_name = \"Data\"\n        cls.signal_data = cls.data\n\n    def setUp(self):\n        self.widget = self.create_widget(OWHeatMap)  # type: OWHeatMap\n\n    def test_input_data(self):\n        \"\"\"Check widget's data with data on the input\"\"\"\n        for data in (self.data, self.housing):\n            self.assertEqual(self.widget.data, None)\n            self.send_signal(self.widget.Inputs.data, data)\n            self.assertEqual(self.widget.data, data)\n            self.assertFalse(self.widget.Error.active)\n            self.assertFalse(self.widget.Warning.active)\n            self.assertFalse(self.widget.Information.active)\n            self.send_signal(self.widget.Inputs.data, None)\n\n        self.send_signal(self.widget.Inputs.data, self.data[:0])\n\n    def test_error_message(self):\n        self.send_signal(self.widget.Inputs.data, self.titanic)\n        self.assertTrue(self.widget.Error.active)\n        self.send_signal(self.widget.Inputs.data, self.data)\n        self.assertFalse(self.widget.Error.active)\n\n    def test_information_message(self):\n        self.widget.set_row_clustering(Clustering.OrderedClustering)\n        continuizer = Continuize()\n        cont_titanic = continuizer(self.titanic)\n        self.widget.MaxClustering = 1000\n        self.send_signal(self.widget.Inputs.data, cont_titanic)\n        self.assertTrue(self.widget.Information.active)\n        self.send_signal(self.widget.Inputs.data, self.data)\n        self.assertFalse(self.widget.Information.active)\n\n    def test_settings_changed(self):\n        self.send_signal(self.widget.Inputs.data, self.data)\n        # check output when \"Sorting Column\" setting changes\n        self._select_data()\n        self.assertIsNotNone(self.get_output(self.widget.Outputs.selected_data))\n        self.widget.set_col_clustering(Clustering.OrderedClustering)\n        self.assertIsNone(self.get_output(self.widget.Outputs.selected_data))\n        # check output when \"Sorting Row\" setting changes\n        self._select_data()\n        self.assertIsNotNone(self.get_output(self.widget.Outputs.selected_data))\n        self.widget.set_row_clustering(Clustering.OrderedClustering)\n        self.assertIsNone(self.get_output(self.widget.Outputs.selected_data))\n        # check output when \"Merge by k-means\" setting changes\n        self._select_data()\n        self.assertIsNotNone(self.get_output(self.widget.Outputs.selected_data))\n        self.widget.controls.merge_kmeans.setChecked(True)\n        self.assertIsNone(self.get_output(self.widget.Outputs.selected_data))\n\n    def _select_data(self):\n        selected_indices = list(range(10, 31))\n        self.widget.scene.widget.selectRows(selected_indices)\n        self.widget.on_selection_finished()\n        return selected_indices\n\n    def test_not_enough_data_settings_changed(self):\n        \"\"\"Check widget for dataset with one feature or for one instance\"\"\"\n        # Test tests that widget handles this exact condition\n        warnings.filterwarnings(\"ignore\", \"Number of distinct clusters\",\n                                ConvergenceWarning)\n\n        msg = self.widget.Error\n        clusterings = (Clustering.None_, Clustering.Clustering,\n                       Clustering.OrderedClustering)\n        for kmeans_checked in (False, True):\n            self.widget.controls.merge_kmeans.setChecked(kmeans_checked)\n            for col_clust in clusterings:\n                self.widget.set_col_clustering(col_clust)\n                self.send_signal(self.widget.Inputs.data, None)\n                self.send_signal(self.widget.Inputs.data, self.data[:, 0])\n                if col_clust != Clustering.None_:\n                    self.assertTrue(msg.not_enough_features.is_shown())\n                for row_clust in clusterings:\n                    self.widget.set_row_clustering(row_clust)\n                    self.send_signal(self.widget.Inputs.data, None)\n                    self.send_signal(self.widget.Inputs.data, self.data[0:1])\n                    if row_clust != Clustering.None_:\n                        self.assertTrue(msg.not_enough_instances.is_shown())\n                    elif kmeans_checked and row_clust != Clustering.None_:\n                        self.assertTrue(msg.not_enough_instances_k_means.is_shown())\n            self.send_signal(self.widget.Inputs.data, None)\n            self.assertFalse(msg.not_enough_features.is_shown())\n            self.assertFalse(msg.not_enough_instances.is_shown())\n            self.assertFalse(msg.not_enough_instances_k_means.is_shown())\n\n    def test_color_low_high(self):\n        \"\"\"\n        Prevent horizontal sliders to set Low >= High.\n        GH-2025\n        \"\"\"\n        self.widget.controls.threshold_low.setValue(4)\n        self.widget.controls.threshold_high.setValue(2)\n        self.assertGreater(self.widget.threshold_high, self.widget.threshold_low)\n\n    def test_data_column_nans(self):\n        \"\"\"\n        Send data with one column with all values set to NaN.\n        ValueError should not be thrown (Invalid number of variable columns)\n        That column is now suppose to be removed in a table array and\n        in a domain as well.\n        GH-2057\n        \"\"\"\n        table = datasets.data_one_column_nans()\n        self.widget.controls.merge_kmeans.setChecked(True)\n        self.send_signal(self.widget.Inputs.data, table)\n\n    def test_cluster_column_on_all_zero_column(self):\n        # Pearson distance used for clustering of columns does not\n        # handle all zero columns well\n        iris = Table(\"iris\")\n        iris[:, 0] = 0\n\n        self.widget.col_clustering = True\n        self.widget.set_dataset(iris)\n\n    def test_empty_clusters(self):\n        \"\"\"Test if empty clusters are not displayed and warning is shown\"\"\"\n        data = np.array([1, 1, 1, 2, 2, 2, 3, 3, 3])\n\n        table = Table.from_numpy(Domain([ContinuousVariable(\"y\")]),\n                                 data.reshape((9, 1)))\n        self.widget.controls.merge_kmeans.setChecked(True)\n\n        # Test tests that widget handles this exact condition\n        warnings.filterwarnings(\"ignore\", \"Number of distinct clusters\",\n                                ConvergenceWarning)\n        self.send_signal(self.widget.Inputs.data, table)\n\n        self.assertTrue(self.widget.Warning.empty_clusters.is_shown())\n        self.assertEqual(len(self.widget.merge_indices), 3)\n\n    def test_use_enough_colors(self):\n        # Before 201906 thresholds modified the palette and decreased\n        # the number of colors used.\n        data = np.arange(1000).reshape(-1, 1)\n        table = Table.from_numpy(Domain([ContinuousVariable(\"y\")]), data)\n        self.send_signal(self.widget.Inputs.data, table)\n        self.widget.threshold_high = 0.05\n        self.widget.update_color_schema()\n        heatmap_widget = self.widget.scene.widget.heatmap_widget_grid[0][0]\n        image = heatmap_widget.pixmap().toImage()\n        colors = image_row_colors(image)\n        unique_colors = len(np.unique(colors, axis=0))\n        self.assertLessEqual(len(data)*self.widget.threshold_low, unique_colors)\n\n    def test_cls_with_single_instance(self):\n        table = Table(Domain([ContinuousVariable(\"c1\")],\n                             [DiscreteVariable(\"c2\", values=(\"a\", \"b\"))]),\n                      np.array([[1], [2], [3]]), np.array([[0], [0], [1]]))\n        self.send_signal(self.widget.Inputs.data, table)\n        self.widget.set_row_clustering(Clustering.Clustering)\n\n    def test_unconditional_commit_on_new_signal(self):\n        with patch.object(self.widget, 'unconditional_commit') as commit:\n            self.widget.auto_commit = False\n            commit.reset_mock()\n            self.send_signal(self.widget.Inputs.data, self.titanic)\n            commit.assert_called()\n\n    def test_saved_selection(self):\n        iris = Table(\"iris\")\n\n        self.send_signal(self.widget.Inputs.data, iris)\n        selected_indices = list(range(10, 31))\n        self.widget.scene.widget.selectRows(selected_indices)\n        self.widget.on_selection_finished()\n        settings = self.widget.settingsHandler.pack_data(self.widget)\n\n        w = self.create_widget(OWHeatMap, stored_settings=settings)\n        self.send_signal(w.Inputs.data, iris, widget=w)\n        self.assertEqual(len(self.get_output(w.Outputs.selected_data)), 21)\n\n    def test_set_split_var(self):\n        data = self.brown_selected[::3]\n        w = self.widget\n        self.send_signal(self.widget.Inputs.data, data, widget=w)\n        self.assertIs(w.split_by_var, data.domain.class_var)\n        self.assertEqual(len(w.parts.rows),\n                         len(data.domain.class_var.values))\n        w.set_split_variable(None)\n        self.assertIs(w.split_by_var, None)\n        self.assertEqual(len(w.parts.rows), 1)\n\n    def test_set_split_var_missing(self):\n        data = self.brown_selected[::3].copy()\n        data.Y[::5] = np.nan\n        w = self.widget\n        self.send_signal(self.widget.Inputs.data, data, widget=w)\n        self.assertIs(w.split_by_var, data.domain.class_var)\n        self.assertEqual(len(w.parts.rows),\n                         len(data.domain.class_var.values))\n        self.assertTrue(w.Warning.missing_split_values.is_shown())\n        w.set_split_variable(None)\n        self.assertFalse(w.Warning.missing_split_values.is_shown())\n\n    def test_palette_centering(self):\n        data = np.arange(2).reshape(-1, 1)\n        table = Table.from_numpy(Domain([ContinuousVariable(\"y\")]), data)\n        self.send_signal(self.widget.Inputs.data, table)\n\n        self.widget.color_palette = lambda: \\\n            colorpalettes.ContinuousPalette.from_colors(\n                (0, 255, 0), (255, 0, 0), (0, 0, 0)).lookup_table()\n\n        desired_uncentered = [[0, 255, 0],\n                              [255, 0, 0]]\n\n        desired_centered = [[0, 0, 0],\n                            [255, 0, 0]]\n\n        for center, desired in [(False, desired_uncentered), (True, desired_centered)]:\n            with patch.object(OWHeatMap, \"center_palette\", center):\n                self.widget.update_color_schema()\n                heatmap_widget = self.widget.scene.widget.heatmap_widget_grid[0][0]\n                image = heatmap_widget.pixmap().toImage()\n                colors = image_row_colors(image)\n                np.testing.assert_almost_equal(colors, desired)\n\n    def test_palette_center(self):\n        widget = self.widget\n        model = widget.color_cb.model()\n        for idx in range(model.rowCount(QModelIndex())):\n            palette = model.data(model.index(idx, 0), Qt.UserRole)\n            if palette is None:\n                continue\n            widget.color_cb.setCurrentIndex(idx)\n            self.assertEqual(widget.center_palette,\n                             bool(palette.flags & palette.Diverging))\n\n    def test_migrate_settings_v3(self):\n        w = self.create_widget(\n            OWHeatMap, stored_settings={\n                \"row_clustering\": False,\n                \"col_clustering\": True,\n            }\n        )\n        self.assertEqual(w.row_clustering, Clustering.None_)\n        self.assertEqual(w.col_clustering, Clustering.OrderedClustering)\n\n    def test_row_color_annotations(self):\n        widget = self.widget\n        data = Table(\"brown-selected\")[::5]\n        self.send_signal(widget.Inputs.data, data, widget=widget)\n        widget.set_annotation_color_var(data.domain[\"function\"])\n        self.assertTrue(widget.scene.widget.right_side_colors[0].isVisible())\n        widget.set_annotation_color_var(None)\n        self.assertFalse(widget.scene.widget.right_side_colors[0].isVisible())\n\n    def test_summary(self):\n        \"\"\"Check if status bar is updated when data is received\"\"\"\n        info = self.widget.info\n        no_input, no_output = \"No data on input\", \"No data on output\"\n\n        data = self.housing\n        self.send_signal(self.widget.Inputs.data, data)\n        summary, details = f\"{len(data)}\", format_summary_details(data)\n        self.assertEqual(info._StateInfo__input_summary.brief, summary)\n        self.assertEqual(info._StateInfo__input_summary.details, details)\n        self.assertEqual(info._StateInfo__output_summary.brief, \"\")\n        self.assertEqual(info._StateInfo__output_summary.details, no_output)\n        self._select_data()\n        output = self.get_output(self.widget.Outputs.selected_data)\n        summary, details = f\"{len(output)}\", format_summary_details(output)\n        self.assertEqual(info._StateInfo__output_summary.brief, summary)\n        self.assertEqual(info._StateInfo__output_summary.details, details)\n\n        self.send_signal(self.widget.Inputs.data, None)\n        self.assertEqual(info._StateInfo__input_summary.brief, \"\")\n        self.assertEqual(info._StateInfo__input_summary.details, no_input)\n        self.assertEqual(info._StateInfo__output_summary.brief, \"\")\n        self.assertEqual(info._StateInfo__output_summary.details, no_output)",
                            "ast_type_str": "ClassDef",
                            "start_lineno": 225,
                            "end_lineno": 235,
                            "parent": {
                                "file_name": "Orange/widgets/visualize/tests/test_owheatmap.py",
                                "type": "class",
                                "name": "TestOWHeatMap",
                                "content": "class TestOWHeatMap(WidgetTest, WidgetOutputsTestMixin):\n    @classmethod\n    def setUpClass(cls):\n        super().setUpClass()\n        WidgetOutputsTestMixin.init(cls)\n\n        cls.housing = Table(\"housing\")\n        cls.titanic = Table(\"titanic\")\n        cls.brown_selected = Table(\"brown-selected\")\n\n        cls.signal_name = \"Data\"\n        cls.signal_data = cls.data\n\n    def setUp(self):\n        self.widget = self.create_widget(OWHeatMap)  # type: OWHeatMap\n\n    def test_input_data(self):\n        \"\"\"Check widget's data with data on the input\"\"\"\n        for data in (self.data, self.housing):\n            self.assertEqual(self.widget.data, None)\n            self.send_signal(self.widget.Inputs.data, data)\n            self.assertEqual(self.widget.data, data)\n            self.assertFalse(self.widget.Error.active)\n            self.assertFalse(self.widget.Warning.active)\n            self.assertFalse(self.widget.Information.active)\n            self.send_signal(self.widget.Inputs.data, None)\n\n        self.send_signal(self.widget.Inputs.data, self.data[:0])\n\n    def test_error_message(self):\n        self.send_signal(self.widget.Inputs.data, self.titanic)\n        self.assertTrue(self.widget.Error.active)\n        self.send_signal(self.widget.Inputs.data, self.data)\n        self.assertFalse(self.widget.Error.active)\n\n    def test_information_message(self):\n        self.widget.set_row_clustering(Clustering.OrderedClustering)\n        continuizer = Continuize()\n        cont_titanic = continuizer(self.titanic)\n        self.widget.MaxClustering = 1000\n        self.send_signal(self.widget.Inputs.data, cont_titanic)\n        self.assertTrue(self.widget.Information.active)\n        self.send_signal(self.widget.Inputs.data, self.data)\n        self.assertFalse(self.widget.Information.active)\n\n    def test_settings_changed(self):\n        self.send_signal(self.widget.Inputs.data, self.data)\n        # check output when \"Sorting Column\" setting changes\n        self._select_data()\n        self.assertIsNotNone(self.get_output(self.widget.Outputs.selected_data))\n        self.widget.set_col_clustering(Clustering.OrderedClustering)\n        self.assertIsNone(self.get_output(self.widget.Outputs.selected_data))\n        # check output when \"Sorting Row\" setting changes\n        self._select_data()\n        self.assertIsNotNone(self.get_output(self.widget.Outputs.selected_data))\n        self.widget.set_row_clustering(Clustering.OrderedClustering)\n        self.assertIsNone(self.get_output(self.widget.Outputs.selected_data))\n        # check output when \"Merge by k-means\" setting changes\n        self._select_data()\n        self.assertIsNotNone(self.get_output(self.widget.Outputs.selected_data))\n        self.widget.controls.merge_kmeans.setChecked(True)\n        self.assertIsNone(self.get_output(self.widget.Outputs.selected_data))\n\n    def _select_data(self):\n        selected_indices = list(range(10, 31))\n        self.widget.scene.widget.selectRows(selected_indices)\n        self.widget.on_selection_finished()\n        return selected_indices\n\n    def test_not_enough_data_settings_changed(self):\n        \"\"\"Check widget for dataset with one feature or for one instance\"\"\"\n        # Test tests that widget handles this exact condition\n        warnings.filterwarnings(\"ignore\", \"Number of distinct clusters\",\n                                ConvergenceWarning)\n\n        msg = self.widget.Error\n        clusterings = (Clustering.None_, Clustering.Clustering,\n                       Clustering.OrderedClustering)\n        for kmeans_checked in (False, True):\n            self.widget.controls.merge_kmeans.setChecked(kmeans_checked)\n            for col_clust in clusterings:\n                self.widget.set_col_clustering(col_clust)\n                self.send_signal(self.widget.Inputs.data, None)\n                self.send_signal(self.widget.Inputs.data, self.data[:, 0])\n                if col_clust != Clustering.None_:\n                    self.assertTrue(msg.not_enough_features.is_shown())\n                for row_clust in clusterings:\n                    self.widget.set_row_clustering(row_clust)\n                    self.send_signal(self.widget.Inputs.data, None)\n                    self.send_signal(self.widget.Inputs.data, self.data[0:1])\n                    if row_clust != Clustering.None_:\n                        self.assertTrue(msg.not_enough_instances.is_shown())\n                    elif kmeans_checked and row_clust != Clustering.None_:\n                        self.assertTrue(msg.not_enough_instances_k_means.is_shown())\n            self.send_signal(self.widget.Inputs.data, None)\n            self.assertFalse(msg.not_enough_features.is_shown())\n            self.assertFalse(msg.not_enough_instances.is_shown())\n            self.assertFalse(msg.not_enough_instances_k_means.is_shown())\n\n    def test_color_low_high(self):\n        \"\"\"\n        Prevent horizontal sliders to set Low >= High.\n        GH-2025\n        \"\"\"\n        self.widget.controls.threshold_low.setValue(4)\n        self.widget.controls.threshold_high.setValue(2)\n        self.assertGreater(self.widget.threshold_high, self.widget.threshold_low)\n\n    def test_data_column_nans(self):\n        \"\"\"\n        Send data with one column with all values set to NaN.\n        ValueError should not be thrown (Invalid number of variable columns)\n        That column is now suppose to be removed in a table array and\n        in a domain as well.\n        GH-2057\n        \"\"\"\n        table = datasets.data_one_column_nans()\n        self.widget.controls.merge_kmeans.setChecked(True)\n        self.send_signal(self.widget.Inputs.data, table)\n\n    def test_cluster_column_on_all_zero_column(self):\n        # Pearson distance used for clustering of columns does not\n        # handle all zero columns well\n        iris = Table(\"iris\")\n        iris[:, 0] = 0\n\n        self.widget.col_clustering = True\n        self.widget.set_dataset(iris)\n\n    def test_empty_clusters(self):\n        \"\"\"Test if empty clusters are not displayed and warning is shown\"\"\"\n        data = np.array([1, 1, 1, 2, 2, 2, 3, 3, 3])\n\n        table = Table.from_numpy(Domain([ContinuousVariable(\"y\")]),\n                                 data.reshape((9, 1)))\n        self.widget.controls.merge_kmeans.setChecked(True)\n\n        # Test tests that widget handles this exact condition\n        warnings.filterwarnings(\"ignore\", \"Number of distinct clusters\",\n                                ConvergenceWarning)\n        self.send_signal(self.widget.Inputs.data, table)\n\n        self.assertTrue(self.widget.Warning.empty_clusters.is_shown())\n        self.assertEqual(len(self.widget.merge_indices), 3)\n\n    def test_use_enough_colors(self):\n        # Before 201906 thresholds modified the palette and decreased\n        # the number of colors used.\n        data = np.arange(1000).reshape(-1, 1)\n        table = Table.from_numpy(Domain([ContinuousVariable(\"y\")]), data)\n        self.send_signal(self.widget.Inputs.data, table)\n        self.widget.threshold_high = 0.05\n        self.widget.update_color_schema()\n        heatmap_widget = self.widget.scene.widget.heatmap_widget_grid[0][0]\n        image = heatmap_widget.pixmap().toImage()\n        colors = image_row_colors(image)\n        unique_colors = len(np.unique(colors, axis=0))\n        self.assertLessEqual(len(data)*self.widget.threshold_low, unique_colors)\n\n    def test_cls_with_single_instance(self):\n        table = Table(Domain([ContinuousVariable(\"c1\")],\n                             [DiscreteVariable(\"c2\", values=(\"a\", \"b\"))]),\n                      np.array([[1], [2], [3]]), np.array([[0], [0], [1]]))\n        self.send_signal(self.widget.Inputs.data, table)\n        self.widget.set_row_clustering(Clustering.Clustering)\n\n    def test_unconditional_commit_on_new_signal(self):\n        with patch.object(self.widget, 'unconditional_commit') as commit:\n            self.widget.auto_commit = False\n            commit.reset_mock()\n            self.send_signal(self.widget.Inputs.data, self.titanic)\n            commit.assert_called()\n\n    def test_saved_selection(self):\n        iris = Table(\"iris\")\n\n        self.send_signal(self.widget.Inputs.data, iris)\n        selected_indices = list(range(10, 31))\n        self.widget.scene.widget.selectRows(selected_indices)\n        self.widget.on_selection_finished()\n        settings = self.widget.settingsHandler.pack_data(self.widget)\n\n        w = self.create_widget(OWHeatMap, stored_settings=settings)\n        self.send_signal(w.Inputs.data, iris, widget=w)\n        self.assertEqual(len(self.get_output(w.Outputs.selected_data)), 21)\n\n    def test_set_split_var(self):\n        data = self.brown_selected[::3]\n        w = self.widget\n        self.send_signal(self.widget.Inputs.data, data, widget=w)\n        self.assertIs(w.split_by_var, data.domain.class_var)\n        self.assertEqual(len(w.parts.rows),\n                         len(data.domain.class_var.values))\n        w.set_split_variable(None)\n        self.assertIs(w.split_by_var, None)\n        self.assertEqual(len(w.parts.rows), 1)\n\n    def test_set_split_var_missing(self):\n        data = self.brown_selected[::3].copy()\n        data.Y[::5] = np.nan\n        w = self.widget\n        self.send_signal(self.widget.Inputs.data, data, widget=w)\n        self.assertIs(w.split_by_var, data.domain.class_var)\n        self.assertEqual(len(w.parts.rows),\n                         len(data.domain.class_var.values))\n        self.assertTrue(w.Warning.missing_split_values.is_shown())\n        w.set_split_variable(None)\n        self.assertFalse(w.Warning.missing_split_values.is_shown())\n\n    def test_palette_centering(self):\n        data = np.arange(2).reshape(-1, 1)\n        table = Table.from_numpy(Domain([ContinuousVariable(\"y\")]), data)\n        self.send_signal(self.widget.Inputs.data, table)\n\n        self.widget.color_palette = lambda: \\\n            colorpalettes.ContinuousPalette.from_colors(\n                (0, 255, 0), (255, 0, 0), (0, 0, 0)).lookup_table()\n\n        desired_uncentered = [[0, 255, 0],\n                              [255, 0, 0]]\n\n        desired_centered = [[0, 0, 0],\n                            [255, 0, 0]]\n\n        for center, desired in [(False, desired_uncentered), (True, desired_centered)]:\n            with patch.object(OWHeatMap, \"center_palette\", center):\n                self.widget.update_color_schema()\n                heatmap_widget = self.widget.scene.widget.heatmap_widget_grid[0][0]\n                image = heatmap_widget.pixmap().toImage()\n                colors = image_row_colors(image)\n                np.testing.assert_almost_equal(colors, desired)\n\n    def test_palette_center(self):\n        widget = self.widget\n        model = widget.color_cb.model()\n        for idx in range(model.rowCount(QModelIndex())):\n            palette = model.data(model.index(idx, 0), Qt.UserRole)\n            if palette is None:\n                continue\n            widget.color_cb.setCurrentIndex(idx)\n            self.assertEqual(widget.center_palette,\n                             bool(palette.flags & palette.Diverging))\n\n    def test_migrate_settings_v3(self):\n        w = self.create_widget(\n            OWHeatMap, stored_settings={\n                \"row_clustering\": False,\n                \"col_clustering\": True,\n            }\n        )\n        self.assertEqual(w.row_clustering, Clustering.None_)\n        self.assertEqual(w.col_clustering, Clustering.OrderedClustering)\n\n    def test_row_color_annotations(self):\n        widget = self.widget\n        data = Table(\"brown-selected\")[::5]\n        self.send_signal(widget.Inputs.data, data, widget=widget)\n        widget.set_annotation_color_var(data.domain[\"function\"])\n        self.assertTrue(widget.scene.widget.right_side_colors[0].isVisible())\n        widget.set_annotation_color_var(None)\n        self.assertFalse(widget.scene.widget.right_side_colors[0].isVisible())\n\n    def test_summary(self):\n        \"\"\"Check if status bar is updated when data is received\"\"\"\n        info = self.widget.info\n        no_input, no_output = \"No data on input\", \"No data on output\"\n\n        data = self.housing\n        self.send_signal(self.widget.Inputs.data, data)\n        summary, details = f\"{len(data)}\", format_summary_details(data)\n        self.assertEqual(info._StateInfo__input_summary.brief, summary)\n        self.assertEqual(info._StateInfo__input_summary.details, details)\n        self.assertEqual(info._StateInfo__output_summary.brief, \"\")\n        self.assertEqual(info._StateInfo__output_summary.details, no_output)\n        self._select_data()\n        output = self.get_output(self.widget.Outputs.selected_data)\n        summary, details = f\"{len(output)}\", format_summary_details(output)\n        self.assertEqual(info._StateInfo__output_summary.brief, summary)\n        self.assertEqual(info._StateInfo__output_summary.details, details)\n\n        self.send_signal(self.widget.Inputs.data, None)\n        self.assertEqual(info._StateInfo__input_summary.brief, \"\")\n        self.assertEqual(info._StateInfo__input_summary.details, no_input)\n        self.assertEqual(info._StateInfo__output_summary.brief, \"\")\n        self.assertEqual(info._StateInfo__output_summary.details, no_output)",
                                "ast_type_str": "ClassDef",
                                "start_lineno": 28,
                                "end_lineno": 312,
                                "parent": null
                            }
                        },
                        {
                            "file_name": "Orange/widgets/visualize/tests/test_owheatmap.py",
                            "type": "class",
                            "name": "TestOWHeatMap",
                            "content": "class TestOWHeatMap(WidgetTest, WidgetOutputsTestMixin):\n    @classmethod\n    def setUpClass(cls):\n        super().setUpClass()\n        WidgetOutputsTestMixin.init(cls)\n\n        cls.housing = Table(\"housing\")\n        cls.titanic = Table(\"titanic\")\n        cls.brown_selected = Table(\"brown-selected\")\n\n        cls.signal_name = \"Data\"\n        cls.signal_data = cls.data\n\n    def setUp(self):\n        self.widget = self.create_widget(OWHeatMap)  # type: OWHeatMap\n\n    def test_input_data(self):\n        \"\"\"Check widget's data with data on the input\"\"\"\n        for data in (self.data, self.housing):\n            self.assertEqual(self.widget.data, None)\n            self.send_signal(self.widget.Inputs.data, data)\n            self.assertEqual(self.widget.data, data)\n            self.assertFalse(self.widget.Error.active)\n            self.assertFalse(self.widget.Warning.active)\n            self.assertFalse(self.widget.Information.active)\n            self.send_signal(self.widget.Inputs.data, None)\n\n        self.send_signal(self.widget.Inputs.data, self.data[:0])\n\n    def test_error_message(self):\n        self.send_signal(self.widget.Inputs.data, self.titanic)\n        self.assertTrue(self.widget.Error.active)\n        self.send_signal(self.widget.Inputs.data, self.data)\n        self.assertFalse(self.widget.Error.active)\n\n    def test_information_message(self):\n        self.widget.set_row_clustering(Clustering.OrderedClustering)\n        continuizer = Continuize()\n        cont_titanic = continuizer(self.titanic)\n        self.widget.MaxClustering = 1000\n        self.send_signal(self.widget.Inputs.data, cont_titanic)\n        self.assertTrue(self.widget.Information.active)\n        self.send_signal(self.widget.Inputs.data, self.data)\n        self.assertFalse(self.widget.Information.active)\n\n    def test_settings_changed(self):\n        self.send_signal(self.widget.Inputs.data, self.data)\n        # check output when \"Sorting Column\" setting changes\n        self._select_data()\n        self.assertIsNotNone(self.get_output(self.widget.Outputs.selected_data))\n        self.widget.set_col_clustering(Clustering.OrderedClustering)\n        self.assertIsNone(self.get_output(self.widget.Outputs.selected_data))\n        # check output when \"Sorting Row\" setting changes\n        self._select_data()\n        self.assertIsNotNone(self.get_output(self.widget.Outputs.selected_data))\n        self.widget.set_row_clustering(Clustering.OrderedClustering)\n        self.assertIsNone(self.get_output(self.widget.Outputs.selected_data))\n        # check output when \"Merge by k-means\" setting changes\n        self._select_data()\n        self.assertIsNotNone(self.get_output(self.widget.Outputs.selected_data))\n        self.widget.controls.merge_kmeans.setChecked(True)\n        self.assertIsNone(self.get_output(self.widget.Outputs.selected_data))\n\n    def _select_data(self):\n        selected_indices = list(range(10, 31))\n        self.widget.scene.widget.selectRows(selected_indices)\n        self.widget.on_selection_finished()\n        return selected_indices\n\n    def test_not_enough_data_settings_changed(self):\n        \"\"\"Check widget for dataset with one feature or for one instance\"\"\"\n        # Test tests that widget handles this exact condition\n        warnings.filterwarnings(\"ignore\", \"Number of distinct clusters\",\n                                ConvergenceWarning)\n\n        msg = self.widget.Error\n        clusterings = (Clustering.None_, Clustering.Clustering,\n                       Clustering.OrderedClustering)\n        for kmeans_checked in (False, True):\n            self.widget.controls.merge_kmeans.setChecked(kmeans_checked)\n            for col_clust in clusterings:\n                self.widget.set_col_clustering(col_clust)\n                self.send_signal(self.widget.Inputs.data, None)\n                self.send_signal(self.widget.Inputs.data, self.data[:, 0])\n                if col_clust != Clustering.None_:\n                    self.assertTrue(msg.not_enough_features.is_shown())\n                for row_clust in clusterings:\n                    self.widget.set_row_clustering(row_clust)\n                    self.send_signal(self.widget.Inputs.data, None)\n                    self.send_signal(self.widget.Inputs.data, self.data[0:1])\n                    if row_clust != Clustering.None_:\n                        self.assertTrue(msg.not_enough_instances.is_shown())\n                    elif kmeans_checked and row_clust != Clustering.None_:\n                        self.assertTrue(msg.not_enough_instances_k_means.is_shown())\n            self.send_signal(self.widget.Inputs.data, None)\n            self.assertFalse(msg.not_enough_features.is_shown())\n            self.assertFalse(msg.not_enough_instances.is_shown())\n            self.assertFalse(msg.not_enough_instances_k_means.is_shown())\n\n    def test_color_low_high(self):\n        \"\"\"\n        Prevent horizontal sliders to set Low >= High.\n        GH-2025\n        \"\"\"\n        self.widget.controls.threshold_low.setValue(4)\n        self.widget.controls.threshold_high.setValue(2)\n        self.assertGreater(self.widget.threshold_high, self.widget.threshold_low)\n\n    def test_data_column_nans(self):\n        \"\"\"\n        Send data with one column with all values set to NaN.\n        ValueError should not be thrown (Invalid number of variable columns)\n        That column is now suppose to be removed in a table array and\n        in a domain as well.\n        GH-2057\n        \"\"\"\n        table = datasets.data_one_column_nans()\n        self.widget.controls.merge_kmeans.setChecked(True)\n        self.send_signal(self.widget.Inputs.data, table)\n\n    def test_cluster_column_on_all_zero_column(self):\n        # Pearson distance used for clustering of columns does not\n        # handle all zero columns well\n        iris = Table(\"iris\")\n        iris[:, 0] = 0\n\n        self.widget.col_clustering = True\n        self.widget.set_dataset(iris)\n\n    def test_empty_clusters(self):\n        \"\"\"Test if empty clusters are not displayed and warning is shown\"\"\"\n        data = np.array([1, 1, 1, 2, 2, 2, 3, 3, 3])\n\n        table = Table.from_numpy(Domain([ContinuousVariable(\"y\")]),\n                                 data.reshape((9, 1)))\n        self.widget.controls.merge_kmeans.setChecked(True)\n\n        # Test tests that widget handles this exact condition\n        warnings.filterwarnings(\"ignore\", \"Number of distinct clusters\",\n                                ConvergenceWarning)\n        self.send_signal(self.widget.Inputs.data, table)\n\n        self.assertTrue(self.widget.Warning.empty_clusters.is_shown())\n        self.assertEqual(len(self.widget.merge_indices), 3)\n\n    def test_use_enough_colors(self):\n        # Before 201906 thresholds modified the palette and decreased\n        # the number of colors used.\n        data = np.arange(1000).reshape(-1, 1)\n        table = Table.from_numpy(Domain([ContinuousVariable(\"y\")]), data)\n        self.send_signal(self.widget.Inputs.data, table)\n        self.widget.threshold_high = 0.05\n        self.widget.update_color_schema()\n        heatmap_widget = self.widget.scene.widget.heatmap_widget_grid[0][0]\n        image = heatmap_widget.pixmap().toImage()\n        colors = image_row_colors(image)\n        unique_colors = len(np.unique(colors, axis=0))\n        self.assertLessEqual(len(data)*self.widget.threshold_low, unique_colors)\n\n    def test_cls_with_single_instance(self):\n        table = Table(Domain([ContinuousVariable(\"c1\")],\n                             [DiscreteVariable(\"c2\", values=(\"a\", \"b\"))]),\n                      np.array([[1], [2], [3]]), np.array([[0], [0], [1]]))\n        self.send_signal(self.widget.Inputs.data, table)\n        self.widget.set_row_clustering(Clustering.Clustering)\n\n    def test_unconditional_commit_on_new_signal(self):\n        with patch.object(self.widget, 'unconditional_commit') as commit:\n            self.widget.auto_commit = False\n            commit.reset_mock()\n            self.send_signal(self.widget.Inputs.data, self.titanic)\n            commit.assert_called()\n\n    def test_saved_selection(self):\n        iris = Table(\"iris\")\n\n        self.send_signal(self.widget.Inputs.data, iris)\n        selected_indices = list(range(10, 31))\n        self.widget.scene.widget.selectRows(selected_indices)\n        self.widget.on_selection_finished()\n        settings = self.widget.settingsHandler.pack_data(self.widget)\n\n        w = self.create_widget(OWHeatMap, stored_settings=settings)\n        self.send_signal(w.Inputs.data, iris, widget=w)\n        self.assertEqual(len(self.get_output(w.Outputs.selected_data)), 21)\n\n    def test_set_split_var(self):\n        data = self.brown_selected[::3]\n        w = self.widget\n        self.send_signal(self.widget.Inputs.data, data, widget=w)\n        self.assertIs(w.split_by_var, data.domain.class_var)\n        self.assertEqual(len(w.parts.rows),\n                         len(data.domain.class_var.values))\n        w.set_split_variable(None)\n        self.assertIs(w.split_by_var, None)\n        self.assertEqual(len(w.parts.rows), 1)\n\n    def test_set_split_var_missing(self):\n        data = self.brown_selected[::3].copy()\n        data.Y[::5] = np.nan\n        w = self.widget\n        self.send_signal(self.widget.Inputs.data, data, widget=w)\n        self.assertIs(w.split_by_var, data.domain.class_var)\n        self.assertEqual(len(w.parts.rows),\n                         len(data.domain.class_var.values))\n        self.assertTrue(w.Warning.missing_split_values.is_shown())\n        w.set_split_variable(None)\n        self.assertFalse(w.Warning.missing_split_values.is_shown())\n\n    def test_palette_centering(self):\n        data = np.arange(2).reshape(-1, 1)\n        table = Table.from_numpy(Domain([ContinuousVariable(\"y\")]), data)\n        self.send_signal(self.widget.Inputs.data, table)\n\n        self.widget.color_palette = lambda: \\\n            colorpalettes.ContinuousPalette.from_colors(\n                (0, 255, 0), (255, 0, 0), (0, 0, 0)).lookup_table()\n\n        desired_uncentered = [[0, 255, 0],\n                              [255, 0, 0]]\n\n        desired_centered = [[0, 0, 0],\n                            [255, 0, 0]]\n\n        for center, desired in [(False, desired_uncentered), (True, desired_centered)]:\n            with patch.object(OWHeatMap, \"center_palette\", center):\n                self.widget.update_color_schema()\n                heatmap_widget = self.widget.scene.widget.heatmap_widget_grid[0][0]\n                image = heatmap_widget.pixmap().toImage()\n                colors = image_row_colors(image)\n                np.testing.assert_almost_equal(colors, desired)\n\n    def test_palette_center(self):\n        widget = self.widget\n        model = widget.color_cb.model()\n        for idx in range(model.rowCount(QModelIndex())):\n            palette = model.data(model.index(idx, 0), Qt.UserRole)\n            if palette is None:\n                continue\n            widget.color_cb.setCurrentIndex(idx)\n            self.assertEqual(widget.center_palette,\n                             bool(palette.flags & palette.Diverging))\n\n    def test_migrate_settings_v3(self):\n        w = self.create_widget(\n            OWHeatMap, stored_settings={\n                \"row_clustering\": False,\n                \"col_clustering\": True,\n            }\n        )\n        self.assertEqual(w.row_clustering, Clustering.None_)\n        self.assertEqual(w.col_clustering, Clustering.OrderedClustering)\n\n    def test_row_color_annotations(self):\n        widget = self.widget\n        data = Table(\"brown-selected\")[::5]\n        self.send_signal(widget.Inputs.data, data, widget=widget)\n        widget.set_annotation_color_var(data.domain[\"function\"])\n        self.assertTrue(widget.scene.widget.right_side_colors[0].isVisible())\n        widget.set_annotation_color_var(None)\n        self.assertFalse(widget.scene.widget.right_side_colors[0].isVisible())\n\n    def test_summary(self):\n        \"\"\"Check if status bar is updated when data is received\"\"\"\n        info = self.widget.info\n        no_input, no_output = \"No data on input\", \"No data on output\"\n\n        data = self.housing\n        self.send_signal(self.widget.Inputs.data, data)\n        summary, details = f\"{len(data)}\", format_summary_details(data)\n        self.assertEqual(info._StateInfo__input_summary.brief, summary)\n        self.assertEqual(info._StateInfo__input_summary.details, details)\n        self.assertEqual(info._StateInfo__output_summary.brief, \"\")\n        self.assertEqual(info._StateInfo__output_summary.details, no_output)\n        self._select_data()\n        output = self.get_output(self.widget.Outputs.selected_data)\n        summary, details = f\"{len(output)}\", format_summary_details(output)\n        self.assertEqual(info._StateInfo__output_summary.brief, summary)\n        self.assertEqual(info._StateInfo__output_summary.details, details)\n\n        self.send_signal(self.widget.Inputs.data, None)\n        self.assertEqual(info._StateInfo__input_summary.brief, \"\")\n        self.assertEqual(info._StateInfo__input_summary.details, no_input)\n        self.assertEqual(info._StateInfo__output_summary.brief, \"\")\n        self.assertEqual(info._StateInfo__output_summary.details, no_output)",
                            "ast_type_str": "ClassDef",
                            "start_lineno": 28,
                            "end_lineno": 312,
                            "parent": null
                        }
                    ],
                    "added_entities": [],
                    "deleted_entities": []
                }
            ]
        }
    ],
    "old_commit_hash": "20b9209e1f6a79c01b34223d47ba6b05838c8fce^",
    "new_commit_hash": "20b9209e1f6a79c01b34223d47ba6b05838c8fce",
    "commit_message": "owheatmap: Split N/A into separate group",
    "commit_date": "2020-04-23T20:51:16+02:00",
    "metadata": {}
}
{
    "repo_name": "orange3",
    "new_commit_hash": "20b9209e1f6a79c01b34223d47ba6b05838c8fce",
    "test_file_codes": [
        "# Test methods with long descriptive names can omit docstrings\n# pylint: disable=missing-docstring, protected-access\nimport warnings\nimport unittest\nfrom unittest.mock import patch\n\nimport numpy as np\nfrom sklearn.exceptions import ConvergenceWarning\n\nfrom AnyQt.QtCore import Qt, QModelIndex\n\nfrom Orange.data import Table, Domain, ContinuousVariable, DiscreteVariable\nfrom Orange.preprocess import Continuize\nfrom Orange.widgets.utils import colorpalettes\nfrom Orange.widgets.utils.state_summary import format_summary_details\nfrom Orange.widgets.visualize.owheatmap import OWHeatMap, Clustering\nfrom Orange.widgets.tests.base import WidgetTest, WidgetOutputsTestMixin, datasets\n\n\ndef image_row_colors(image):\n    colors = np.full((image.height(), 3), np.nan)\n    for r in range(image.height()):\n        c = image.pixelColor(0, r)\n        colors[r] = c.red(), c.green(), c.blue()\n    return colors\n\n\nclass TestOWHeatMap(WidgetTest, WidgetOutputsTestMixin):\n    @classmethod\n    def setUpClass(cls):\n        super().setUpClass()\n        WidgetOutputsTestMixin.init(cls)\n\n        cls.housing = Table(\"housing\")\n        cls.titanic = Table(\"titanic\")\n        cls.brown_selected = Table(\"brown-selected\")\n\n        cls.signal_name = \"Data\"\n        cls.signal_data = cls.data\n\n    def setUp(self):\n        self.widget = self.create_widget(OWHeatMap)  # type: OWHeatMap\n\n    def test_input_data(self):\n        \"\"\"Check widget's data with data on the input\"\"\"\n        for data in (self.data, self.housing):\n            self.assertEqual(self.widget.data, None)\n            self.send_signal(self.widget.Inputs.data, data)\n            self.assertEqual(self.widget.data, data)\n            self.assertFalse(self.widget.Error.active)\n            self.assertFalse(self.widget.Warning.active)\n            self.assertFalse(self.widget.Information.active)\n            self.send_signal(self.widget.Inputs.data, None)\n\n        self.send_signal(self.widget.Inputs.data, self.data[:0])\n\n    def test_error_message(self):\n        self.send_signal(self.widget.Inputs.data, self.titanic)\n        self.assertTrue(self.widget.Error.active)\n        self.send_signal(self.widget.Inputs.data, self.data)\n        self.assertFalse(self.widget.Error.active)\n\n    def test_information_message(self):\n        self.widget.set_row_clustering(Clustering.OrderedClustering)\n        continuizer = Continuize()\n        cont_titanic = continuizer(self.titanic)\n        self.widget.MaxClustering = 1000\n        self.send_signal(self.widget.Inputs.data, cont_titanic)\n        self.assertTrue(self.widget.Information.active)\n        self.send_signal(self.widget.Inputs.data, self.data)\n        self.assertFalse(self.widget.Information.active)\n\n    def test_settings_changed(self):\n        self.send_signal(self.widget.Inputs.data, self.data)\n        # check output when \"Sorting Column\" setting changes\n        self._select_data()\n        self.assertIsNotNone(self.get_output(self.widget.Outputs.selected_data))\n        self.widget.set_col_clustering(Clustering.OrderedClustering)\n        self.assertIsNone(self.get_output(self.widget.Outputs.selected_data))\n        # check output when \"Sorting Row\" setting changes\n        self._select_data()\n        self.assertIsNotNone(self.get_output(self.widget.Outputs.selected_data))\n        self.widget.set_row_clustering(Clustering.OrderedClustering)\n        self.assertIsNone(self.get_output(self.widget.Outputs.selected_data))\n        # check output when \"Merge by k-means\" setting changes\n        self._select_data()\n        self.assertIsNotNone(self.get_output(self.widget.Outputs.selected_data))\n        self.widget.controls.merge_kmeans.setChecked(True)\n        self.assertIsNone(self.get_output(self.widget.Outputs.selected_data))\n\n    def _select_data(self):\n        selected_indices = list(range(10, 31))\n        self.widget.scene.widget.selectRows(selected_indices)\n        self.widget.on_selection_finished()\n        return selected_indices\n\n    def test_not_enough_data_settings_changed(self):\n        \"\"\"Check widget for dataset with one feature or for one instance\"\"\"\n        # Test tests that widget handles this exact condition\n        warnings.filterwarnings(\"ignore\", \"Number of distinct clusters\",\n                                ConvergenceWarning)\n\n        msg = self.widget.Error\n        clusterings = (Clustering.None_, Clustering.Clustering,\n                       Clustering.OrderedClustering)\n        for kmeans_checked in (False, True):\n            self.widget.controls.merge_kmeans.setChecked(kmeans_checked)\n            for col_clust in clusterings:\n                self.widget.set_col_clustering(col_clust)\n                self.send_signal(self.widget.Inputs.data, None)\n                self.send_signal(self.widget.Inputs.data, self.data[:, 0])\n                if col_clust != Clustering.None_:\n                    self.assertTrue(msg.not_enough_features.is_shown())\n                for row_clust in clusterings:\n                    self.widget.set_row_clustering(row_clust)\n                    self.send_signal(self.widget.Inputs.data, None)\n                    self.send_signal(self.widget.Inputs.data, self.data[0:1])\n                    if row_clust != Clustering.None_:\n                        self.assertTrue(msg.not_enough_instances.is_shown())\n                    elif kmeans_checked and row_clust != Clustering.None_:\n                        self.assertTrue(msg.not_enough_instances_k_means.is_shown())\n            self.send_signal(self.widget.Inputs.data, None)\n            self.assertFalse(msg.not_enough_features.is_shown())\n            self.assertFalse(msg.not_enough_instances.is_shown())\n            self.assertFalse(msg.not_enough_instances_k_means.is_shown())\n\n    def test_color_low_high(self):\n        \"\"\"\n        Prevent horizontal sliders to set Low >= High.\n        GH-2025\n        \"\"\"\n        self.widget.controls.threshold_low.setValue(4)\n        self.widget.controls.threshold_high.setValue(2)\n        self.assertGreater(self.widget.threshold_high, self.widget.threshold_low)\n\n    def test_data_column_nans(self):\n        \"\"\"\n        Send data with one column with all values set to NaN.\n        ValueError should not be thrown (Invalid number of variable columns)\n        That column is now suppose to be removed in a table array and\n        in a domain as well.\n        GH-2057\n        \"\"\"\n        table = datasets.data_one_column_nans()\n        self.widget.controls.merge_kmeans.setChecked(True)\n        self.send_signal(self.widget.Inputs.data, table)\n\n    def test_cluster_column_on_all_zero_column(self):\n        # Pearson distance used for clustering of columns does not\n        # handle all zero columns well\n        iris = Table(\"iris\")\n        iris[:, 0] = 0\n\n        self.widget.col_clustering = True\n        self.widget.set_dataset(iris)\n\n    def test_empty_clusters(self):\n        \"\"\"Test if empty clusters are not displayed and warning is shown\"\"\"\n        data = np.array([1, 1, 1, 2, 2, 2, 3, 3, 3])\n\n        table = Table.from_numpy(Domain([ContinuousVariable(\"y\")]),\n                                 data.reshape((9, 1)))\n        self.widget.controls.merge_kmeans.setChecked(True)\n\n        # Test tests that widget handles this exact condition\n        warnings.filterwarnings(\"ignore\", \"Number of distinct clusters\",\n                                ConvergenceWarning)\n        self.send_signal(self.widget.Inputs.data, table)\n\n        self.assertTrue(self.widget.Warning.empty_clusters.is_shown())\n        self.assertEqual(len(self.widget.merge_indices), 3)\n\n    def test_use_enough_colors(self):\n        # Before 201906 thresholds modified the palette and decreased\n        # the number of colors used.\n        data = np.arange(1000).reshape(-1, 1)\n        table = Table.from_numpy(Domain([ContinuousVariable(\"y\")]), data)\n        self.send_signal(self.widget.Inputs.data, table)\n        self.widget.threshold_high = 0.05\n        self.widget.update_color_schema()\n        heatmap_widget = self.widget.scene.widget.heatmap_widget_grid[0][0]\n        image = heatmap_widget.pixmap().toImage()\n        colors = image_row_colors(image)\n        unique_colors = len(np.unique(colors, axis=0))\n        self.assertLessEqual(len(data)*self.widget.threshold_low, unique_colors)\n\n    def test_cls_with_single_instance(self):\n        table = Table(Domain([ContinuousVariable(\"c1\")],\n                             [DiscreteVariable(\"c2\", values=(\"a\", \"b\"))]),\n                      np.array([[1], [2], [3]]), np.array([[0], [0], [1]]))\n        self.send_signal(self.widget.Inputs.data, table)\n        self.widget.set_row_clustering(Clustering.Clustering)\n\n    def test_unconditional_commit_on_new_signal(self):\n        with patch.object(self.widget, 'unconditional_commit') as commit:\n            self.widget.auto_commit = False\n            commit.reset_mock()\n            self.send_signal(self.widget.Inputs.data, self.titanic)\n            commit.assert_called()\n\n    def test_saved_selection(self):\n        iris = Table(\"iris\")\n\n        self.send_signal(self.widget.Inputs.data, iris)\n        selected_indices = list(range(10, 31))\n        self.widget.scene.widget.selectRows(selected_indices)\n        self.widget.on_selection_finished()\n        settings = self.widget.settingsHandler.pack_data(self.widget)\n\n        w = self.create_widget(OWHeatMap, stored_settings=settings)\n        self.send_signal(w.Inputs.data, iris, widget=w)\n        self.assertEqual(len(self.get_output(w.Outputs.selected_data)), 21)\n\n    def test_set_split_var(self):\n        data = self.brown_selected[::3]\n        w = self.widget\n        self.send_signal(self.widget.Inputs.data, data, widget=w)\n        self.assertIs(w.split_by_var, data.domain.class_var)\n        self.assertEqual(len(w.parts.rows),\n                         len(data.domain.class_var.values))\n        w.set_split_variable(None)\n        self.assertIs(w.split_by_var, None)\n        self.assertEqual(len(w.parts.rows), 1)\n\n    def test_set_split_var_missing(self):\n        data = self.brown_selected[::3].copy()\n        data.Y[::5] = np.nan\n        w = self.widget\n        self.send_signal(self.widget.Inputs.data, data, widget=w)\n        self.assertIs(w.split_by_var, data.domain.class_var)\n        self.assertEqual(len(w.parts.rows),\n                         len(data.domain.class_var.values) + 1)\n\n    def test_palette_centering(self):\n        data = np.arange(2).reshape(-1, 1)\n        table = Table.from_numpy(Domain([ContinuousVariable(\"y\")]), data)\n        self.send_signal(self.widget.Inputs.data, table)\n\n        self.widget.color_palette = lambda: \\\n            colorpalettes.ContinuousPalette.from_colors(\n                (0, 255, 0), (255, 0, 0), (0, 0, 0)).lookup_table()\n\n        desired_uncentered = [[0, 255, 0],\n                              [255, 0, 0]]\n\n        desired_centered = [[0, 0, 0],\n                            [255, 0, 0]]\n\n        for center, desired in [(False, desired_uncentered), (True, desired_centered)]:\n            with patch.object(OWHeatMap, \"center_palette\", center):\n                self.widget.update_color_schema()\n                heatmap_widget = self.widget.scene.widget.heatmap_widget_grid[0][0]\n                image = heatmap_widget.pixmap().toImage()\n                colors = image_row_colors(image)\n                np.testing.assert_almost_equal(colors, desired)\n\n    def test_palette_center(self):\n        widget = self.widget\n        model = widget.color_cb.model()\n        for idx in range(model.rowCount(QModelIndex())):\n            palette = model.data(model.index(idx, 0), Qt.UserRole)\n            if palette is None:\n                continue\n            widget.color_cb.setCurrentIndex(idx)\n            self.assertEqual(widget.center_palette,\n                             bool(palette.flags & palette.Diverging))\n\n    def test_migrate_settings_v3(self):\n        w = self.create_widget(\n            OWHeatMap, stored_settings={\n                \"row_clustering\": False,\n                \"col_clustering\": True,\n            }\n        )\n        self.assertEqual(w.row_clustering, Clustering.None_)\n        self.assertEqual(w.col_clustering, Clustering.OrderedClustering)\n\n    def test_row_color_annotations(self):\n        widget = self.widget\n        data = Table(\"brown-selected\")[::5]\n        self.send_signal(widget.Inputs.data, data, widget=widget)\n        widget.set_annotation_color_var(data.domain[\"function\"])\n        self.assertTrue(widget.scene.widget.right_side_colors[0].isVisible())\n        widget.set_annotation_color_var(None)\n        self.assertFalse(widget.scene.widget.right_side_colors[0].isVisible())\n\n    def test_summary(self):\n        \"\"\"Check if status bar is updated when data is received\"\"\"\n        info = self.widget.info\n        no_input, no_output = \"No data on input\", \"No data on output\"\n\n        data = self.housing\n        self.send_signal(self.widget.Inputs.data, data)\n        summary, details = f\"{len(data)}\", format_summary_details(data)\n        self.assertEqual(info._StateInfo__input_summary.brief, summary)\n        self.assertEqual(info._StateInfo__input_summary.details, details)\n        self.assertEqual(info._StateInfo__output_summary.brief, \"\")\n        self.assertEqual(info._StateInfo__output_summary.details, no_output)\n        self._select_data()\n        output = self.get_output(self.widget.Outputs.selected_data)\n        summary, details = f\"{len(output)}\", format_summary_details(output)\n        self.assertEqual(info._StateInfo__output_summary.brief, summary)\n        self.assertEqual(info._StateInfo__output_summary.details, details)\n\n        self.send_signal(self.widget.Inputs.data, None)\n        self.assertEqual(info._StateInfo__input_summary.brief, \"\")\n        self.assertEqual(info._StateInfo__input_summary.details, no_input)\n        self.assertEqual(info._StateInfo__output_summary.brief, \"\")\n        self.assertEqual(info._StateInfo__output_summary.details, no_output)\n\n\nif __name__ == \"__main__\":\n    unittest.main()\n"
    ],
    "test_file_names": [
        "test_1.py"
    ],
    "setup_res_code": 0,
    "setup_res_stdout": "Starting Orange installation attempts...\nAttempting installation with Python 3.7...\nrunning build_ext\nrunning build_src\nbuild_src\nbuilding extension \"Orange.classification._simple_tree\" sources\nbuilding extension \"Orange.classification._tree_scorers\" sources\nbuilding extension \"Orange.data._valuecount\" sources\nbuilding extension \"Orange.data._contingency\" sources\nbuilding extension \"Orange.data._io\" sources\nbuilding extension \"Orange.data._variable\" sources\nbuilding extension \"Orange.distance._distance\" sources\nbuilding extension \"Orange.preprocess._discretize\" sources\nbuilding extension \"Orange.preprocess._relieff\" sources\nbuilding extension \"Orange.projection._som\" sources\nbuilding extension \"Orange.widgets.utils._grid_density\" sources\nbuild_src: building npy-pkg config files\ncustomize UnixCCompiler\nC compiler: cc -pthread -Wno-unused-result -Wsign-compare -Wunreachable-code -DNDEBUG -g -fwrapv -O3 -Wall -fPIC -I/tools/deps/include -I/tools/deps/include/ncursesw -fPIC\n\ncreating /tmp/tmpgaot41qc/tmp\ncreating /tmp/tmpgaot41qc/tmp/tmpgaot41qc\ncompile options: '-MMD -MF /tmp/tmpgaot41qc/file.c.d -c'\ncc: /tmp/tmpgaot41qc/file.c\ncustomize UnixCCompiler using build_ext\ncustomize UnixCCompiler\nC compiler: cc -pthread -Wno-unused-result -Wsign-compare -Wunreachable-code -DNDEBUG -g -fwrapv -O3 -Wall -fPIC -I/tools/deps/include -I/tools/deps/include/ncursesw -fPIC\n\ncreating /tmp/tmp5rqp07na/tmp\ncreating /tmp/tmp5rqp07na/tmp/tmp5rqp07na\ncompile options: '-MMD -MF /tmp/tmp5rqp07na/file.c.d -c'\ncc: /tmp/tmp5rqp07na/file.c\ncustomize UnixCCompiler using build_ext\nbuilding 'Orange.classification._simple_tree' extension\ncompiling C sources\nC compiler: cc -pthread -Wno-unused-result -Wsign-compare -Wunreachable-code -DNDEBUG -g -fwrapv -O3 -Wall -fPIC -I/tools/deps/include -I/tools/deps/include/ncursesw -fPIC\n\ncreating build/temp.linux-x86_64-3.7/Orange\ncreating build/temp.linux-x86_64-3.7/Orange/classification\ncompile options: '-I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_20b9209e1f6a79c01b34223d47ba6b05838c8fce/.venv/lib/python3.7/site-packages/numpy/core/include -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_20b9209e1f6a79c01b34223d47ba6b05838c8fce/.venv/include -I/home/gcpuser/.local/share/uv/python/cpython-3.7.9-linux-x86_64-gnu/include/python3.7m -c'\ncc: Orange/classification/_simple_tree.c\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 Orange/classification/_simple_tree.cpython-37m-x86_64-linux-gnu.so\nbuilding 'Orange.classification._tree_scorers' extension\ncompiling C sources\nC compiler: cc -pthread -Wno-unused-result -Wsign-compare -Wunreachable-code -DNDEBUG -g -fwrapv -O3 -Wall -fPIC -I/tools/deps/include -I/tools/deps/include/ncursesw -fPIC\n\ncompile options: '-I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_20b9209e1f6a79c01b34223d47ba6b05838c8fce/.venv/lib/python3.7/site-packages/numpy/core/include -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_20b9209e1f6a79c01b34223d47ba6b05838c8fce/.venv/lib/python3.7/site-packages/numpy/core/include -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_20b9209e1f6a79c01b34223d47ba6b05838c8fce/.venv/include -I/home/gcpuser/.local/share/uv/python/cpython-3.7.9-linux-x86_64-gnu/include/python3.7m -c'\ncc: Orange/classification/_tree_scorers.c\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 Orange/classification/_tree_scorers.cpython-37m-x86_64-linux-gnu.so\nbuilding 'Orange.data._valuecount' extension\ncompiling C sources\nC compiler: cc -pthread -Wno-unused-result -Wsign-compare -Wunreachable-code -DNDEBUG -g -fwrapv -O3 -Wall -fPIC -I/tools/deps/include -I/tools/deps/include/ncursesw -fPIC\n\ncreating build/temp.linux-x86_64-3.7/Orange/data\ncompile options: '-I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_20b9209e1f6a79c01b34223d47ba6b05838c8fce/.venv/lib/python3.7/site-packages/numpy/core/include -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_20b9209e1f6a79c01b34223d47ba6b05838c8fce/.venv/lib/python3.7/site-packages/numpy/core/include -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_20b9209e1f6a79c01b34223d47ba6b05838c8fce/.venv/include -I/home/gcpuser/.local/share/uv/python/cpython-3.7.9-linux-x86_64-gnu/include/python3.7m -c'\ncc: Orange/data/_valuecount.c\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 Orange/data/_valuecount.cpython-37m-x86_64-linux-gnu.so\nbuilding 'Orange.data._contingency' extension\ncompiling C sources\nC compiler: cc -pthread -Wno-unused-result -Wsign-compare -Wunreachable-code -DNDEBUG -g -fwrapv -O3 -Wall -fPIC -I/tools/deps/include -I/tools/deps/include/ncursesw -fPIC\n\ncompile options: '-I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_20b9209e1f6a79c01b34223d47ba6b05838c8fce/.venv/lib/python3.7/site-packages/numpy/core/include -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_20b9209e1f6a79c01b34223d47ba6b05838c8fce/.venv/lib/python3.7/site-packages/numpy/core/include -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_20b9209e1f6a79c01b34223d47ba6b05838c8fce/.venv/include -I/home/gcpuser/.local/share/uv/python/cpython-3.7.9-linux-x86_64-gnu/include/python3.7m -c'\ncc: Orange/data/_contingency.c\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 Orange/data/_contingency.cpython-37m-x86_64-linux-gnu.so\nbuilding 'Orange.data._io' extension\ncompiling C sources\nC compiler: cc -pthread -Wno-unused-result -Wsign-compare -Wunreachable-code -DNDEBUG -g -fwrapv -O3 -Wall -fPIC -I/tools/deps/include -I/tools/deps/include/ncursesw -fPIC\n\ncompile options: '-I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_20b9209e1f6a79c01b34223d47ba6b05838c8fce/.venv/lib/python3.7/site-packages/numpy/core/include -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_20b9209e1f6a79c01b34223d47ba6b05838c8fce/.venv/lib/python3.7/site-packages/numpy/core/include -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_20b9209e1f6a79c01b34223d47ba6b05838c8fce/.venv/include -I/home/gcpuser/.local/share/uv/python/cpython-3.7.9-linux-x86_64-gnu/include/python3.7m -c'\ncc: Orange/data/_io.c\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 Orange/data/_io.cpython-37m-x86_64-linux-gnu.so\nbuilding 'Orange.data._variable' extension\ncompiling C sources\nC compiler: cc -pthread -Wno-unused-result -Wsign-compare -Wunreachable-code -DNDEBUG -g -fwrapv -O3 -Wall -fPIC -I/tools/deps/include -I/tools/deps/include/ncursesw -fPIC\n\ncompile options: '-I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_20b9209e1f6a79c01b34223d47ba6b05838c8fce/.venv/lib/python3.7/site-packages/numpy/core/include -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_20b9209e1f6a79c01b34223d47ba6b05838c8fce/.venv/lib/python3.7/site-packages/numpy/core/include -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_20b9209e1f6a79c01b34223d47ba6b05838c8fce/.venv/include -I/home/gcpuser/.local/share/uv/python/cpython-3.7.9-linux-x86_64-gnu/include/python3.7m -c'\ncc: Orange/data/_variable.c\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 Orange/data/_variable.cpython-37m-x86_64-linux-gnu.so\nbuilding 'Orange.distance._distance' extension\ncompiling C sources\nC compiler: cc -pthread -Wno-unused-result -Wsign-compare -Wunreachable-code -DNDEBUG -g -fwrapv -O3 -Wall -fPIC -I/tools/deps/include -I/tools/deps/include/ncursesw -fPIC\n\ncreating build/temp.linux-x86_64-3.7/Orange/distance\ncompile options: '-I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_20b9209e1f6a79c01b34223d47ba6b05838c8fce/.venv/lib/python3.7/site-packages/numpy/core/include -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_20b9209e1f6a79c01b34223d47ba6b05838c8fce/.venv/lib/python3.7/site-packages/numpy/core/include -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_20b9209e1f6a79c01b34223d47ba6b05838c8fce/.venv/include -I/home/gcpuser/.local/share/uv/python/cpython-3.7.9-linux-x86_64-gnu/include/python3.7m -c'\ncc: Orange/distance/_distance.c\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 Orange/distance/_distance.cpython-37m-x86_64-linux-gnu.so\nbuilding 'Orange.preprocess._discretize' extension\ncompiling C sources\nC compiler: cc -pthread -Wno-unused-result -Wsign-compare -Wunreachable-code -DNDEBUG -g -fwrapv -O3 -Wall -fPIC -I/tools/deps/include -I/tools/deps/include/ncursesw -fPIC\n\ncreating build/temp.linux-x86_64-3.7/Orange/preprocess\ncompile options: '-I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_20b9209e1f6a79c01b34223d47ba6b05838c8fce/.venv/lib/python3.7/site-packages/numpy/core/include -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_20b9209e1f6a79c01b34223d47ba6b05838c8fce/.venv/lib/python3.7/site-packages/numpy/core/include -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_20b9209e1f6a79c01b34223d47ba6b05838c8fce/.venv/include -I/home/gcpuser/.local/share/uv/python/cpython-3.7.9-linux-x86_64-gnu/include/python3.7m -c'\ncc: Orange/preprocess/_discretize.c\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 Orange/preprocess/_discretize.cpython-37m-x86_64-linux-gnu.so\nbuilding 'Orange.preprocess._relieff' extension\ncompiling C++ sources\nC compiler: c++ -pthread -Wno-unused-result -Wsign-compare -Wunreachable-code -DNDEBUG -g -fwrapv -O3 -Wall -fPIC -I/tools/deps/include -I/tools/deps/include/ncursesw -fPIC\n\ncompile options: '-I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_20b9209e1f6a79c01b34223d47ba6b05838c8fce/.venv/lib/python3.7/site-packages/numpy/core/include -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_20b9209e1f6a79c01b34223d47ba6b05838c8fce/.venv/lib/python3.7/site-packages/numpy/core/include -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_20b9209e1f6a79c01b34223d47ba6b05838c8fce/.venv/include -I/home/gcpuser/.local/share/uv/python/cpython-3.7.9-linux-x86_64-gnu/include/python3.7m -c'\nc++: Orange/preprocess/_relieff.cpp\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 Orange/preprocess/_relieff.cpython-37m-x86_64-linux-gnu.so\nbuilding 'Orange.projection._som' extension\ncompiling C sources\nC compiler: cc -pthread -Wno-unused-result -Wsign-compare -Wunreachable-code -DNDEBUG -g -fwrapv -O3 -Wall -fPIC -I/tools/deps/include -I/tools/deps/include/ncursesw -fPIC\n\ncreating build/temp.linux-x86_64-3.7/Orange/projection\ncompile options: '-I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_20b9209e1f6a79c01b34223d47ba6b05838c8fce/.venv/lib/python3.7/site-packages/numpy/core/include -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_20b9209e1f6a79c01b34223d47ba6b05838c8fce/.venv/lib/python3.7/site-packages/numpy/core/include -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_20b9209e1f6a79c01b34223d47ba6b05838c8fce/.venv/include -I/home/gcpuser/.local/share/uv/python/cpython-3.7.9-linux-x86_64-gnu/include/python3.7m -c'\ncc: Orange/projection/_som.c\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 Orange/projection/_som.cpython-37m-x86_64-linux-gnu.so\nbuilding 'Orange.widgets.utils._grid_density' extension\ncompiling C++ sources\nC compiler: c++ -pthread -Wno-unused-result -Wsign-compare -Wunreachable-code -DNDEBUG -g -fwrapv -O3 -Wall -fPIC -I/tools/deps/include -I/tools/deps/include/ncursesw -fPIC\n\ncreating build/temp.linux-x86_64-3.7/Orange/widgets\ncreating build/temp.linux-x86_64-3.7/Orange/widgets/utils\ncompile options: '-I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_20b9209e1f6a79c01b34223d47ba6b05838c8fce/.venv/lib/python3.7/site-packages/numpy/core/include -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_20b9209e1f6a79c01b34223d47ba6b05838c8fce/.venv/include -I/home/gcpuser/.local/share/uv/python/cpython-3.7.9-linux-x86_64-gnu/include/python3.7m -c'\nc++: Orange/widgets/utils/_grid_density.cpp\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 Orange/widgets/utils/_grid_density.cpython-37m-x86_64-linux-gnu.so\nrunning develop\nrunning build_scripts\nrunning egg_info\nrunning build_src\nbuild_src\nbuilding extension \"Orange.classification._simple_tree\" sources\nbuilding extension \"Orange.classification._tree_scorers\" sources\nbuilding extension \"Orange.data._valuecount\" sources\nbuilding extension \"Orange.data._contingency\" sources\nbuilding extension \"Orange.data._io\" sources\nbuilding extension \"Orange.data._variable\" sources\nbuilding extension \"Orange.distance._distance\" sources\nbuilding extension \"Orange.preprocess._discretize\" sources\nbuilding extension \"Orange.preprocess._relieff\" sources\nbuilding extension \"Orange.projection._som\" sources\nbuilding extension \"Orange.widgets.utils._grid_density\" sources\nbuild_src: building npy-pkg config files\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'\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 '*.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 'COPYING'\nadding license file 'LICENSE'\nwriting manifest file 'Orange3.egg-info/SOURCES.txt'\nrunning build_ext\ncustomize UnixCCompiler\nC compiler: cc -pthread -Wno-unused-result -Wsign-compare -Wunreachable-code -DNDEBUG -g -fwrapv -O3 -Wall -fPIC -I/tools/deps/include -I/tools/deps/include/ncursesw -fPIC\n\ncreating /tmp/tmphfnka11c/tmp\ncreating /tmp/tmphfnka11c/tmp/tmphfnka11c\ncompile options: '-MMD -MF /tmp/tmphfnka11c/file.c.d -c'\ncc: /tmp/tmphfnka11c/file.c\ncustomize UnixCCompiler using build_ext\ncustomize UnixCCompiler\nC compiler: cc -pthread -Wno-unused-result -Wsign-compare -Wunreachable-code -DNDEBUG -g -fwrapv -O3 -Wall -fPIC -I/tools/deps/include -I/tools/deps/include/ncursesw -fPIC\n\ncreating /tmp/tmpnnx6a9da/tmp\ncreating /tmp/tmpnnx6a9da/tmp/tmpnnx6a9da\ncompile options: '-MMD -MF /tmp/tmpnnx6a9da/file.c.d -c'\ncc: /tmp/tmpnnx6a9da/file.c\ncustomize UnixCCompiler using build_ext\nCreating /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_20b9209e1f6a79c01b34223d47ba6b05838c8fce/.venv/lib/python3.7/site-packages/Orange3.egg-link (link to .)\nAdding Orange3 3.26.0.dev0+20b9209 to easy-install.pth file\nInstalling orange-canvas script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_20b9209e1f6a79c01b34223d47ba6b05838c8fce/.venv/bin\n\nInstalled /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_20b9209e1f6a79c01b34223d47ba6b05838c8fce\nProcessing dependencies for Orange3==3.26.0.dev0+20b9209\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_20b9209e1f6a79c01b34223d47ba6b05838c8fce/.venv/lib/python3.7/site-packages\nSearching for xlrd==2.0.1\nBest match: xlrd 2.0.1\nAdding xlrd 2.0.1 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_20b9209e1f6a79c01b34223d47ba6b05838c8fce/.venv/lib/python3.7/site-packages\nSearching for setuptools==59.8.0\nBest match: setuptools 59.8.0\nAdding setuptools 59.8.0 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_20b9209e1f6a79c01b34223d47ba6b05838c8fce/.venv/lib/python3.7/site-packages\nSearching for serverfiles==0.3.1\nBest match: serverfiles 0.3.1\nAdding serverfiles 0.3.1 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_20b9209e1f6a79c01b34223d47ba6b05838c8fce/.venv/lib/python3.7/site-packages\nSearching for scipy==1.7.3\nBest match: scipy 1.7.3\nAdding scipy 1.7.3 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_20b9209e1f6a79c01b34223d47ba6b05838c8fce/.venv/lib/python3.7/site-packages\nSearching for scikit-learn==1.0.2\nBest match: scikit-learn 1.0.2\nAdding scikit-learn 1.0.2 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_20b9209e1f6a79c01b34223d47ba6b05838c8fce/.venv/lib/python3.7/site-packages\nSearching for requests==2.31.0\nBest match: requests 2.31.0\nAdding requests 2.31.0 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_20b9209e1f6a79c01b34223d47ba6b05838c8fce/.venv/lib/python3.7/site-packages\nSearching for PyYAML==6.0.1\nBest match: PyYAML 6.0.1\nAdding PyYAML 6.0.1 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_20b9209e1f6a79c01b34223d47ba6b05838c8fce/.venv/lib/python3.7/site-packages\nSearching for python-louvain==0.16\nBest match: python-louvain 0.16\nAdding python-louvain 0.16 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_20b9209e1f6a79c01b34223d47ba6b05838c8fce/.venv/lib/python3.7/site-packages\nSearching for pyqtgraph==0.12.4\nBest match: pyqtgraph 0.12.4\nAdding pyqtgraph 0.12.4 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_20b9209e1f6a79c01b34223d47ba6b05838c8fce/.venv/lib/python3.7/site-packages\nSearching for pip==24.0\nBest match: pip 24.0\nAdding pip 24.0 to easy-install.pth file\nInstalling pip script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_20b9209e1f6a79c01b34223d47ba6b05838c8fce/.venv/bin\nInstalling pip3 script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_20b9209e1f6a79c01b34223d47ba6b05838c8fce/.venv/bin\nInstalling pip3.10 script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_20b9209e1f6a79c01b34223d47ba6b05838c8fce/.venv/bin\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_20b9209e1f6a79c01b34223d47ba6b05838c8fce/.venv/lib/python3.7/site-packages\nSearching for pandas==1.3.5\nBest match: pandas 1.3.5\nAdding pandas 1.3.5 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_20b9209e1f6a79c01b34223d47ba6b05838c8fce/.venv/lib/python3.7/site-packages\nSearching for orange-widget-base==4.22.0\nBest match: orange-widget-base 4.22.0\nAdding orange-widget-base 4.22.0 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_20b9209e1f6a79c01b34223d47ba6b05838c8fce/.venv/lib/python3.7/site-packages\nSearching for orange-canvas-core==0.1.35\nBest match: orange-canvas-core 0.1.35\nAdding orange-canvas-core 0.1.35 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_20b9209e1f6a79c01b34223d47ba6b05838c8fce/.venv/lib/python3.7/site-packages\nSearching for openpyxl==3.1.3\nBest match: openpyxl 3.1.3\nAdding openpyxl 3.1.3 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_20b9209e1f6a79c01b34223d47ba6b05838c8fce/.venv/lib/python3.7/site-packages\nSearching for openTSNE==1.0.1\nBest match: openTSNE 1.0.1\nAdding openTSNE 1.0.1 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_20b9209e1f6a79c01b34223d47ba6b05838c8fce/.venv/lib/python3.7/site-packages\nSearching for numpy==1.17.5\nBest match: numpy 1.17.5\nAdding numpy 1.17.5 to easy-install.pth file\nInstalling f2py script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_20b9209e1f6a79c01b34223d47ba6b05838c8fce/.venv/bin\nInstalling f2py3 script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_20b9209e1f6a79c01b34223d47ba6b05838c8fce/.venv/bin\nInstalling f2py3.7 script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_20b9209e1f6a79c01b34223d47ba6b05838c8fce/.venv/bin\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_20b9209e1f6a79c01b34223d47ba6b05838c8fce/.venv/lib/python3.7/site-packages\nSearching for networkx==2.6.3\nBest match: networkx 2.6.3\nAdding networkx 2.6.3 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_20b9209e1f6a79c01b34223d47ba6b05838c8fce/.venv/lib/python3.7/site-packages\nSearching for matplotlib==3.5.3\nBest match: matplotlib 3.5.3\nAdding matplotlib 3.5.3 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_20b9209e1f6a79c01b34223d47ba6b05838c8fce/.venv/lib/python3.7/site-packages\nSearching for keyrings.alt==4.2.0\nBest match: keyrings.alt 4.2.0\nAdding keyrings.alt 4.2.0 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_20b9209e1f6a79c01b34223d47ba6b05838c8fce/.venv/lib/python3.7/site-packages\nSearching for keyring==24.1.1\nBest match: keyring 24.1.1\nAdding keyring 24.1.1 to easy-install.pth file\nInstalling keyring script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_20b9209e1f6a79c01b34223d47ba6b05838c8fce/.venv/bin\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_20b9209e1f6a79c01b34223d47ba6b05838c8fce/.venv/lib/python3.7/site-packages\nSearching for joblib==1.3.2\nBest match: joblib 1.3.2\nAdding joblib 1.3.2 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_20b9209e1f6a79c01b34223d47ba6b05838c8fce/.venv/lib/python3.7/site-packages\nSearching for httpx==0.24.1\nBest match: httpx 0.24.1\nAdding httpx 0.24.1 to easy-install.pth file\nInstalling httpx script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_20b9209e1f6a79c01b34223d47ba6b05838c8fce/.venv/bin\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_20b9209e1f6a79c01b34223d47ba6b05838c8fce/.venv/lib/python3.7/site-packages\nSearching for docutils==0.20.1\nBest match: docutils 0.20.1\nAdding docutils 0.20.1 to easy-install.pth file\nInstalling docutils script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_20b9209e1f6a79c01b34223d47ba6b05838c8fce/.venv/bin\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_20b9209e1f6a79c01b34223d47ba6b05838c8fce/.venv/lib/python3.7/site-packages\nSearching for chardet==5.2.0\nBest match: chardet 5.2.0\nAdding chardet 5.2.0 to easy-install.pth file\nInstalling chardetect script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_20b9209e1f6a79c01b34223d47ba6b05838c8fce/.venv/bin\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_20b9209e1f6a79c01b34223d47ba6b05838c8fce/.venv/lib/python3.7/site-packages\nSearching for Bottleneck==1.4.0\nBest match: Bottleneck 1.4.0\nAdding Bottleneck 1.4.0 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_20b9209e1f6a79c01b34223d47ba6b05838c8fce/.venv/lib/python3.7/site-packages\nSearching for baycomp==1.0.3\nBest match: baycomp 1.0.3\nAdding baycomp 1.0.3 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_20b9209e1f6a79c01b34223d47ba6b05838c8fce/.venv/lib/python3.7/site-packages\nSearching for AnyQt==0.2.0\nBest match: AnyQt 0.2.0\nAdding AnyQt 0.2.0 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_20b9209e1f6a79c01b34223d47ba6b05838c8fce/.venv/lib/python3.7/site-packages\nSearching for threadpoolctl==3.1.0\nBest match: threadpoolctl 3.1.0\nAdding threadpoolctl 3.1.0 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_20b9209e1f6a79c01b34223d47ba6b05838c8fce/.venv/lib/python3.7/site-packages\nSearching for urllib3==2.0.7\nBest match: urllib3 2.0.7\nAdding urllib3 2.0.7 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_20b9209e1f6a79c01b34223d47ba6b05838c8fce/.venv/lib/python3.7/site-packages\nSearching for certifi==2024.12.14\nBest match: certifi 2024.12.14\nAdding certifi 2024.12.14 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_20b9209e1f6a79c01b34223d47ba6b05838c8fce/.venv/lib/python3.7/site-packages\nSearching for idna==3.10\nBest match: idna 3.10\nAdding idna 3.10 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_20b9209e1f6a79c01b34223d47ba6b05838c8fce/.venv/lib/python3.7/site-packages\nSearching for charset-normalizer==3.4.1\nBest match: charset-normalizer 3.4.1\nAdding charset-normalizer 3.4.1 to easy-install.pth file\nInstalling normalizer script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_20b9209e1f6a79c01b34223d47ba6b05838c8fce/.venv/bin\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_20b9209e1f6a79c01b34223d47ba6b05838c8fce/.venv/lib/python3.7/site-packages\nSearching for python-dateutil==2.9.0.post0\nBest match: python-dateutil 2.9.0.post0\nAdding python-dateutil 2.9.0.post0 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_20b9209e1f6a79c01b34223d47ba6b05838c8fce/.venv/lib/python3.7/site-packages\nSearching for pytz==2024.2\nBest match: pytz 2024.2\nAdding pytz 2024.2 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_20b9209e1f6a79c01b34223d47ba6b05838c8fce/.venv/lib/python3.7/site-packages\nSearching for typing-extensions==4.7.1\nBest match: typing-extensions 4.7.1\nAdding typing-extensions 4.7.1 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_20b9209e1f6a79c01b34223d47ba6b05838c8fce/.venv/lib/python3.7/site-packages\nSearching for dictdiffer==0.9.0\nBest match: dictdiffer 0.9.0\nAdding dictdiffer 0.9.0 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_20b9209e1f6a79c01b34223d47ba6b05838c8fce/.venv/lib/python3.7/site-packages\nSearching for importlib-metadata==6.7.0\nBest match: importlib-metadata 6.7.0\nAdding importlib-metadata 6.7.0 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_20b9209e1f6a79c01b34223d47ba6b05838c8fce/.venv/lib/python3.7/site-packages\nSearching for qasync==0.25.0\nBest match: qasync 0.25.0\nAdding qasync 0.25.0 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_20b9209e1f6a79c01b34223d47ba6b05838c8fce/.venv/lib/python3.7/site-packages\nSearching for packaging==24.0\nBest match: packaging 24.0\nAdding packaging 24.0 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_20b9209e1f6a79c01b34223d47ba6b05838c8fce/.venv/lib/python3.7/site-packages\nSearching for commonmark==0.9.1\nBest match: commonmark 0.9.1\nAdding commonmark 0.9.1 to easy-install.pth file\nInstalling cmark script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_20b9209e1f6a79c01b34223d47ba6b05838c8fce/.venv/bin\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_20b9209e1f6a79c01b34223d47ba6b05838c8fce/.venv/lib/python3.7/site-packages\nSearching for requests-cache==1.1.1\nBest match: requests-cache 1.1.1\nAdding requests-cache 1.1.1 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_20b9209e1f6a79c01b34223d47ba6b05838c8fce/.venv/lib/python3.7/site-packages\nSearching for et-xmlfile==1.1.0\nBest match: et-xmlfile 1.1.0\nAdding et-xmlfile 1.1.0 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_20b9209e1f6a79c01b34223d47ba6b05838c8fce/.venv/lib/python3.7/site-packages\nSearching for fonttools==4.38.0\nBest match: fonttools 4.38.0\nAdding fonttools 4.38.0 to easy-install.pth file\nInstalling fonttools script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_20b9209e1f6a79c01b34223d47ba6b05838c8fce/.venv/bin\nInstalling pyftmerge script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_20b9209e1f6a79c01b34223d47ba6b05838c8fce/.venv/bin\nInstalling pyftsubset script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_20b9209e1f6a79c01b34223d47ba6b05838c8fce/.venv/bin\nInstalling ttx script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_20b9209e1f6a79c01b34223d47ba6b05838c8fce/.venv/bin\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_20b9209e1f6a79c01b34223d47ba6b05838c8fce/.venv/lib/python3.7/site-packages\nSearching for Pillow==9.5.0\nBest match: Pillow 9.5.0\nAdding Pillow 9.5.0 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_20b9209e1f6a79c01b34223d47ba6b05838c8fce/.venv/lib/python3.7/site-packages\nSearching for pyparsing==3.1.4\nBest match: pyparsing 3.1.4\nAdding pyparsing 3.1.4 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_20b9209e1f6a79c01b34223d47ba6b05838c8fce/.venv/lib/python3.7/site-packages\nSearching for kiwisolver==1.4.5\nBest match: kiwisolver 1.4.5\nAdding kiwisolver 1.4.5 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_20b9209e1f6a79c01b34223d47ba6b05838c8fce/.venv/lib/python3.7/site-packages\nSearching for cycler==0.11.0\nBest match: cycler 0.11.0\nAdding cycler 0.11.0 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_20b9209e1f6a79c01b34223d47ba6b05838c8fce/.venv/lib/python3.7/site-packages\nSearching for jaraco.classes==3.2.3\nBest match: jaraco.classes 3.2.3\nAdding jaraco.classes 3.2.3 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_20b9209e1f6a79c01b34223d47ba6b05838c8fce/.venv/lib/python3.7/site-packages\nSearching for jeepney==0.8.0\nBest match: jeepney 0.8.0\nAdding jeepney 0.8.0 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_20b9209e1f6a79c01b34223d47ba6b05838c8fce/.venv/lib/python3.7/site-packages\nSearching for SecretStorage==3.3.3\nBest match: SecretStorage 3.3.3\nAdding SecretStorage 3.3.3 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_20b9209e1f6a79c01b34223d47ba6b05838c8fce/.venv/lib/python3.7/site-packages\nSearching for importlib-resources==5.12.0\nBest match: importlib-resources 5.12.0\nAdding importlib-resources 5.12.0 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_20b9209e1f6a79c01b34223d47ba6b05838c8fce/.venv/lib/python3.7/site-packages\nSearching for httpcore==0.17.3\nBest match: httpcore 0.17.3\nAdding httpcore 0.17.3 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_20b9209e1f6a79c01b34223d47ba6b05838c8fce/.venv/lib/python3.7/site-packages\nSearching for sniffio==1.3.1\nBest match: sniffio 1.3.1\nAdding sniffio 1.3.1 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_20b9209e1f6a79c01b34223d47ba6b05838c8fce/.venv/lib/python3.7/site-packages\nSearching for six==1.17.0\nBest match: six 1.17.0\nAdding six 1.17.0 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_20b9209e1f6a79c01b34223d47ba6b05838c8fce/.venv/lib/python3.7/site-packages\nSearching for zipp==3.15.0\nBest match: zipp 3.15.0\nAdding zipp 3.15.0 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_20b9209e1f6a79c01b34223d47ba6b05838c8fce/.venv/lib/python3.7/site-packages\nSearching for cattrs==23.1.2\nBest match: cattrs 23.1.2\nAdding cattrs 23.1.2 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_20b9209e1f6a79c01b34223d47ba6b05838c8fce/.venv/lib/python3.7/site-packages\nSearching for platformdirs==4.0.0\nBest match: platformdirs 4.0.0\nAdding platformdirs 4.0.0 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_20b9209e1f6a79c01b34223d47ba6b05838c8fce/.venv/lib/python3.7/site-packages\nSearching for url-normalize==1.4.3\nBest match: url-normalize 1.4.3\nAdding url-normalize 1.4.3 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_20b9209e1f6a79c01b34223d47ba6b05838c8fce/.venv/lib/python3.7/site-packages\nSearching for attrs==24.2.0\nBest match: attrs 24.2.0\nAdding attrs 24.2.0 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_20b9209e1f6a79c01b34223d47ba6b05838c8fce/.venv/lib/python3.7/site-packages\nSearching for more-itertools==9.1.0\nBest match: more-itertools 9.1.0\nAdding more-itertools 9.1.0 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_20b9209e1f6a79c01b34223d47ba6b05838c8fce/.venv/lib/python3.7/site-packages\nSearching for cryptography==44.0.0\nBest match: cryptography 44.0.0\nAdding cryptography 44.0.0 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_20b9209e1f6a79c01b34223d47ba6b05838c8fce/.venv/lib/python3.7/site-packages\nSearching for anyio==3.7.1\nBest match: anyio 3.7.1\nAdding anyio 3.7.1 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_20b9209e1f6a79c01b34223d47ba6b05838c8fce/.venv/lib/python3.7/site-packages\nSearching for h11==0.14.0\nBest match: h11 0.14.0\nAdding h11 0.14.0 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_20b9209e1f6a79c01b34223d47ba6b05838c8fce/.venv/lib/python3.7/site-packages\nSearching for exceptiongroup==1.2.2\nBest match: exceptiongroup 1.2.2\nAdding exceptiongroup 1.2.2 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_20b9209e1f6a79c01b34223d47ba6b05838c8fce/.venv/lib/python3.7/site-packages\nSearching for cffi==1.15.1\nBest match: cffi 1.15.1\nAdding cffi 1.15.1 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_20b9209e1f6a79c01b34223d47ba6b05838c8fce/.venv/lib/python3.7/site-packages\nSearching for pycparser==2.21\nBest match: pycparser 2.21\nAdding pycparser 2.21 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_20b9209e1f6a79c01b34223d47ba6b05838c8fce/.venv/lib/python3.7/site-packages\nFinished processing dependencies for Orange3==3.26.0.dev0+20b9209\nVerifying Orange installation...\n✅ Orange installation successful!\nSuccessfully installed orange using Python 3.7\n",
    "setup_res_stderr": "Using CPython 3.7.9\nCreating virtual environment at: .venv\nActivate with: source .venv/bin/activate\nResolved 18 packages in 154ms\nPrepared 18 packages in 30ms\nInstalled 18 packages in 668ms\n + cython==0.29.37\n + exceptiongroup==1.2.2\n + importlib-metadata==6.7.0\n + iniconfig==2.0.0\n + numpy==1.17.5\n + packaging==24.0\n + pluggy==1.2.0\n + pyqt5==5.15.10\n + pyqt5-qt5==5.15.16\n + pyqt5-sip==12.13.0\n + pyqtwebengine==5.15.6\n + pyqtwebengine-qt5==5.15.16\n + pytest==7.4.4\n + setuptools==59.8.0\n + tomli==2.0.1\n + typing-extensions==4.7.1\n + wheel==0.42.0\n + zipp==3.15.0\nResolved 54 packages in 325ms\nInstalled 47 packages in 726ms\n + anyio==3.7.1\n + baycomp==1.0.3\n + bottleneck==1.4.0\n + certifi==2024.12.14\n + cffi==1.15.1\n + chardet==5.2.0\n + charset-normalizer==3.4.1\n + cryptography==44.0.0\n + cycler==0.11.0\n + et-xmlfile==1.1.0\n + fonttools==4.38.0\n + h11==0.14.0\n + httpcore==0.17.3\n + httpx==0.24.1\n + idna==3.10\n + importlib-resources==5.12.0\n + jaraco-classes==3.2.3\n + jeepney==0.8.0\n + joblib==1.3.2\n + keyring==24.1.1\n + keyrings-alt==4.2.0\n + kiwisolver==1.4.5\n + matplotlib==3.5.3\n + more-itertools==9.1.0\n + networkx==2.6.3\n + openpyxl==3.1.3\n + opentsne==1.0.1\n + pandas==1.3.5\n + pillow==9.5.0\n + pip==24.0\n + pycparser==2.21\n + pyparsing==3.1.4\n + python-dateutil==2.9.0.post0\n + python-louvain==0.16\n + pytz==2024.2\n + pyyaml==6.0.1\n + requests==2.31.0\n + scikit-learn==1.0.2\n + scipy==1.7.3\n + secretstorage==3.3.3\n + serverfiles==0.3.1\n + six==1.17.0\n + sniffio==1.3.1\n + threadpoolctl==3.1.0\n + urllib3==2.0.7\n + xlrd==2.0.1\n + xlsxwriter==3.2.0\nResolved 34 packages in 65ms\nInstalled 13 packages in 95ms\n + anyqt==0.2.0\n + attrs==24.2.0\n + cattrs==23.1.2\n + commonmark==0.9.1\n + dictdiffer==0.9.0\n + docutils==0.20.1\n + orange-canvas-core==0.1.35\n + orange-widget-base==4.22.0\n + platformdirs==4.0.0\n + pyqtgraph==0.12.4\n + qasync==0.25.0\n + requests-cache==1.1.1\n + url-normalize==1.4.3\nResolved 2 packages in 11ms\nInstalled 2 packages in 20ms\n + psycopg2==2.9.9\n + pymssql==2.3.2\nAudited 1 package in 50ms\nAudited 2 packages in 1ms\nOrange/classification/_simple_tree.c: In function ‘gain_ratio_c’:\nOrange/classification/_simple_tree.c:63:9: warning: implicit declaration of function ‘qsort_r’; did you mean ‘qsort’? [-Wimplicit-function-declaration]\n   63 |         qsort_r(base, nel, size, compar, thunk)\n      |         ^~~~~~~\nOrange/classification/_simple_tree.c:147:9: note: in expansion of macro ‘QSORT_R_FUNC’\n  147 |         QSORT_R_FUNC(examples, size, sizeof(struct Example), (void*) &compar_attr, compar_examples);\n      |         ^~~~~~~~~~~~\nIn file included from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_20b9209e1f6a79c01b34223d47ba6b05838c8fce/.venv/lib/python3.7/site-packages/numpy/core/include/numpy/ndarraytypes.h:1830,\n                 from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_20b9209e1f6a79c01b34223d47ba6b05838c8fce/.venv/lib/python3.7/site-packages/numpy/core/include/numpy/ndarrayobject.h:12,\n                 from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_20b9209e1f6a79c01b34223d47ba6b05838c8fce/.venv/lib/python3.7/site-packages/numpy/core/include/numpy/arrayobject.h:4,\n                 from Orange/classification/_tree_scorers.c:567:\n/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_20b9209e1f6a79c01b34223d47ba6b05838c8fce/.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:3115:27: warning: comparison of integer expressions of different signedness: ‘unsigned int’ and ‘int’ [-Wsign-compare]\n 3115 |   __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:4619:27: warning: comparison of integer expressions of different signedness: ‘unsigned int’ and ‘int’ [-Wsign-compare]\n 4619 |   __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:5396:43: warning: comparison of integer expressions of different signedness: ‘unsigned int’ and ‘int’ [-Wsign-compare]\n 5396 |           for (__pyx_t_22 = 0; __pyx_t_22 < __pyx_t_21; __pyx_t_22+=1) {\n      |                                           ^\nOrange/classification/_tree_scorers.c:5517:39: warning: comparison of integer expressions of different signedness: ‘unsigned int’ and ‘int’ [-Wsign-compare]\n 5517 |           __pyx_t_27 = ((__pyx_v_left >= __pyx_v_min_leaf) != 0);\n      |                                       ^~\nOrange/classification/_tree_scorers.c:5523:53: warning: comparison of integer expressions of different signedness: ‘unsigned int’ and ‘int’ [-Wsign-compare]\n 5523 |           __pyx_t_27 = (((__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:7051:41: warning: comparison of integer expressions of different signedness: ‘unsigned int’ and ‘int’ [-Wsign-compare]\n 7051 |         for (__pyx_t_11 = 0; __pyx_t_11 < __pyx_t_10; __pyx_t_11+=1) {\n      |                                         ^\nOrange/classification/_tree_scorers.c: In function ‘__pyx_pf_6Orange_14classification_13_tree_scorers_16compute_predictions_csc’:\nOrange/classification/_tree_scorers.c:7741:41: warning: comparison of integer expressions of different signedness: ‘unsigned int’ and ‘int’ [-Wsign-compare]\n 7741 |         for (__pyx_t_11 = 0; __pyx_t_11 < __pyx_t_10; __pyx_t_11+=1) {\n      |                                         ^\nOrange/classification/_tree_scorers.c:7801:143: warning: comparison of integer expressions of different signedness: ‘__pyx_t_5numpy_int32_t’ {aka ‘int’} and ‘unsigned int’ [-Wsign-compare]\n 7801 |               __pyx_t_17 = (((*((__pyx_t_5numpy_int32_t *) ( /* dim=0 */ (__pyx_v_indices.data + __pyx_t_18 * __pyx_v_indices.strides[0]) ))) != __pyx_v_i) != 0);\n      |                                                                                                                                               ^~\nIn file included from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_20b9209e1f6a79c01b34223d47ba6b05838c8fce/.venv/lib/python3.7/site-packages/numpy/core/include/numpy/ndarraytypes.h:1830,\n                 from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_20b9209e1f6a79c01b34223d47ba6b05838c8fce/.venv/lib/python3.7/site-packages/numpy/core/include/numpy/ndarrayobject.h:12,\n                 from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_20b9209e1f6a79c01b34223d47ba6b05838c8fce/.venv/lib/python3.7/site-packages/numpy/core/include/numpy/arrayobject.h:4,\n                 from Orange/data/_valuecount.c:567:\n/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_20b9209e1f6a79c01b34223d47ba6b05838c8fce/.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_20b9209e1f6a79c01b34223d47ba6b05838c8fce/.venv/lib/python3.7/site-packages/numpy/core/include/numpy/ndarraytypes.h:1830,\n                 from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_20b9209e1f6a79c01b34223d47ba6b05838c8fce/.venv/lib/python3.7/site-packages/numpy/core/include/numpy/ndarrayobject.h:12,\n                 from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_20b9209e1f6a79c01b34223d47ba6b05838c8fce/.venv/lib/python3.7/site-packages/numpy/core/include/numpy/arrayobject.h:4,\n                 from Orange/data/_contingency.c:598:\n/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_20b9209e1f6a79c01b34223d47ba6b05838c8fce/.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:3881:24: warning: ‘__pyx_v_decs’ may be used uninitialized in this function [-Wmaybe-uninitialized]\n 3881 |           __pyx_v_decs = (__pyx_v_decs / 10.0);\n      |           ~~~~~~~~~~~~~^~~~~~~~~~~~~~~~~~~~~~~\nOrange/data/_io.c:4070:24: warning: ‘__pyx_v_value’ may be used uninitialized in this function [-Wmaybe-uninitialized]\n 4070 |           __pyx_t_10 = PyFloat_FromDouble(__pyx_v_value); if (unlikely(!__pyx_t_10)) __PYX_ERR(0, 221, __pyx_L5_error)\n      |                        ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\nOrange/data/_io.c:480:40: warning: ‘__pyx_v_attr_index’ may be used uninitialized in this function [-Wmaybe-uninitialized]\n  480 |   #define PyInt_FromLong               PyLong_FromLong\n      |                                        ^~~~~~~~~~~~~~~\nOrange/data/_io.c:1301:7: note: ‘__pyx_v_attr_index’ was declared here\n 1301 |   int __pyx_v_attr_index;\n      |       ^~~~~~~~~~~~~~~~~~\nOrange/data/_io.c:2935:25: warning: ‘__pyx_v_atomp’ may be used uninitialized in this function [-Wmaybe-uninitialized]\n 2935 |           __pyx_v_atomp = (__pyx_v_atomp + 1);\n      |           ~~~~~~~~~~~~~~^~~~~~~~~~~~~~~~~~~~~\nIn file included from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_20b9209e1f6a79c01b34223d47ba6b05838c8fce/.venv/lib/python3.7/site-packages/numpy/core/include/numpy/ndarraytypes.h:1830,\n                 from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_20b9209e1f6a79c01b34223d47ba6b05838c8fce/.venv/lib/python3.7/site-packages/numpy/core/include/numpy/ndarrayobject.h:12,\n                 from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_20b9209e1f6a79c01b34223d47ba6b05838c8fce/.venv/lib/python3.7/site-packages/numpy/core/include/numpy/arrayobject.h:4,\n                 from Orange/distance/_distance.c:598:\n/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_20b9209e1f6a79c01b34223d47ba6b05838c8fce/.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_20b9209e1f6a79c01b34223d47ba6b05838c8fce/.venv/lib/python3.7/site-packages/numpy/core/include/numpy/ndarraytypes.h:1830,\n                 from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_20b9209e1f6a79c01b34223d47ba6b05838c8fce/.venv/lib/python3.7/site-packages/numpy/core/include/numpy/ndarrayobject.h:12,\n                 from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_20b9209e1f6a79c01b34223d47ba6b05838c8fce/.venv/lib/python3.7/site-packages/numpy/core/include/numpy/arrayobject.h:4,\n                 from Orange/preprocess/_discretize.c:567:\n/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_20b9209e1f6a79c01b34223d47ba6b05838c8fce/.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_20b9209e1f6a79c01b34223d47ba6b05838c8fce/.venv/lib/python3.7/site-packages/numpy/core/include/numpy/ndarraytypes.h:1830,\n                 from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_20b9209e1f6a79c01b34223d47ba6b05838c8fce/.venv/lib/python3.7/site-packages/numpy/core/include/numpy/ndarrayobject.h:12,\n                 from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_20b9209e1f6a79c01b34223d47ba6b05838c8fce/.venv/lib/python3.7/site-packages/numpy/core/include/numpy/arrayobject.h:4,\n                 from Orange/preprocess/_relieff.cpp:581:\n/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_20b9209e1f6a79c01b34223d47ba6b05838c8fce/.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_20b9209e1f6a79c01b34223d47ba6b05838c8fce/.venv/lib/python3.7/site-packages/numpy/core/include/numpy/ndarraytypes.h:1830,\n                 from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_20b9209e1f6a79c01b34223d47ba6b05838c8fce/.venv/lib/python3.7/site-packages/numpy/core/include/numpy/ndarrayobject.h:12,\n                 from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_20b9209e1f6a79c01b34223d47ba6b05838c8fce/.venv/lib/python3.7/site-packages/numpy/core/include/numpy/arrayobject.h:4,\n                 from Orange/projection/_som.c:598:\n/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_20b9209e1f6a79c01b34223d47ba6b05838c8fce/.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:3655:7: warning: ‘__pyx_v_win_y’ may be used uninitialized in this function [-Wmaybe-uninitialized]\n 3655 |   int __pyx_v_win_y;\n      |       ^~~~~~~~~~~~~\nOrange/projection/_som.c:3654:7: warning: ‘__pyx_v_win_x’ may be used uninitialized in this function [-Wmaybe-uninitialized]\n 3654 |   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:4547:223: warning: ‘__pyx_v_win_y’ may be used uninitialized in this function [-Wmaybe-uninitialized]\n 4547 |           *__Pyx_BufPtrStrided2d(__pyx_t_5numpy_int16_t *, __pyx_pybuffernd_winners.rcbuffer->pybuffer.buf, __pyx_t_38, __pyx_pybuffernd_winners.diminfo[0].strides, __pyx_t_39, __pyx_pybuffernd_winners.diminfo[1].strides) = __pyx_v_win_y;\n      |           ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^~~~~~~~~~~~~~~\nOrange/projection/_som.c:4114:7: note: ‘__pyx_v_win_y’ was declared here\n 4114 |   int __pyx_v_win_y;\n      |       ^~~~~~~~~~~~~\nOrange/projection/_som.c:4536:223: warning: ‘__pyx_v_win_x’ may be used uninitialized in this function [-Wmaybe-uninitialized]\n 4536 |           *__Pyx_BufPtrStrided2d(__pyx_t_5numpy_int16_t *, __pyx_pybuffernd_winners.rcbuffer->pybuffer.buf, __pyx_t_36, __pyx_pybuffernd_winners.diminfo[0].strides, __pyx_t_37, __pyx_pybuffernd_winners.diminfo[1].strides) = __pyx_v_win_x;\n      |           ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^~~~~~~~~~~~~~~\nOrange/projection/_som.c:4113:7: note: ‘__pyx_v_win_x’ was declared here\n 4113 |   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:4746:7: warning: ‘__pyx_v_win_y’ may be used uninitialized in this function [-Wmaybe-uninitialized]\n 4746 |   int __pyx_v_win_y;\n      |       ^~~~~~~~~~~~~\nOrange/projection/_som.c:4745:7: warning: ‘__pyx_v_win_x’ may be used uninitialized in this function [-Wmaybe-uninitialized]\n 4745 |   int __pyx_v_win_x;\n      |       ^~~~~~~~~~~~~\nOrange/projection/_som.c: In function ‘__pyx_pw_6Orange_10projection_4_som_3update’:\nOrange/projection/_som.c:3141:7: warning: ‘__pyx_v_win_y’ may be used uninitialized in this function [-Wmaybe-uninitialized]\n 3141 |   int __pyx_v_win_y;\n      |       ^~~~~~~~~~~~~\nOrange/projection/_som.c: In function ‘__pyx_pw_6Orange_10projection_4_som_1get_winners’:\nOrange/projection/_som.c:2964:223: warning: ‘__pyx_v_win_y’ may be used uninitialized in this function [-Wmaybe-uninitialized]\n 2964 |           *__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:2665:7: note: ‘__pyx_v_win_y’ was declared here\n 2665 |   int __pyx_v_win_y;\n      |       ^~~~~~~~~~~~~\nOrange/projection/_som.c:2953:223: warning: ‘__pyx_v_win_x’ may be used uninitialized in this function [-Wmaybe-uninitialized]\n 2953 |           *__Pyx_BufPtrStrided2d(__pyx_t_5numpy_int16_t *, __pyx_pybuffernd_winners.rcbuffer->pybuffer.buf, __pyx_t_25, __pyx_pybuffernd_winners.diminfo[0].strides, __pyx_t_26, __pyx_pybuffernd_winners.diminfo[1].strides) = __pyx_v_win_x;\n      |           ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^~~~~~~~~~~~~~~\nOrange/projection/_som.c:2664:7: note: ‘__pyx_v_win_x’ was declared here\n 2664 |   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:5515:7: warning: ‘__pyx_v_win_y’ may be used uninitialized in this function [-Wmaybe-uninitialized]\n 5515 |   int __pyx_v_win_y;\n      |       ^~~~~~~~~~~~~\nOrange/projection/_som.c:5514:7: warning: ‘__pyx_v_win_x’ may be used uninitialized in this function [-Wmaybe-uninitialized]\n 5514 |   int __pyx_v_win_x;\n      |       ^~~~~~~~~~~~~\n/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_20b9209e1f6a79c01b34223d47ba6b05838c8fce/.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_20b9209e1f6a79c01b34223d47ba6b05838c8fce/.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_20b9209e1f6a79c01b34223d47ba6b05838c8fce/.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_20b9209e1f6a79c01b34223d47ba6b05838c8fce/.venv/lib/python3.7/site-packages/setuptools/command/egg_info.py:628: SetuptoolsDeprecationWarning: Custom 'build_py' does not implement 'get_data_files_without_manifest'.\nPlease extend command classes from setuptools instead of distutils.\n  SetuptoolsDeprecationWarning\n",
    "new_commit_res_code": 1,
    "new_commit_res_stdout": "============================= test session starts ==============================\nplatform linux -- Python 3.7.9, pytest-7.4.4, pluggy-1.2.0\nrootdir: /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_20b9209e1f6a79c01b34223d47ba6b05838c8fce\nplugins: anyio-3.7.1\ncollected 27 items\n\nr2e_tests/test_1.py sss.....................F..                          [100%]\n\n=================================== FAILURES ===================================\n__________________________ TestOWHeatMap.test_summary __________________________\n\nself = <r2e_tests.test_1.TestOWHeatMap testMethod=test_summary>\n\n    def test_summary(self):\n        \"\"\"Check if status bar is updated when data is received\"\"\"\n        info = self.widget.info\n        no_input, no_output = \"No data on input\", \"No data on output\"\n    \n        data = self.housing\n        self.send_signal(self.widget.Inputs.data, data)\n        summary, details = f\"{len(data)}\", format_summary_details(data)\n        self.assertEqual(info._StateInfo__input_summary.brief, summary)\n        self.assertEqual(info._StateInfo__input_summary.details, details)\n>       self.assertEqual(info._StateInfo__output_summary.brief, \"\")\nE       AssertionError: '-' != ''\nE       - -\nE       +\n\nr2e_tests/test_1.py:297: AssertionError\n----------------------------- Captured stderr call -----------------------------\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\n==================================== PASSES ====================================\n_________________ TestOWHeatMap.test_cls_with_single_instance __________________\n----------------------------- Captured stderr call -----------------------------\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\n_____________ TestOWHeatMap.test_cluster_column_on_all_zero_column _____________\n----------------------------- Captured stderr call -----------------------------\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\n_____________________ TestOWHeatMap.test_data_column_nans ______________________\n----------------------------- Captured stderr call -----------------------------\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\n______________________ TestOWHeatMap.test_empty_clusters _______________________\n----------------------------- Captured stderr call -----------------------------\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\n_______________________ TestOWHeatMap.test_error_message _______________________\n----------------------------- Captured stderr call -----------------------------\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\n____________________ TestOWHeatMap.test_information_message ____________________\n----------------------------- Captured stderr call -----------------------------\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\n________________________ TestOWHeatMap.test_input_data _________________________\n----------------------------- Captured stderr call -----------------------------\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\n_____________ TestOWHeatMap.test_not_enough_data_settings_changed ______________\n----------------------------- Captured stderr call -----------------------------\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\n__________________________ TestOWHeatMap.test_outputs __________________________\n----------------------------- Captured stderr call -----------------------------\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\n_____________________ TestOWHeatMap.test_palette_centering _____________________\n----------------------------- Captured stderr call -----------------------------\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\n___________________ TestOWHeatMap.test_row_color_annotations ___________________\n----------------------------- Captured stderr call -----------------------------\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\n______________________ TestOWHeatMap.test_saved_selection ______________________\n----------------------------- Captured stderr call -----------------------------\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\n_______________________ TestOWHeatMap.test_set_split_var _______________________\n----------------------------- Captured stderr call -----------------------------\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\n___________________ TestOWHeatMap.test_set_split_var_missing ___________________\n----------------------------- Captured stderr call -----------------------------\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\n_____________________ TestOWHeatMap.test_settings_changed ______________________\n----------------------------- Captured stderr call -----------------------------\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\n_____________________ TestOWHeatMap.test_use_enough_colors _____________________\n----------------------------- Captured stderr call -----------------------------\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\n=========================== short test summary info ============================\nPASSED r2e_tests/test_1.py::TestOWHeatMap::test_cls_with_single_instance\nPASSED r2e_tests/test_1.py::TestOWHeatMap::test_cluster_column_on_all_zero_column\nPASSED r2e_tests/test_1.py::TestOWHeatMap::test_color_low_high\nPASSED r2e_tests/test_1.py::TestOWHeatMap::test_data_column_nans\nPASSED r2e_tests/test_1.py::TestOWHeatMap::test_empty_clusters\nPASSED r2e_tests/test_1.py::TestOWHeatMap::test_error_message\nPASSED r2e_tests/test_1.py::TestOWHeatMap::test_image_export\nPASSED r2e_tests/test_1.py::TestOWHeatMap::test_information_message\nPASSED r2e_tests/test_1.py::TestOWHeatMap::test_input_data\nPASSED r2e_tests/test_1.py::TestOWHeatMap::test_migrate_settings_v3\nPASSED r2e_tests/test_1.py::TestOWHeatMap::test_minimum_size\nPASSED r2e_tests/test_1.py::TestOWHeatMap::test_msg_base_class\nPASSED r2e_tests/test_1.py::TestOWHeatMap::test_not_enough_data_settings_changed\nPASSED r2e_tests/test_1.py::TestOWHeatMap::test_outputs\nPASSED r2e_tests/test_1.py::TestOWHeatMap::test_palette_center\nPASSED r2e_tests/test_1.py::TestOWHeatMap::test_palette_centering\nPASSED r2e_tests/test_1.py::TestOWHeatMap::test_row_color_annotations\nPASSED r2e_tests/test_1.py::TestOWHeatMap::test_saved_selection\nPASSED r2e_tests/test_1.py::TestOWHeatMap::test_set_split_var\nPASSED r2e_tests/test_1.py::TestOWHeatMap::test_set_split_var_missing\nPASSED r2e_tests/test_1.py::TestOWHeatMap::test_settings_changed\nPASSED r2e_tests/test_1.py::TestOWHeatMap::test_unconditional_commit_on_new_signal\nPASSED r2e_tests/test_1.py::TestOWHeatMap::test_use_enough_colors\nSKIPPED [1] .venv/lib/python3.7/site-packages/orangewidget/tests/base.py:244: image exporting not tested as .widget was not set\nSKIPPED [1] .venv/lib/python3.7/site-packages/orangewidget/tests/base.py:238: minimum size not tested as .widget was not set\nSKIPPED [1] .venv/lib/python3.7/site-packages/orangewidget/tests/base.py:250: msg base class not tested as .widget was not set\nFAILED r2e_tests/test_1.py::TestOWHeatMap::test_summary - AssertionError: '-'...\n=================== 1 failed, 23 passed, 3 skipped in 11.95s ===================\nWidgets left: 0    Max widgets: 4137 \n\n",
    "new_commit_res_stderr": "",
    "old_commit_res_code": 1,
    "old_commit_res_stdout": "============================= test session starts ==============================\nplatform linux -- Python 3.7.9, pytest-7.4.4, pluggy-1.2.0\nrootdir: /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_20b9209e1f6a79c01b34223d47ba6b05838c8fce\nplugins: anyio-3.7.1\ncollected 27 items\n\nr2e_tests/test_1.py sss...................F.F..                          [100%]\n\n=================================== FAILURES ===================================\n___________________ TestOWHeatMap.test_set_split_var_missing ___________________\n\nself = <r2e_tests.test_1.TestOWHeatMap testMethod=test_set_split_var_missing>\n\n    def test_set_split_var_missing(self):\n        data = self.brown_selected[::3].copy()\n        data.Y[::5] = np.nan\n        w = self.widget\n        self.send_signal(self.widget.Inputs.data, data, widget=w)\n        self.assertIs(w.split_by_var, data.domain.class_var)\n        self.assertEqual(len(w.parts.rows),\n>                        len(data.domain.class_var.values) + 1)\nE       AssertionError: 3 != 4\n\nr2e_tests/test_1.py:232: AssertionError\n----------------------------- Captured stderr call -----------------------------\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\n__________________________ TestOWHeatMap.test_summary __________________________\n\nself = <r2e_tests.test_1.TestOWHeatMap testMethod=test_summary>\n\n    def test_summary(self):\n        \"\"\"Check if status bar is updated when data is received\"\"\"\n        info = self.widget.info\n        no_input, no_output = \"No data on input\", \"No data on output\"\n    \n        data = self.housing\n        self.send_signal(self.widget.Inputs.data, data)\n        summary, details = f\"{len(data)}\", format_summary_details(data)\n        self.assertEqual(info._StateInfo__input_summary.brief, summary)\n        self.assertEqual(info._StateInfo__input_summary.details, details)\n>       self.assertEqual(info._StateInfo__output_summary.brief, \"\")\nE       AssertionError: '-' != ''\nE       - -\nE       +\n\nr2e_tests/test_1.py:297: AssertionError\n----------------------------- Captured stderr call -----------------------------\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\n==================================== PASSES ====================================\n_________________ TestOWHeatMap.test_cls_with_single_instance __________________\n----------------------------- Captured stderr call -----------------------------\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\n_____________ TestOWHeatMap.test_cluster_column_on_all_zero_column _____________\n----------------------------- Captured stderr call -----------------------------\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\n_____________________ TestOWHeatMap.test_data_column_nans ______________________\n----------------------------- Captured stderr call -----------------------------\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\n______________________ TestOWHeatMap.test_empty_clusters _______________________\n----------------------------- Captured stderr call -----------------------------\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\n_______________________ TestOWHeatMap.test_error_message _______________________\n----------------------------- Captured stderr call -----------------------------\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\n____________________ TestOWHeatMap.test_information_message ____________________\n----------------------------- Captured stderr call -----------------------------\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\n________________________ TestOWHeatMap.test_input_data _________________________\n----------------------------- Captured stderr call -----------------------------\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\n_____________ TestOWHeatMap.test_not_enough_data_settings_changed ______________\n----------------------------- Captured stderr call -----------------------------\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\n__________________________ TestOWHeatMap.test_outputs __________________________\n----------------------------- Captured stderr call -----------------------------\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\n_____________________ TestOWHeatMap.test_palette_centering _____________________\n----------------------------- Captured stderr call -----------------------------\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\n___________________ TestOWHeatMap.test_row_color_annotations ___________________\n----------------------------- Captured stderr call -----------------------------\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\n______________________ TestOWHeatMap.test_saved_selection ______________________\n----------------------------- Captured stderr call -----------------------------\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\n_______________________ TestOWHeatMap.test_set_split_var _______________________\n----------------------------- Captured stderr call -----------------------------\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\n_____________________ TestOWHeatMap.test_settings_changed ______________________\n----------------------------- Captured stderr call -----------------------------\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\n_____________________ TestOWHeatMap.test_use_enough_colors _____________________\n----------------------------- Captured stderr call -----------------------------\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\nQFont::setPointSizeF: Point size <= 0 (-0.720000), must be greater than 0\n=========================== short test summary info ============================\nPASSED r2e_tests/test_1.py::TestOWHeatMap::test_cls_with_single_instance\nPASSED r2e_tests/test_1.py::TestOWHeatMap::test_cluster_column_on_all_zero_column\nPASSED r2e_tests/test_1.py::TestOWHeatMap::test_color_low_high\nPASSED r2e_tests/test_1.py::TestOWHeatMap::test_data_column_nans\nPASSED r2e_tests/test_1.py::TestOWHeatMap::test_empty_clusters\nPASSED r2e_tests/test_1.py::TestOWHeatMap::test_error_message\nPASSED r2e_tests/test_1.py::TestOWHeatMap::test_image_export\nPASSED r2e_tests/test_1.py::TestOWHeatMap::test_information_message\nPASSED r2e_tests/test_1.py::TestOWHeatMap::test_input_data\nPASSED r2e_tests/test_1.py::TestOWHeatMap::test_migrate_settings_v3\nPASSED r2e_tests/test_1.py::TestOWHeatMap::test_minimum_size\nPASSED r2e_tests/test_1.py::TestOWHeatMap::test_msg_base_class\nPASSED r2e_tests/test_1.py::TestOWHeatMap::test_not_enough_data_settings_changed\nPASSED r2e_tests/test_1.py::TestOWHeatMap::test_outputs\nPASSED r2e_tests/test_1.py::TestOWHeatMap::test_palette_center\nPASSED r2e_tests/test_1.py::TestOWHeatMap::test_palette_centering\nPASSED r2e_tests/test_1.py::TestOWHeatMap::test_row_color_annotations\nPASSED r2e_tests/test_1.py::TestOWHeatMap::test_saved_selection\nPASSED r2e_tests/test_1.py::TestOWHeatMap::test_set_split_var\nPASSED r2e_tests/test_1.py::TestOWHeatMap::test_settings_changed\nPASSED r2e_tests/test_1.py::TestOWHeatMap::test_unconditional_commit_on_new_signal\nPASSED r2e_tests/test_1.py::TestOWHeatMap::test_use_enough_colors\nSKIPPED [1] .venv/lib/python3.7/site-packages/orangewidget/tests/base.py:244: image exporting not tested as .widget was not set\nSKIPPED [1] .venv/lib/python3.7/site-packages/orangewidget/tests/base.py:238: minimum size not tested as .widget was not set\nSKIPPED [1] .venv/lib/python3.7/site-packages/orangewidget/tests/base.py:250: msg base class not tested as .widget was not set\nFAILED r2e_tests/test_1.py::TestOWHeatMap::test_set_split_var_missing - Asser...\nFAILED r2e_tests/test_1.py::TestOWHeatMap::test_summary - AssertionError: '-'...\n=================== 2 failed, 22 passed, 3 skipped in 6.30s ====================\nWidgets left: 0    Max widgets: 4135 \n\n",
    "old_commit_res_stderr": ""
}
Orange/widgets/visualize/owheatmap.py Orange/widgets/visualize/tests/test_owheatmap.py
We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
[ISSUE]
**Title:** Missing Split Variable Values Not Displayed as Separate "N/A" Group in HeatMap Widget

**Description:**
When using the HeatMap widget with a split variable that contains missing values (`NaN`), the widget fails to display these instances as a separate "N/A" group. Instead, instances with undefined split variable values are excluded from the visualization, leading to incomplete data representation.

**Example:**
Suppose you have a dataset where the split variable has some missing values:

```python
data = self.brown_selected[::3].copy()
data.Y[::5] = np.nan  # Introduce missing values
self.send_signal(self.widget.Inputs.data, data, widget=w)
```

In this scenario, the HeatMap should create an additional group labeled "N/A" to represent the instances with missing split variable values. However, currently, the widget only displays groups for the defined split variable values and omits the "N/A" group.

**Expected Behavior:**
The HeatMap widget should include an "N/A" group that aggregates all instances with undefined split variable values, ensuring that all data is represented accurately in the visualization.

**Actual Behavior:**
Instances with missing split variable values are not displayed in the HeatMap, and no "N/A" group is present. This results in an incomplete visualization where some data points are excluded without indication.

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

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

--- BEGIN FILE ---
```
### Orange/widgets/visualize/owheatmap.py
import enum
from itertools import islice
from typing import (
    Iterable, Mapping, Any, TypeVar, Type, NamedTuple, Sequence, Optional,
    Union, Tuple, List, Callable
)

import numpy as np
import scipy.sparse as sp

from AnyQt.QtWidgets import (
    QGraphicsScene, QGraphicsView, QFormLayout, QComboBox, QGroupBox,
    QMenu, QAction, QSizePolicy
)
from AnyQt.QtGui import QStandardItemModel, QStandardItem, QFont, QKeySequence
from AnyQt.QtCore import Qt, QSize, QRectF, QObject

from orangewidget.utils.combobox import ComboBox, ComboBoxSearch
from Orange.data import Domain, Table, Variable, DiscreteVariable
from Orange.data.sql.table import SqlTable
import Orange.distance

from Orange.clustering import hierarchical, kmeans
from Orange.widgets.utils import colorpalettes, apply_all
from Orange.widgets.utils.itemmodels import DomainModel
from Orange.widgets.utils.stickygraphicsview import StickyGraphicsView
from Orange.widgets.utils.graphicsview import GraphicsWidgetView
from Orange.widgets.utils.colorpalettes import DiscretePalette, Palette

from Orange.widgets.utils.annotated_data import (create_annotated_table,
                                                 ANNOTATED_DATA_SIGNAL_NAME)
from Orange.widgets import widget, gui, settings
from Orange.widgets.widget import Msg, Input, Output

from Orange.widgets.data.oweditdomain import table_column_data
from Orange.widgets.visualize.utils.heatmap import HeatmapGridWidget, \
    ColorMap, CategoricalColorMap, GradientColorMap
from Orange.widgets.utils.widgetpreview import WidgetPreview
from Orange.widgets.utils.state_summary import format_summary_details


__all__ = []


def kmeans_compress(X, k=50):
    km = kmeans.KMeans(n_clusters=k, n_init=5, random_state=42)
    return km.get_model(X)


def cbselect(cb: QComboBox, value, role: Qt.ItemDataRole = Qt.EditRole) -> None:
    """
    Find and select the `value` in the `cb` QComboBox.

    Parameters
    ----------
    cb: QComboBox
    value: Any
    role: Qt.ItemDataRole
        The data role in the combo box model to match value against
    """
    cb.setCurrentIndex(cb.findData(value, role))


class Clustering(enum.IntEnum):
    #: No clustering
    None_ = 0
    #: Hierarchical clustering
    Clustering = 1
    #: Hierarchical clustering with optimal leaf ordering
    OrderedClustering = 2


ClusteringRole = Qt.UserRole + 13
#: Item data for clustering method selection models
ClusteringModelData = [
    {
        Qt.DisplayRole: "None",
        Qt.ToolTipRole: "No clustering",
        ClusteringRole: Clustering.None_,
    }, {
        Qt.DisplayRole: "Clustering",
        Qt.ToolTipRole: "Apply hierarchical clustering",
        ClusteringRole: Clustering.Clustering,
    }, {
        Qt.DisplayRole: "Clustering (opt. ordering)",
        Qt.ToolTipRole: "Apply hierarchical clustering with optimal leaf "
                        "ordering.",
        ClusteringRole: Clustering.OrderedClustering,
    }
]

ColumnLabelsPosData = [
    {Qt.DisplayRole: name, Qt.UserRole: value}
    for name, value in [
        ("None", HeatmapGridWidget.NoPosition),
        ("Top", HeatmapGridWidget.PositionTop),
        ("Bottom", HeatmapGridWidget.PositionBottom),
        ("Top and Bottom", (HeatmapGridWidget.PositionTop |
                            HeatmapGridWidget.PositionBottom)),
    ]
]


def create_list_model(
        items: Iterable[Mapping[Qt.ItemDataRole, Any]],
        parent: Optional[QObject] = None,
) -> QStandardItemModel:
    """Create list model from an item date iterable."""
    model = QStandardItemModel(parent)
    for item in items:
        sitem = QStandardItem()
        for role, value in item.items():
            sitem.setData(value, role)
        model.appendRow([sitem])
    return model


E = TypeVar("E", bound=enum.Enum)  # pylint: disable=invalid-name


def enum_get(etype: Type[E], name: str, default: E) -> E:
    """
    Return an Enum member by `name`. If no such member exists in `etype`
    return `default`.
    """
    try:
        return etype[name]
    except LookupError:
        return default


class OWHeatMap(widget.OWWidget):
    name = "Heat Map"
    description = "Plot a data matrix heatmap."
    icon = "icons/Heatmap.svg"
    priority = 260
    keywords = []

    class Inputs:
        data = Input("Data", Table)

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

    settings_version = 3

    settingsHandler = settings.DomainContextHandler()

    # Disable clustering for inputs bigger than this
    MaxClustering = 25000
    # Disable cluster leaf ordering for inputs bigger than this
    MaxOrderedClustering = 1000

    threshold_low = settings.Setting(0.0)
    threshold_high = settings.Setting(1.0)

    merge_kmeans = settings.Setting(False)
    merge_kmeans_k = settings.Setting(50)

    # Display column with averages
    averages: bool = settings.Setting(True)
    # Display legend
    legend: bool = settings.Setting(True)
    # Annotations
    #: text row annotation (row names)
    annotation_var = settings.ContextSetting(None)
    #: color row annotation
    annotation_color_var = settings.ContextSetting(None)
    # Discrete variable used to split that data/heatmaps (vertically)
    split_by_var = settings.ContextSetting(None)
    # Selected row/column clustering method (name)
    col_clustering_method: str = settings.Setting(Clustering.None_.name)
    row_clustering_method: str = settings.Setting(Clustering.None_.name)

    palette_name = settings.Setting(colorpalettes.DefaultContinuousPaletteName)
    column_label_pos: int = settings.Setting(1)
    selected_rows: List[int] = settings.Setting(None, schema_only=True)

    auto_commit = settings.Setting(True)

    graph_name = "scene"

    left_side_scrolling = True

    class Information(widget.OWWidget.Information):
        sampled = Msg("Data has been sampled")
        discrete_ignored = Msg("{} categorical feature{} ignored")
        row_clust = Msg("{}")
        col_clust = Msg("{}")
        sparse_densified = Msg("Showing this data may require a lot of memory")

    class Error(widget.OWWidget.Error):
        no_continuous = Msg("No numeric features")
        not_enough_features = Msg("Not enough features for column clustering")
        not_enough_instances = Msg("Not enough instances for clustering")
        not_enough_instances_k_means = Msg(
            "Not enough instances for k-means merging")
        not_enough_memory = Msg("Not enough memory to show this data")

    class Warning(widget.OWWidget.Warning):
        empty_clusters = Msg("Empty clusters were removed")
        missing_split_values = Msg(
            "Instances with undefined split variable values are not shown")

    def __init__(self):
        super().__init__()
        self.__pending_selection = self.selected_rows

        # A kingdom for a save_state/restore_state
        self.col_clustering = enum_get(
            Clustering, self.col_clustering_method, Clustering.None_)
        self.row_clustering = enum_get(
            Clustering, self.row_clustering_method, Clustering.None_)

        @self.settingsAboutToBePacked.connect
        def _():
            self.col_clustering_method = self.col_clustering.name
            self.row_clustering_method = self.row_clustering.name

        self.keep_aspect = False

        #: The original data with all features (retained to
        #: preserve the domain on the output)
        self.input_data = None
        #: The effective data striped of discrete features, and often
        #: merged using k-means
        self.data = None
        self.effective_data = None
        #: kmeans model used to merge rows of input_data
        self.kmeans_model = None
        #: merge indices derived from kmeans
        #: a list (len==k) of int ndarray where the i-th item contains
        #: the indices which merge the input_data into the heatmap row i
        self.merge_indices = None
        self.parts: Optional[Parts] = None
        self.__rows_cache = {}
        self.__columns_cache = {}

        self.info.set_input_summary(self.info.NoInput)
        self.info.set_output_summary(self.info.NoOutput)

        # GUI definition
        colorbox = gui.vBox(self.controlArea, "Color")
        self.color_cb = gui.palette_combo_box(self.palette_name)
        self.color_cb.currentIndexChanged.connect(self.update_color_schema)
        colorbox.layout().addWidget(self.color_cb)

        form = QFormLayout(
            formAlignment=Qt.AlignLeft,
            labelAlignment=Qt.AlignLeft,
            fieldGrowthPolicy=QFormLayout.AllNonFixedFieldsGrow
        )
        lowslider = gui.hSlider(
            colorbox, self, "threshold_low", minValue=0.0, maxValue=1.0,
            step=0.05, ticks=True, intOnly=False,
            createLabel=False, callback=self.update_lowslider)
        highslider = gui.hSlider(
            colorbox, self, "threshold_high", minValue=0.0, maxValue=1.0,
            step=0.05, ticks=True, intOnly=False,
            createLabel=False, callback=self.update_highslider)

        form.addRow("Low:", lowslider)
        form.addRow("High:", highslider)

        colorbox.layout().addLayout(form)

        mergebox = gui.vBox(self.controlArea, "Merge",)
        gui.checkBox(mergebox, self, "merge_kmeans", "Merge by k-means",
                     callback=self.__update_row_clustering)
        ibox = gui.indentedBox(mergebox)
        gui.spin(ibox, self, "merge_kmeans_k", minv=5, maxv=500,
                 label="Clusters:", keyboardTracking=False,
                 callbackOnReturn=True, callback=self.update_merge)

        cluster_box = gui.vBox(self.controlArea, "Clustering")
        # Row clustering
        self.row_cluster_cb = cb = ComboBox()
        cb.setModel(create_list_model(ClusteringModelData, self))
        cbselect(cb, self.row_clustering, ClusteringRole)
        self.connect_control(
            "row_clustering",
            lambda value, cb=cb: cbselect(cb, value, ClusteringRole)
        )
        @cb.activated.connect
        def _(idx, cb=cb):
            self.set_row_clustering(cb.itemData(idx, ClusteringRole))

        # Column clustering
        self.col_cluster_cb = cb = ComboBox()
        cb.setModel(create_list_model(ClusteringModelData, self))
        cbselect(cb, self.col_clustering, ClusteringRole)
        self.connect_control(
            "col_clustering",
            lambda value, cb=cb: cbselect(cb, value, ClusteringRole)
        )
        @cb.activated.connect
        def _(idx, cb=cb):
            self.set_col_clustering(cb.itemData(idx, ClusteringRole))

        form = QFormLayout(
            labelAlignment=Qt.AlignLeft, formAlignment=Qt.AlignLeft,
            fieldGrowthPolicy=QFormLayout.AllNonFixedFieldsGrow,
        )
        form.addRow("Rows:", self.row_cluster_cb)
        form.addRow("Columns:", self.col_cluster_cb)
        cluster_box.layout().addLayout(form)
        box = gui.vBox(self.controlArea, "Split By")

        self.row_split_model = DomainModel(
            placeholder="(None)",
            valid_types=(Orange.data.DiscreteVariable,),
            parent=self,
        )
        self.row_split_cb = cb = ComboBoxSearch(
            enabled=not self.merge_kmeans,
            sizeAdjustPolicy=ComboBox.AdjustToMinimumContentsLengthWithIcon,
            minimumContentsLength=14,
            toolTip="Split the heatmap vertically by a categorical column"
        )
        self.row_split_cb.setModel(self.row_split_model)
        self.connect_control(
            "split_by_var", lambda value, cb=cb: cbselect(cb, value)
        )
        self.connect_control(
            "merge_kmeans", self.row_split_cb.setDisabled
        )
        self.split_by_var = None

        self.row_split_cb.activated.connect(
            self.__on_split_rows_activated
        )
        box.layout().addWidget(self.row_split_cb)

        box = gui.vBox(self.controlArea, 'Annotation && Legends')

        gui.checkBox(box, self, 'legend', 'Show legend',
                     callback=self.update_legend)

        gui.checkBox(box, self, 'averages', 'Stripes with averages',
                     callback=self.update_averages_stripe)
        annotbox = QGroupBox("Row Annotations", flat=True)
        form = QFormLayout(
            annotbox,
            formAlignment=Qt.AlignLeft,
            labelAlignment=Qt.AlignLeft,
            fieldGrowthPolicy=QFormLayout.AllNonFixedFieldsGrow
        )
        self.annotation_model = DomainModel(placeholder="(None)")
        self.annotation_text_cb = ComboBoxSearch(
            minimumContentsLength=12,
            sizeAdjustPolicy=QComboBox.AdjustToMinimumContentsLength
        )
        self.annotation_text_cb.setModel(self.annotation_model)
        self.annotation_text_cb.activated.connect(self.set_annotation_var)
        self.connect_control("annotation_var", self.annotation_var_changed)

        self.row_side_color_model = DomainModel(
            order=(DomainModel.CLASSES, DomainModel.Separator,
                   DomainModel.METAS),
            placeholder="(None)", valid_types=DomainModel.PRIMITIVE,
            flags=Qt.ItemIsSelectable | Qt.ItemIsEnabled,
            parent=self,
        )
        self.row_side_color_cb = ComboBoxSearch(
            sizeAdjustPolicy=QComboBox.AdjustToMinimumContentsLength,
            minimumContentsLength=12
        )
        self.row_side_color_cb.setModel(self.row_side_color_model)
        self.row_side_color_cb.activated.connect(self.set_annotation_color_var)
        self.connect_control("annotation_color_var", self.annotation_color_var_changed)
        form.addRow("Text", self.annotation_text_cb)
        form.addRow("Color", self.row_side_color_cb)
        box.layout().addWidget(annotbox)
        posbox = gui.vBox(box, "Column Labels Position", addSpace=False)
        posbox.setFlat(True)
        cb = gui.comboBox(
            posbox, self, "column_label_pos",
            callback=self.update_column_annotations)
        cb.setModel(create_list_model(ColumnLabelsPosData, parent=self))
        cb.setCurrentIndex(self.column_label_pos)
        gui.checkBox(self.controlArea, self, "keep_aspect",
                     "Keep aspect ratio", box="Resize",
                     callback=self.__aspect_mode_changed)

        gui.rubber(self.controlArea)
        gui.auto_send(self.controlArea, self, "auto_commit")

        # Scene with heatmap
        class HeatmapScene(QGraphicsScene):
            widget: Optional[HeatmapGridWidget] = None

        self.scene = self.scene = HeatmapScene(parent=self)
        self.view = GraphicsView(
            self.scene,
            verticalScrollBarPolicy=Qt.ScrollBarAlwaysOn,
            horizontalScrollBarPolicy=Qt.ScrollBarAlwaysOn,
            viewportUpdateMode=QGraphicsView.FullViewportUpdate,
            widgetResizable=True,
        )
        self.view.setContextMenuPolicy(Qt.CustomContextMenu)
        self.view.customContextMenuRequested.connect(
            self._on_view_context_menu
        )
        self.mainArea.layout().addWidget(self.view)
        self.selected_rows = []
        self.__font_inc = QAction(
            "Increase Font", self, shortcut=QKeySequence("ctrl+>"))
        self.__font_dec = QAction(
            "Decrease Font", self, shortcut=QKeySequence("ctrl+<"))
        self.__font_inc.triggered.connect(lambda: self.__adjust_font_size(1))
        self.__font_dec.triggered.connect(lambda: self.__adjust_font_size(-1))
        if hasattr(QAction, "setShortcutVisibleInContextMenu"):
            apply_all(
                [self.__font_inc, self.__font_dec],
                lambda a: a.setShortcutVisibleInContextMenu(True)
            )
        self.addActions([self.__font_inc, self.__font_dec])

    @property
    def center_palette(self):
        palette = self.color_cb.currentData()
        return bool(palette.flags & palette.Diverging)

    @property
    def _column_label_pos(self) -> HeatmapGridWidget.Position:
        return ColumnLabelsPosData[self.column_label_pos][Qt.UserRole]

    def annotation_color_var_changed(self, value):
        cbselect(self.row_side_color_cb, value, Qt.EditRole)

    def annotation_var_changed(self, value):
        cbselect(self.annotation_text_cb, value, Qt.EditRole)

    def set_row_clustering(self, method: Clustering) -> None:
        assert isinstance(method, Clustering)
        if self.row_clustering != method:
            self.row_clustering = method
            cbselect(self.row_cluster_cb, method, ClusteringRole)
            self.__update_row_clustering()

    def set_col_clustering(self, method: Clustering) -> None:
        assert isinstance(method, Clustering)
        if self.col_clustering != method:
            self.col_clustering = method
            cbselect(self.col_cluster_cb, method, ClusteringRole)
            self.__update_column_clustering()

    def sizeHint(self) -> QSize:
        return super().sizeHint().expandedTo(QSize(900, 700))

    def color_palette(self):
        return self.color_cb.currentData().lookup_table()

    def color_map(self) -> GradientColorMap:
        return GradientColorMap(
            self.color_palette(), (self.threshold_low, self.threshold_high),
            0 if self.center_palette else None
        )

    def clear(self):
        self.data = None
        self.input_data = None
        self.effective_data = None
        self.kmeans_model = None
        self.merge_indices = None
        self.annotation_model.set_domain(None)
        self.annotation_var = None
        self.row_side_color_model.set_domain(None)
        self.annotation_color_var = None
        self.row_split_model.set_domain(None)
        self.split_by_var = None
        self.parts = None
        self.clear_scene()
        self.selected_rows = []
        self.__columns_cache.clear()
        self.__rows_cache.clear()
        self.__update_clustering_enable_state(None)

    def clear_scene(self):
        if self.scene.widget is not None:
            self.scene.widget.layoutDidActivate.disconnect(
                self.__on_layout_activate
            )
            self.scene.widget.selectionFinished.disconnect(
                self.on_selection_finished
            )
        self.scene.widget = None
        self.scene.clear()

        self.view.setSceneRect(QRectF())
        self.view.setHeaderSceneRect(QRectF())
        self.view.setFooterSceneRect(QRectF())

    @Inputs.data
    def set_dataset(self, data=None):
        """Set the input dataset to display."""
        self.closeContext()
        self.clear()
        self.clear_messages()
        self._set_input_summary(data)

        if isinstance(data, SqlTable):
            if data.approx_len() < 4000:
                data = Table(data)
            else:
                self.Information.sampled()
                data_sample = data.sample_time(1, no_cache=True)
                data_sample.download_data(2000, partial=True)
                data = Table(data_sample)

        if data is not None and not len(data):
            data = None

        if data is not None and sp.issparse(data.X):
            try:
                data = data.to_dense()
            except MemoryError:
                data = None
                self.Error.not_enough_memory()
            else:
                self.Information.sparse_densified()

        input_data = data

        # Data contains no attributes or meta attributes only
        if data is not None and len(data.domain.attributes) == 0:
            self.Error.no_continuous()
            input_data = data = None

        # Data contains some discrete attributes which must be filtered
        if data is not None and \
                any(var.is_discrete for var in data.domain.attributes):
            ndisc = sum(var.is_discrete for var in data.domain.attributes)
            data = data.transform(
                Domain([var for var in data.domain.attributes
                        if var.is_continuous],
                       data.domain.class_vars,
                       data.domain.metas))
            if not data.domain.attributes:
                self.Error.no_continuous()
                input_data = data = None
            else:
                self.Information.discrete_ignored(
                    ndisc, "s" if ndisc > 1 else "")

        self.data = data
        self.input_data = input_data

        if data is not None:
            self.annotation_model.set_domain(self.input_data.domain)
            self.row_side_color_model.set_domain(self.input_data.domain)
            self.annotation_var = None
            self.annotation_color_var = None
            self.row_split_model.set_domain(data.domain)
            if data.domain.has_discrete_class:
                self.split_by_var = data.domain.class_var
            else:
                self.split_by_var = None
            self.openContext(self.input_data)
            if self.split_by_var not in self.row_split_model:
                self.split_by_var = None

        self.update_heatmaps()
        if data is not None and self.__pending_selection is not None:
            assert self.scene.widget is not None
            self.scene.widget.selectRows(self.__pending_selection)
            self.selected_rows = self.__pending_selection
            self.__pending_selection = None

        self.unconditional_commit()

    def _set_input_summary(self, data):
        summary = len(data) if data else self.info.NoInput
        details = format_summary_details(data) if data else ""
        self.info.set_input_summary(summary, details)

    def __on_split_rows_activated(self):
        self.set_split_variable(self.row_split_cb.currentData(Qt.EditRole))

    def set_split_variable(self, var):
        if var != self.split_by_var:
            self.split_by_var = var
            self.update_heatmaps()

    def update_heatmaps(self):
        if self.data is not None:
            self.clear_scene()
            self.clear_messages()
            if self.col_clustering != Clustering.None_ and \
                    len(self.data.domain.attributes) < 2:
                self.Error.not_enough_features()
            elif (self.col_clustering != Clustering.None_ or
                  self.row_clustering != Clustering.None_) and \
                    len(self.data) < 2:
                self.Error.not_enough_instances()
            elif self.merge_kmeans and len(self.data) < 3:
                self.Error.not_enough_instances_k_means()
            else:
                parts = self.construct_heatmaps(self.data, self.split_by_var)
                self.construct_heatmaps_scene(parts, self.effective_data)
                self.selected_rows = []
        else:
            self.clear()
        if self.parts is not None and self.effective_data is not None:
            nrows = sum(len(p.indices) for p in self.parts.rows)
            self.Warning.missing_split_values(
                shown=nrows < len(self.effective_data))
        else:
            self.Warning.missing_split_values.clear()

    def update_merge(self):
        self.kmeans_model = None
        self.merge_indices = None
        if self.data is not None and self.merge_kmeans:
            self.update_heatmaps()
            self.commit()

    def _make_parts(self, data, group_var=None):
        """
        Make initial `Parts` for data, split by group_var, group_key
        """
        if group_var is not None:
            assert group_var.is_discrete
            _col_data = table_column_data(data, group_var)
            row_indices = [np.flatnonzero(_col_data == i)
                           for i in range(len(group_var.values))]
            row_groups = [RowPart(title=name, indices=ind,
                                  cluster=None, cluster_ordered=None)
                          for name, ind in zip(group_var.values, row_indices)]
        else:
            row_groups = [RowPart(title=None, indices=range(0, len(data)),
                                  cluster=None, cluster_ordered=None)]

        col_groups = [
            ColumnPart(
                title=None, indices=range(0, len(data.domain.attributes)),
                domain=data.domain, cluster=None, cluster_ordered=None)
        ]

        minv, maxv = np.nanmin(data.X), np.nanmax(data.X)
        return Parts(row_groups, col_groups, span=(minv, maxv))

    def cluster_rows(self, data: Table, parts: 'Parts', ordered=False) -> 'Parts':
        row_groups = []
        for row in parts.rows:
            if row.cluster is not None:
                cluster = row.cluster
            else:
                cluster = None
            if row.cluster_ordered is not None:
                cluster_ord = row.cluster_ordered
            else:
                cluster_ord = None

            if row.can_cluster:
                matrix = None
                need_dist = cluster is None or (ordered and cluster_ord is None)
                if need_dist:
                    subset = data[row.indices]
                    matrix = Orange.distance.Euclidean(subset)

                if cluster is None:
                    assert len(matrix) < self.MaxClustering
                    cluster = hierarchical.dist_matrix_clustering(
                        matrix, linkage=hierarchical.WARD
                    )
                if ordered and cluster_ord is None:
                    assert len(matrix) < self.MaxOrderedClustering
                    cluster_ord = hierarchical.optimal_leaf_ordering(
                        cluster, matrix,
                    )
            row_groups.append(row._replace(cluster=cluster, cluster_ordered=cluster_ord))

        return parts._replace(rows=row_groups)

    def cluster_columns(self, data, parts, ordered=False):
        assert len(parts.columns) == 1, "columns split is no longer supported"
        assert all(var.is_continuous for var in data.domain.attributes)

        col0 = parts.columns[0]
        if col0.cluster is not None:
            cluster = col0.cluster
        else:
            cluster = None
        if col0.cluster_ordered is not None:
            cluster_ord = col0.cluster_ordered
        else:
            cluster_ord = None
        need_dist = cluster is None or (ordered and cluster_ord is None)
        matrix = None
        if need_dist:
            data = Orange.distance._preprocess(data)
            matrix = np.asarray(Orange.distance.PearsonR(data, axis=0))
            # nan values break clustering below
            matrix = np.nan_to_num(matrix)

        if cluster is None:
            assert matrix is not None
            assert len(matrix) < self.MaxClustering
            cluster = hierarchical.dist_matrix_clustering(
                matrix, linkage=hierarchical.WARD
            )
        if ordered and cluster_ord is None:
            assert len(matrix) < self.MaxOrderedClustering
            cluster_ord = hierarchical.optimal_leaf_ordering(cluster, matrix)

        col_groups = [col._replace(cluster=cluster, cluster_ordered=cluster_ord)
                      for col in parts.columns]
        return parts._replace(columns=col_groups)

    def construct_heatmaps(self, data, group_var=None) -> 'Parts':
        if self.merge_kmeans:
            if self.kmeans_model is None:
                effective_data = self.input_data.transform(
                    Orange.data.Domain(
                        [var for var in self.input_data.domain.attributes
                         if var.is_continuous],
                        self.input_data.domain.class_vars,
                        self.input_data.domain.metas))
                nclust = min(self.merge_kmeans_k, len(effective_data) - 1)
                self.kmeans_model = kmeans_compress(effective_data, k=nclust)
                effective_data.domain = self.kmeans_model.domain
                merge_indices = [np.flatnonzero(self.kmeans_model.labels == ind)
                                 for ind in range(nclust)]
                not_empty_indices = [i for i, x in enumerate(merge_indices)
                                     if len(x) > 0]
                self.merge_indices = \
                    [merge_indices[i] for i in not_empty_indices]
                if len(merge_indices) != len(self.merge_indices):
                    self.Warning.empty_clusters()
                effective_data = Orange.data.Table(
                    Orange.data.Domain(effective_data.domain.attributes),
                    self.kmeans_model.centroids[not_empty_indices]
                )
            else:
                effective_data = self.effective_data

            group_var = None
        else:
            self.kmeans_model = None
            self.merge_indices = None
            effective_data = data

        self.effective_data = effective_data

        self.__update_clustering_enable_state(effective_data)

        parts = self._make_parts(effective_data, group_var)
        # Restore/update the row/columns items descriptions from cache if
        # available
        rows_cache_key = (group_var,
                          self.merge_kmeans_k if self.merge_kmeans else None)
        if rows_cache_key in self.__rows_cache:
            parts = parts._replace(rows=self.__rows_cache[rows_cache_key].rows)

        if self.row_clustering != Clustering.None_:
            parts = self.cluster_rows(
                effective_data, parts,
                ordered=self.row_clustering == Clustering.OrderedClustering
            )
        if self.col_clustering != Clustering.None_:
            parts = self.cluster_columns(
                effective_data, parts,
                ordered=self.col_clustering == Clustering.OrderedClustering
            )

        # Cache the updated parts
        self.__rows_cache[rows_cache_key] = parts
        return parts

    def construct_heatmaps_scene(self, parts: 'Parts', data: Table) -> None:
        _T = TypeVar("_T", bound=Union[RowPart, ColumnPart])

        def select_cluster(clustering: Clustering, item: _T) -> _T:
            if clustering == Clustering.None_:
                return item._replace(cluster=None, cluster_ordered=None)
            elif clustering == Clustering.Clustering:
                return item._replace(cluster=item.cluster, cluster_ordered=None)
            elif clustering == Clustering.OrderedClustering:
                return item._replace(cluster=item.cluster_ordered, cluster_ordered=None)
            else:  # pragma: no cover
                raise TypeError()

        rows = [select_cluster(self.row_clustering, rowitem)
                for rowitem in parts.rows]
        cols = [select_cluster(self.col_clustering, colitem)
                for colitem in parts.columns]
        parts = Parts(columns=cols, rows=rows, span=parts.span)

        self.setup_scene(parts, data)

    def setup_scene(self, parts, data):
        # type: (Parts, Table) -> None
        widget = HeatmapGridWidget()
        widget.setColorMap(self.color_map())
        self.scene.addItem(widget)
        self.scene.widget = widget
        columns = [v.name for v in data.domain.attributes]
        parts = HeatmapGridWidget.Parts(
            rows=[
                HeatmapGridWidget.RowItem(r.title, r.indices, r.cluster)
                for r in parts.rows
            ],
            columns=[
                HeatmapGridWidget.ColumnItem(c.title, c.indices, c.cluster)
                for c in parts.columns
            ],
            data=data.X,
            span=parts.span,
            row_names=None,
            col_names=columns,
        )
        widget.setHeatmaps(parts)
        side = self.row_side_colors()
        if side is not None:
            widget.setRowSideColorAnnotations(side[0], side[1], name=side[2].name)
        widget.setColumnLabelsPosition(self._column_label_pos)
        widget.setAspectRatioMode(
            Qt.KeepAspectRatio if self.keep_aspect else Qt.IgnoreAspectRatio
        )
        widget.setShowAverages(self.averages)
        widget.setLegendVisible(self.legend)

        widget.layoutDidActivate.connect(self.__on_layout_activate)
        widget.selectionFinished.connect(self.on_selection_finished)

        self.update_annotations()
        self.view.setCentralWidget(widget)
        self.parts = parts

    def __update_scene_rects(self):
        widget = self.scene.widget
        if widget is None:
            return
        rect = widget.geometry()
        self.scene.setSceneRect(rect)
        self.view.setSceneRect(rect)
        self.view.setHeaderSceneRect(widget.headerGeometry())
        self.view.setFooterSceneRect(widget.footerGeometry())

    def __on_layout_activate(self):
        self.__update_scene_rects()

    def __aspect_mode_changed(self):
        widget = self.scene.widget
        if widget is None:
            return
        widget.setAspectRatioMode(
            Qt.KeepAspectRatio if self.keep_aspect else Qt.IgnoreAspectRatio
        )
        # when aspect fixed the vertical sh is fixex, when not, it can
        # shrink vertically
        sp = widget.sizePolicy()
        if self.keep_aspect:
            sp.setVerticalPolicy(QSizePolicy.Fixed)
        else:
            sp.setVerticalPolicy(QSizePolicy.Preferred)
        widget.setSizePolicy(sp)

    def __update_clustering_enable_state(self, data):
        if data is not None:
            N = len(data)
            M = len(data.domain.attributes)
        else:
            N = M = 0

        rc_enabled = N <= self.MaxClustering
        rco_enabled = N <= self.MaxOrderedClustering
        cc_enabled = M <= self.MaxClustering
        cco_enabled = M <= self.MaxOrderedClustering
        row_clust, col_clust = self.row_clustering, self.col_clustering

        row_clust_msg = ""
        col_clust_msg = ""

        if not rco_enabled and row_clust == Clustering.OrderedClustering:
            row_clust = Clustering.Clustering
            row_clust_msg = "Row cluster ordering was disabled due to the " \
                            "input matrix being to big"
        if not rc_enabled and row_clust == Clustering.Clustering:
            row_clust = Clustering.None_
            row_clust_msg = "Row clustering was was disabled due to the " \
                            "input matrix being to big"

        if not cco_enabled and col_clust == Clustering.OrderedClustering:
            col_clust = Clustering.Clustering
            col_clust_msg = "Column cluster ordering was disabled due to " \
                            "the input matrix being to big"
        if not cc_enabled and col_clust == Clustering.Clustering:
            col_clust = Clustering.None_
            col_clust_msg = "Column clustering was disabled due to the " \
                            "input matrix being to big"

        self.col_clustering = col_clust
        self.row_clustering = row_clust

        self.Information.row_clust(row_clust_msg, shown=bool(row_clust_msg))
        self.Information.col_clust(col_clust_msg, shown=bool(col_clust_msg))

        # Disable/enable the combobox items for the clustering methods
        def setenabled(cb: QComboBox, clu: bool, clu_op: bool):
            model = cb.model()
            assert isinstance(model, QStandardItemModel)
            idx = cb.findData(Clustering.OrderedClustering, ClusteringRole)
            assert idx != -1
            model.item(idx).setEnabled(clu_op)
            idx = cb.findData(Clustering.Clustering, ClusteringRole)
            assert idx != -1
            model.item(idx).setEnabled(clu)

        setenabled(self.row_cluster_cb, rc_enabled, rco_enabled)
        setenabled(self.col_cluster_cb, cc_enabled, cco_enabled)

    def update_averages_stripe(self):
        """Update the visibility of the averages stripe.
        """
        widget = self.scene.widget
        if widget is not None:
            widget.setShowAverages(self.averages)

    def update_lowslider(self):
        low, high = self.controls.threshold_low, self.controls.threshold_high
        if low.value() >= high.value():
            low.setSliderPosition(high.value() - 1)
        self.update_color_schema()

    def update_highslider(self):
        low, high = self.controls.threshold_low, self.controls.threshold_high
        if low.value() >= high.value():
            high.setSliderPosition(low.value() + 1)
        self.update_color_schema()

    def update_color_schema(self):
        self.palette_name = self.color_cb.currentData().name
        w = self.scene.widget
        if w is not None:
            w.setColorMap(self.color_map())

    def __update_column_clustering(self):
        self.update_heatmaps()
        self.commit()

    def __update_row_clustering(self):
        self.update_heatmaps()
        self.commit()

    def update_legend(self):
        widget = self.scene.widget
        if widget is not None:
            widget.setLegendVisible(self.legend)

    def row_annotation_var(self):
        return self.annotation_var

    def row_annotation_data(self):
        var = self.row_annotation_var()
        if var is None:
            return None
        return column_str_from_table(self.input_data, var)

    def _merge_row_indices(self):
        if self.merge_kmeans and self.kmeans_model is not None:
            return self.merge_indices
        else:
            return None

    def set_annotation_var(self, var: Union[None, Variable, int]):
        if isinstance(var, int):
            var = self.annotation_model[var]
        if self.annotation_var != var:
            self.annotation_var = var
            self.update_annotations()

    def update_annotations(self):
        widget = self.scene.widget
        if widget is not None:
            annot_col = self.row_annotation_data()
            merge_indices = self._merge_row_indices()
            if merge_indices is not None and annot_col is not None:
                join = lambda _1: join_elided(", ", 42, _1, " ({} more)")
                annot_col = aggregate_apply(join, annot_col, merge_indices)
            if annot_col is not None:
                widget.setRowLabels(annot_col)
                widget.setRowLabelsVisible(True)
            else:
                widget.setRowLabelsVisible(False)
                widget.setRowLabels(None)

    def row_side_colors(self):
        var = self.annotation_color_var
        if var is None:
            return None
        column_data = column_data_from_table(self.input_data, var)
        span = (np.nanmin(column_data), np.nanmax(column_data))
        merges = self._merge_row_indices()
        if merges is not None:
            column_data = aggregate(var, column_data, merges)
        data, colormap = self._colorize(var, column_data)
        if var.is_continuous:
            colormap.span = span
        return data, colormap, var

    def set_annotation_color_var(self, var: Union[None, Variable, int]):
        """Set the current side color annotation variable."""
        if isinstance(var, int):
            var = self.row_side_color_model[var]
        if self.annotation_color_var != var:
            self.annotation_color_var = var
            self.update_row_side_colors()

    def update_row_side_colors(self):
        widget = self.scene.widget
        if widget is None:
            return
        colors = self.row_side_colors()
        if colors is None:
            widget.setRowSideColorAnnotations(None)
        else:
            widget.setRowSideColorAnnotations(colors[0], colors[1], colors[2].name)

    def _colorize(self, var: Variable, data: np.ndarray) -> Tuple[np.ndarray, ColorMap]:
        palette = var.palette  # type: Palette
        colors = np.array(
            [[c.red(), c.green(), c.blue()] for c in palette.qcolors_w_nan],
            dtype=np.uint8,
        )
        if var.is_discrete:
            mask = np.isnan(data)
            data[mask] = -1
            data = data.astype(int)
            if mask.any():
                values = (*var.values, "N/A")
            else:
                values = var.values
                colors = colors[: -1]
            return data, CategoricalColorMap(colors, values)
        elif var.is_continuous:
            cmap = GradientColorMap(colors[:-1])
            return data, cmap
        else:
            raise TypeError

    def update_column_annotations(self):
        widget = self.scene.widget
        if self.data is not None and widget is not None:
            widget.setColumnLabelsPosition(self._column_label_pos)

    def __adjust_font_size(self, diff):
        widget = self.scene.widget
        if widget is None:
            return
        curr = widget.font().pointSizeF()
        new = curr + diff

        self.__font_dec.setEnabled(new > 1.0)
        self.__font_inc.setEnabled(new <= 32)
        if new > 1.0:
            font = QFont()
            font.setPointSizeF(new)
            widget.setFont(font)

    def _on_view_context_menu(self, pos):
        widget = self.scene.widget
        if widget is None:
            return
        assert isinstance(widget, HeatmapGridWidget)
        menu = QMenu(self.view.viewport())
        menu.setAttribute(Qt.WA_DeleteOnClose)
        menu.addActions(self.view.actions())
        menu.addSeparator()
        menu.addActions([self.__font_inc, self.__font_dec])
        menu.addSeparator()
        a = QAction("Keep aspect ratio", menu, checkable=True)
        a.setChecked(self.keep_aspect)

        def ontoggled(state):
            self.keep_aspect = state
            self.__aspect_mode_changed()
        a.toggled.connect(ontoggled)
        menu.addAction(a)
        menu.popup(self.view.viewport().mapToGlobal(pos))

    def on_selection_finished(self):
        if self.scene.widget is not None:
            self.selected_rows = list(self.scene.widget.selectedRows())
        else:
            self.selected_rows = []
        self.commit()

    def commit(self):
        data = None
        indices = None
        if self.merge_kmeans:
            merge_indices = self.merge_indices
        else:
            merge_indices = None

        if self.input_data is not None and self.selected_rows:
            indices = self.selected_rows
            if merge_indices is not None:
                # expand merged indices
                indices = np.hstack([merge_indices[i] for i in indices])

            data = self.input_data[indices]

        summary = len(data) if data else self.info.NoOutput
        details = format_summary_details(data) if data else ""
        self.info.set_output_summary(summary, details)
        self.Outputs.selected_data.send(data)
        self.Outputs.annotated_data.send(create_annotated_table(self.input_data, indices))

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

    def send_report(self):
        self.report_items((
            ("Columns:", "Clustering" if self.col_clustering else "No sorting"),
            ("Rows:", "Clustering" if self.row_clustering else "No sorting"),
            ("Split:",
             self.split_by_var is not None and self.split_by_var.name),
            ("Row annotation",
             self.annotation_var is not None and self.annotation_var.name),
        ))
        self.report_plot()

    @classmethod
    def migrate_settings(cls, settings, version):
        if version is not None and version < 3:
            def st2cl(state: bool) -> Clustering:
                return Clustering.OrderedClustering if state else \
                    Clustering.None_

            rc = settings.pop("row_clustering", False)
            cc = settings.pop("col_clustering", False)
            settings["row_clustering_method"] = st2cl(rc).name
            settings["col_clustering_method"] = st2cl(cc).name


# If StickyGraphicsView ever defines qt signals/slots/properties this will
# break
class GraphicsView(GraphicsWidgetView, StickyGraphicsView):
    pass


class RowPart(NamedTuple):
    """
    A row group

    Attributes
    ----------
    title: str
        Group title
    indices : (N, ) Sequence[int]
        Indices in the input data to retrieve the row subset for the group.
    cluster : hierarchical.Tree optional
    cluster_ordered : hierarchical.Tree optional
    """
    title: str
    indices: Sequence[int]
    cluster: Optional[hierarchical.Tree] = None
    cluster_ordered: Optional[hierarchical.Tree] = None

    @property
    def can_cluster(self) -> bool:
        if isinstance(self.indices, slice):
            return (self.indices.stop - self.indices.start) > 1
        else:
            return len(self.indices) > 1


class ColumnPart(NamedTuple):
    """
    A column group

    Attributes
    ----------
    title : str
        Column group title
    indices : (N, ) int ndarray
        Indexes the input data to retrieve the column subset for the group.
    domain : List[Variable]
        List of variables in the group.
    cluster : hierarchical.Tree optional
    cluster_ordered : hierarchical.Tree optional
    """
    title: str
    indices: Sequence[int]
    domain: Sequence[int]
    cluster: Optional[hierarchical.Tree] = None
    cluster_ordered: Optional[hierarchical.Tree] = None


class Parts(NamedTuple):
    rows: Sequence[RowPart]
    columns: Sequence[ColumnPart]
    span: Tuple[float, float]


def join_elided(sep, maxlen, values, elidetemplate="..."):
    def generate(sep, ellidetemplate, values):
        count = len(values)
        length = 0
        parts = []
        for i, val in enumerate(values):
            elide = ellidetemplate.format(count - i) if count - i > 1 else ""
            parts.append(val)
            length += len(val) + (len(sep) if parts else 0)
            yield i, islice(parts, i + 1), length, elide

    best = None
    for _, parts, length, elide in generate(sep, elidetemplate, values):
        if length > maxlen:
            if best is None:
                best = sep.join(parts) + elide
            return best
        fulllen = length + len(elide)
        if fulllen < maxlen or best is None:
            best = sep.join(parts) + elide
    return best


def column_str_from_table(
        table: Orange.data.Table,
        column: Union[int, Orange.data.Variable],
) -> np.ndarray:
    var = table.domain[column]
    data, _ = table.get_column_view(column)
    return np.asarray([var.str_val(v) for v in data], dtype=object)


def column_data_from_table(
        table: Orange.data.Table,
        column: Union[int, Orange.data.Variable],
) -> np.ndarray:
    var = table.domain[column]
    data, _ = table.get_column_view(column)
    if var.is_primitive() and data.dtype.kind != "f":
        data = data.astype(float)
    return data


def aggregate(
        var: Variable, data: np.ndarray, groupindices: Sequence[Sequence[int]],
) -> np.ndarray:
    if var.is_string:
        join = lambda values: (join_elided(", ", 42, values, " ({} more)"))
        # collect all original labels for every merged row
        values = [data[indices] for indices in groupindices]
        data = [join(list(map(var.str_val, vals))) for vals in values]
        return np.array(data, dtype=object)
    elif var.is_continuous:
        data = [np.nanmean(data[indices]) if len(indices) else np.nan
                for indices in groupindices]
        return np.array(data, dtype=float)
    elif var.is_discrete:
        from Orange.statistics.util import nanmode
        data = [nanmode(data[indices])[0] if len(indices) else np.nan
                for indices in groupindices]
        return np.asarray(data, dtype=float)
    else:
        raise TypeError(type(var))


def agg_join_str(var, data, groupindices, maxlen=50, elidetemplate=" ({} more)"):
    join_s = lambda values: (
        join_elided(", ", maxlen, values, elidetemplate=elidetemplate)
    )
    join = lambda values: join_s(map(var.str_val, values))
    return aggregate_apply(join, data, groupindices)


_T = TypeVar("_T")


def aggregate_apply(
        f: Callable[[Sequence], _T],
        data: np.ndarray,
        groupindices: Sequence[Sequence[int]]
) -> Sequence[_T]:
    return [f(data[indices]) for indices in groupindices]


if __name__ == "__main__":  # pragma: no cover
    WidgetPreview(OWHeatMap).run(Table("brown-selected.tab"))

```
--- 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:** Missing Split Variable Values Not Displayed as Separate "N/A" Group in HeatMap Widget

**Description:**
When using the HeatMap widget with a split variable that contains missing values (`NaN`), the widget fails to display these instances as a separate "N/A" group. Instead, instances with undefined split variable values are excluded from the visualization, leading to incomplete data representation.

**Example:**
Suppose you have a dataset where the split variable has some missing values:

```python
data = self.brown_selected[::3].copy()
data.Y[::5] = np.nan  # Introduce missing values
self.send_signal(self.widget.Inputs.data, data, widget=w)
```

In this scenario, the HeatMap should create an additional group labeled "N/A" to represent the instances with missing split variable values. However, currently, the widget only displays groups for the defined split variable values and omits the "N/A" group.

**Expected Behavior:**
The HeatMap widget should include an "N/A" group that aggregates all instances with undefined split variable values, ensuring that all data is represented accurately in the visualization.

**Actual Behavior:**
Instances with missing split variable values are not displayed in the HeatMap, and no "N/A" group is present. This results in an incomplete visualization where some data points are excluded without indication.

[/ISSUE]
{
    "TestOWHeatMap.test_cls_with_single_instance": "PASSED",
    "TestOWHeatMap.test_cluster_column_on_all_zero_column": "PASSED",
    "TestOWHeatMap.test_color_low_high": "PASSED",
    "TestOWHeatMap.test_data_column_nans": "PASSED",
    "TestOWHeatMap.test_empty_clusters": "PASSED",
    "TestOWHeatMap.test_error_message": "PASSED",
    "TestOWHeatMap.test_image_export": "PASSED",
    "TestOWHeatMap.test_information_message": "PASSED",
    "TestOWHeatMap.test_input_data": "PASSED",
    "TestOWHeatMap.test_migrate_settings_v3": "PASSED",
    "TestOWHeatMap.test_minimum_size": "PASSED",
    "TestOWHeatMap.test_msg_base_class": "PASSED",
    "TestOWHeatMap.test_not_enough_data_settings_changed": "PASSED",
    "TestOWHeatMap.test_outputs": "PASSED",
    "TestOWHeatMap.test_palette_center": "PASSED",
    "TestOWHeatMap.test_palette_centering": "PASSED",
    "TestOWHeatMap.test_row_color_annotations": "PASSED",
    "TestOWHeatMap.test_saved_selection": "PASSED",
    "TestOWHeatMap.test_set_split_var": "PASSED",
    "TestOWHeatMap.test_set_split_var_missing": "PASSED",
    "TestOWHeatMap.test_settings_changed": "PASSED",
    "TestOWHeatMap.test_unconditional_commit_on_new_signal": "PASSED",
    "TestOWHeatMap.test_use_enough_colors": "PASSED",
    "TestOWHeatMap.test_summary": "FAILED"
}
diff --git a/Orange/widgets/visualize/owheatmap.py b/Orange/widgets/visualize/owheatmap.py
index 5d1aabf8b..6a28b7c9f 100644
--- a/Orange/widgets/visualize/owheatmap.py
+++ b/Orange/widgets/visualize/owheatmap.py
@@ -200,8 +200,6 @@ class OWHeatMap(widget.OWWidget):
 
     class Warning(widget.OWWidget.Warning):
         empty_clusters = Msg("Empty clusters were removed")
-        missing_split_values = Msg(
-            "Instances with undefined split variable values are not shown")
 
     def __init__(self):
         super().__init__()
@@ -602,12 +600,6 @@ class OWHeatMap(widget.OWWidget):
                 self.selected_rows = []
         else:
             self.clear()
-        if self.parts is not None and self.effective_data is not None:
-            nrows = sum(len(p.indices) for p in self.parts.rows)
-            self.Warning.missing_split_values(
-                shown=nrows < len(self.effective_data))
-        else:
-            self.Warning.missing_split_values.clear()
 
     def update_merge(self):
         self.kmeans_model = None
@@ -625,9 +617,15 @@ class OWHeatMap(widget.OWWidget):
             _col_data = table_column_data(data, group_var)
             row_indices = [np.flatnonzero(_col_data == i)
                            for i in range(len(group_var.values))]
+
             row_groups = [RowPart(title=name, indices=ind,
                                   cluster=None, cluster_ordered=None)
                           for name, ind in zip(group_var.values, row_indices)]
+            if np.any(_col_data.mask):
+                row_groups.append(RowPart(
+                    title="N/A", indices=np.flatnonzero(_col_data.mask),
+                    cluster=None, cluster_ordered=None
+                ))
         else:
             row_groups = [RowPart(title=None, indices=range(0, len(data)),
                                   cluster=None, cluster_ordered=None)]

{"Orange/widgets/visualize/owheatmap.py": "import enum\nfrom itertools import islice\nfrom typing import (\n    Iterable, Mapping, Any, TypeVar, Type, NamedTuple, Sequence, Optional,\n    Union, Tuple, List, Callable\n)\n\nimport numpy as np\nimport scipy.sparse as sp\n\nfrom AnyQt.QtWidgets import (\n    QGraphicsScene, QGraphicsView, QFormLayout, QComboBox, QGroupBox,\n    QMenu, QAction, QSizePolicy\n)\nfrom AnyQt.QtGui import QStandardItemModel, QStandardItem, QFont, QKeySequence\nfrom AnyQt.QtCore import Qt, QSize, QRectF, QObject\n\nfrom orangewidget.utils.combobox import ComboBox, ComboBoxSearch\nfrom Orange.data import Domain, Table, Variable, DiscreteVariable\nfrom Orange.data.sql.table import SqlTable\nimport Orange.distance\n\nfrom Orange.clustering import hierarchical, kmeans\nfrom Orange.widgets.utils import colorpalettes, apply_all\nfrom Orange.widgets.utils.itemmodels import DomainModel\nfrom Orange.widgets.utils.stickygraphicsview import StickyGraphicsView\nfrom Orange.widgets.utils.graphicsview import GraphicsWidgetView\nfrom Orange.widgets.utils.colorpalettes import DiscretePalette, Palette\n\nfrom Orange.widgets.utils.annotated_data import (create_annotated_table,\n                                                 ANNOTATED_DATA_SIGNAL_NAME)\nfrom Orange.widgets import widget, gui, settings\nfrom Orange.widgets.widget import Msg, Input, Output\n\nfrom Orange.widgets.data.oweditdomain import table_column_data\nfrom Orange.widgets.visualize.utils.heatmap import HeatmapGridWidget, \\\n    ColorMap, CategoricalColorMap, GradientColorMap\nfrom Orange.widgets.utils.widgetpreview import WidgetPreview\nfrom Orange.widgets.utils.state_summary import format_summary_details\n\n\n__all__ = []\n\n\ndef kmeans_compress(X, k=50):\n    km = kmeans.KMeans(n_clusters=k, n_init=5, random_state=42)\n    return km.get_model(X)\n\n\ndef cbselect(cb: QComboBox, value, role: Qt.ItemDataRole = Qt.EditRole) -> None:\n    \"\"\"\n    Find and select the `value` in the `cb` QComboBox.\n\n    Parameters\n    ----------\n    cb: QComboBox\n    value: Any\n    role: Qt.ItemDataRole\n        The data role in the combo box model to match value against\n    \"\"\"\n    cb.setCurrentIndex(cb.findData(value, role))\n\n\nclass Clustering(enum.IntEnum):\n    #: No clustering\n    None_ = 0\n    #: Hierarchical clustering\n    Clustering = 1\n    #: Hierarchical clustering with optimal leaf ordering\n    OrderedClustering = 2\n\n\nClusteringRole = Qt.UserRole + 13\n#: Item data for clustering method selection models\nClusteringModelData = [\n    {\n        Qt.DisplayRole: \"None\",\n        Qt.ToolTipRole: \"No clustering\",\n        ClusteringRole: Clustering.None_,\n    }, {\n        Qt.DisplayRole: \"Clustering\",\n        Qt.ToolTipRole: \"Apply hierarchical clustering\",\n        ClusteringRole: Clustering.Clustering,\n    }, {\n        Qt.DisplayRole: \"Clustering (opt. ordering)\",\n        Qt.ToolTipRole: \"Apply hierarchical clustering with optimal leaf \"\n                        \"ordering.\",\n        ClusteringRole: Clustering.OrderedClustering,\n    }\n]\n\nColumnLabelsPosData = [\n    {Qt.DisplayRole: name, Qt.UserRole: value}\n    for name, value in [\n        (\"None\", HeatmapGridWidget.NoPosition),\n        (\"Top\", HeatmapGridWidget.PositionTop),\n        (\"Bottom\", HeatmapGridWidget.PositionBottom),\n        (\"Top and Bottom\", (HeatmapGridWidget.PositionTop |\n                            HeatmapGridWidget.PositionBottom)),\n    ]\n]\n\n\ndef create_list_model(\n        items: Iterable[Mapping[Qt.ItemDataRole, Any]],\n        parent: Optional[QObject] = None,\n) -> QStandardItemModel:\n    \"\"\"Create list model from an item date iterable.\"\"\"\n    model = QStandardItemModel(parent)\n    for item in items:\n        sitem = QStandardItem()\n        for role, value in item.items():\n            sitem.setData(value, role)\n        model.appendRow([sitem])\n    return model\n\n\nE = TypeVar(\"E\", bound=enum.Enum)  # pylint: disable=invalid-name\n\n\ndef enum_get(etype: Type[E], name: str, default: E) -> E:\n    \"\"\"\n    Return an Enum member by `name`. If no such member exists in `etype`\n    return `default`.\n    \"\"\"\n    try:\n        return etype[name]\n    except LookupError:\n        return default\n\n\nclass OWHeatMap(widget.OWWidget):\n    name = \"Heat Map\"\n    description = \"Plot a data matrix heatmap.\"\n    icon = \"icons/Heatmap.svg\"\n    priority = 260\n    keywords = []\n\n    class Inputs:\n        data = Input(\"Data\", Table)\n\n    class Outputs:\n        selected_data = Output(\"Selected Data\", Table, default=True)\n        annotated_data = Output(ANNOTATED_DATA_SIGNAL_NAME, Table)\n\n    settings_version = 3\n\n    settingsHandler = settings.DomainContextHandler()\n\n    # Disable clustering for inputs bigger than this\n    MaxClustering = 25000\n    # Disable cluster leaf ordering for inputs bigger than this\n    MaxOrderedClustering = 1000\n\n    threshold_low = settings.Setting(0.0)\n    threshold_high = settings.Setting(1.0)\n\n    merge_kmeans = settings.Setting(False)\n    merge_kmeans_k = settings.Setting(50)\n\n    # Display column with averages\n    averages: bool = settings.Setting(True)\n    # Display legend\n    legend: bool = settings.Setting(True)\n    # Annotations\n    #: text row annotation (row names)\n    annotation_var = settings.ContextSetting(None)\n    #: color row annotation\n    annotation_color_var = settings.ContextSetting(None)\n    # Discrete variable used to split that data/heatmaps (vertically)\n    split_by_var = settings.ContextSetting(None)\n    # Selected row/column clustering method (name)\n    col_clustering_method: str = settings.Setting(Clustering.None_.name)\n    row_clustering_method: str = settings.Setting(Clustering.None_.name)\n\n    palette_name = settings.Setting(colorpalettes.DefaultContinuousPaletteName)\n    column_label_pos: int = settings.Setting(1)\n    selected_rows: List[int] = settings.Setting(None, schema_only=True)\n\n    auto_commit = settings.Setting(True)\n\n    graph_name = \"scene\"\n\n    left_side_scrolling = True\n\n    class Information(widget.OWWidget.Information):\n        sampled = Msg(\"Data has been sampled\")\n        discrete_ignored = Msg(\"{} categorical feature{} ignored\")\n        row_clust = Msg(\"{}\")\n        col_clust = Msg(\"{}\")\n        sparse_densified = Msg(\"Showing this data may require a lot of memory\")\n\n    class Error(widget.OWWidget.Error):\n        no_continuous = Msg(\"No numeric features\")\n        not_enough_features = Msg(\"Not enough features for column clustering\")\n        not_enough_instances = Msg(\"Not enough instances for clustering\")\n        not_enough_instances_k_means = Msg(\n            \"Not enough instances for k-means merging\")\n        not_enough_memory = Msg(\"Not enough memory to show this data\")\n\n    class Warning(widget.OWWidget.Warning):\n        empty_clusters = Msg(\"Empty clusters were removed\")\n        missing_split_values = Msg(\n            \"Instances with undefined split variable values are not shown\")\n\n    def __init__(self):\n        super().__init__()\n        self.__pending_selection = self.selected_rows\n\n        # A kingdom for a save_state/restore_state\n        self.col_clustering = enum_get(\n            Clustering, self.col_clustering_method, Clustering.None_)\n        self.row_clustering = enum_get(\n            Clustering, self.row_clustering_method, Clustering.None_)\n\n        @self.settingsAboutToBePacked.connect\n        def _():\n            self.col_clustering_method = self.col_clustering.name\n            self.row_clustering_method = self.row_clustering.name\n\n        self.keep_aspect = False\n\n        #: The original data with all features (retained to\n        #: preserve the domain on the output)\n        self.input_data = None\n        #: The effective data striped of discrete features, and often\n        #: merged using k-means\n        self.data = None\n        self.effective_data = None\n        #: kmeans model used to merge rows of input_data\n        self.kmeans_model = None\n        #: merge indices derived from kmeans\n        #: a list (len==k) of int ndarray where the i-th item contains\n        #: the indices which merge the input_data into the heatmap row i\n        self.merge_indices = None\n        self.parts: Optional[Parts] = None\n        self.__rows_cache = {}\n        self.__columns_cache = {}\n\n        self.info.set_input_summary(self.info.NoInput)\n        self.info.set_output_summary(self.info.NoOutput)\n\n        # GUI definition\n        colorbox = gui.vBox(self.controlArea, \"Color\")\n        self.color_cb = gui.palette_combo_box(self.palette_name)\n        self.color_cb.currentIndexChanged.connect(self.update_color_schema)\n        colorbox.layout().addWidget(self.color_cb)\n\n        form = QFormLayout(\n            formAlignment=Qt.AlignLeft,\n            labelAlignment=Qt.AlignLeft,\n            fieldGrowthPolicy=QFormLayout.AllNonFixedFieldsGrow\n        )\n        lowslider = gui.hSlider(\n            colorbox, self, \"threshold_low\", minValue=0.0, maxValue=1.0,\n            step=0.05, ticks=True, intOnly=False,\n            createLabel=False, callback=self.update_lowslider)\n        highslider = gui.hSlider(\n            colorbox, self, \"threshold_high\", minValue=0.0, maxValue=1.0,\n            step=0.05, ticks=True, intOnly=False,\n            createLabel=False, callback=self.update_highslider)\n\n        form.addRow(\"Low:\", lowslider)\n        form.addRow(\"High:\", highslider)\n\n        colorbox.layout().addLayout(form)\n\n        mergebox = gui.vBox(self.controlArea, \"Merge\",)\n        gui.checkBox(mergebox, self, \"merge_kmeans\", \"Merge by k-means\",\n                     callback=self.__update_row_clustering)\n        ibox = gui.indentedBox(mergebox)\n        gui.spin(ibox, self, \"merge_kmeans_k\", minv=5, maxv=500,\n                 label=\"Clusters:\", keyboardTracking=False,\n                 callbackOnReturn=True, callback=self.update_merge)\n\n        cluster_box = gui.vBox(self.controlArea, \"Clustering\")\n        # Row clustering\n        self.row_cluster_cb = cb = ComboBox()\n        cb.setModel(create_list_model(ClusteringModelData, self))\n        cbselect(cb, self.row_clustering, ClusteringRole)\n        self.connect_control(\n            \"row_clustering\",\n            lambda value, cb=cb: cbselect(cb, value, ClusteringRole)\n        )\n        @cb.activated.connect\n        def _(idx, cb=cb):\n            self.set_row_clustering(cb.itemData(idx, ClusteringRole))\n\n        # Column clustering\n        self.col_cluster_cb = cb = ComboBox()\n        cb.setModel(create_list_model(ClusteringModelData, self))\n        cbselect(cb, self.col_clustering, ClusteringRole)\n        self.connect_control(\n            \"col_clustering\",\n            lambda value, cb=cb: cbselect(cb, value, ClusteringRole)\n        )\n        @cb.activated.connect\n        def _(idx, cb=cb):\n            self.set_col_clustering(cb.itemData(idx, ClusteringRole))\n\n        form = QFormLayout(\n            labelAlignment=Qt.AlignLeft, formAlignment=Qt.AlignLeft,\n            fieldGrowthPolicy=QFormLayout.AllNonFixedFieldsGrow,\n        )\n        form.addRow(\"Rows:\", self.row_cluster_cb)\n        form.addRow(\"Columns:\", self.col_cluster_cb)\n        cluster_box.layout().addLayout(form)\n        box = gui.vBox(self.controlArea, \"Split By\")\n\n        self.row_split_model = DomainModel(\n            placeholder=\"(None)\",\n            valid_types=(Orange.data.DiscreteVariable,),\n            parent=self,\n        )\n        self.row_split_cb = cb = ComboBoxSearch(\n            enabled=not self.merge_kmeans,\n            sizeAdjustPolicy=ComboBox.AdjustToMinimumContentsLengthWithIcon,\n            minimumContentsLength=14,\n            toolTip=\"Split the heatmap vertically by a categorical column\"\n        )\n        self.row_split_cb.setModel(self.row_split_model)\n        self.connect_control(\n            \"split_by_var\", lambda value, cb=cb: cbselect(cb, value)\n        )\n        self.connect_control(\n            \"merge_kmeans\", self.row_split_cb.setDisabled\n        )\n        self.split_by_var = None\n\n        self.row_split_cb.activated.connect(\n            self.__on_split_rows_activated\n        )\n        box.layout().addWidget(self.row_split_cb)\n\n        box = gui.vBox(self.controlArea, 'Annotation && Legends')\n\n        gui.checkBox(box, self, 'legend', 'Show legend',\n                     callback=self.update_legend)\n\n        gui.checkBox(box, self, 'averages', 'Stripes with averages',\n                     callback=self.update_averages_stripe)\n        annotbox = QGroupBox(\"Row Annotations\", flat=True)\n        form = QFormLayout(\n            annotbox,\n            formAlignment=Qt.AlignLeft,\n            labelAlignment=Qt.AlignLeft,\n            fieldGrowthPolicy=QFormLayout.AllNonFixedFieldsGrow\n        )\n        self.annotation_model = DomainModel(placeholder=\"(None)\")\n        self.annotation_text_cb = ComboBoxSearch(\n            minimumContentsLength=12,\n            sizeAdjustPolicy=QComboBox.AdjustToMinimumContentsLength\n        )\n        self.annotation_text_cb.setModel(self.annotation_model)\n        self.annotation_text_cb.activated.connect(self.set_annotation_var)\n        self.connect_control(\"annotation_var\", self.annotation_var_changed)\n\n        self.row_side_color_model = DomainModel(\n            order=(DomainModel.CLASSES, DomainModel.Separator,\n                   DomainModel.METAS),\n            placeholder=\"(None)\", valid_types=DomainModel.PRIMITIVE,\n            flags=Qt.ItemIsSelectable | Qt.ItemIsEnabled,\n            parent=self,\n        )\n        self.row_side_color_cb = ComboBoxSearch(\n            sizeAdjustPolicy=QComboBox.AdjustToMinimumContentsLength,\n            minimumContentsLength=12\n        )\n        self.row_side_color_cb.setModel(self.row_side_color_model)\n        self.row_side_color_cb.activated.connect(self.set_annotation_color_var)\n        self.connect_control(\"annotation_color_var\", self.annotation_color_var_changed)\n        form.addRow(\"Text\", self.annotation_text_cb)\n        form.addRow(\"Color\", self.row_side_color_cb)\n        box.layout().addWidget(annotbox)\n        posbox = gui.vBox(box, \"Column Labels Position\", addSpace=False)\n        posbox.setFlat(True)\n        cb = gui.comboBox(\n            posbox, self, \"column_label_pos\",\n            callback=self.update_column_annotations)\n        cb.setModel(create_list_model(ColumnLabelsPosData, parent=self))\n        cb.setCurrentIndex(self.column_label_pos)\n        gui.checkBox(self.controlArea, self, \"keep_aspect\",\n                     \"Keep aspect ratio\", box=\"Resize\",\n                     callback=self.__aspect_mode_changed)\n\n        gui.rubber(self.controlArea)\n        gui.auto_send(self.controlArea, self, \"auto_commit\")\n\n        # Scene with heatmap\n        class HeatmapScene(QGraphicsScene):\n            widget: Optional[HeatmapGridWidget] = None\n\n        self.scene = self.scene = HeatmapScene(parent=self)\n        self.view = GraphicsView(\n            self.scene,\n            verticalScrollBarPolicy=Qt.ScrollBarAlwaysOn,\n            horizontalScrollBarPolicy=Qt.ScrollBarAlwaysOn,\n            viewportUpdateMode=QGraphicsView.FullViewportUpdate,\n            widgetResizable=True,\n        )\n        self.view.setContextMenuPolicy(Qt.CustomContextMenu)\n        self.view.customContextMenuRequested.connect(\n            self._on_view_context_menu\n        )\n        self.mainArea.layout().addWidget(self.view)\n        self.selected_rows = []\n        self.__font_inc = QAction(\n            \"Increase Font\", self, shortcut=QKeySequence(\"ctrl+>\"))\n        self.__font_dec = QAction(\n            \"Decrease Font\", self, shortcut=QKeySequence(\"ctrl+<\"))\n        self.__font_inc.triggered.connect(lambda: self.__adjust_font_size(1))\n        self.__font_dec.triggered.connect(lambda: self.__adjust_font_size(-1))\n        if hasattr(QAction, \"setShortcutVisibleInContextMenu\"):\n            apply_all(\n                [self.__font_inc, self.__font_dec],\n                lambda a: a.setShortcutVisibleInContextMenu(True)\n            )\n        self.addActions([self.__font_inc, self.__font_dec])\n\n    @property\n    def center_palette(self):\n        palette = self.color_cb.currentData()\n        return bool(palette.flags & palette.Diverging)\n\n    @property\n    def _column_label_pos(self) -> HeatmapGridWidget.Position:\n        return ColumnLabelsPosData[self.column_label_pos][Qt.UserRole]\n\n    def annotation_color_var_changed(self, value):\n        cbselect(self.row_side_color_cb, value, Qt.EditRole)\n\n    def annotation_var_changed(self, value):\n        cbselect(self.annotation_text_cb, value, Qt.EditRole)\n\n    def set_row_clustering(self, method: Clustering) -> None:\n        assert isinstance(method, Clustering)\n        if self.row_clustering != method:\n            self.row_clustering = method\n            cbselect(self.row_cluster_cb, method, ClusteringRole)\n            self.__update_row_clustering()\n\n    def set_col_clustering(self, method: Clustering) -> None:\n        assert isinstance(method, Clustering)\n        if self.col_clustering != method:\n            self.col_clustering = method\n            cbselect(self.col_cluster_cb, method, ClusteringRole)\n            self.__update_column_clustering()\n\n    def sizeHint(self) -> QSize:\n        return super().sizeHint().expandedTo(QSize(900, 700))\n\n    def color_palette(self):\n        return self.color_cb.currentData().lookup_table()\n\n    def color_map(self) -> GradientColorMap:\n        return GradientColorMap(\n            self.color_palette(), (self.threshold_low, self.threshold_high),\n            0 if self.center_palette else None\n        )\n\n    def clear(self):\n        self.data = None\n        self.input_data = None\n        self.effective_data = None\n        self.kmeans_model = None\n        self.merge_indices = None\n        self.annotation_model.set_domain(None)\n        self.annotation_var = None\n        self.row_side_color_model.set_domain(None)\n        self.annotation_color_var = None\n        self.row_split_model.set_domain(None)\n        self.split_by_var = None\n        self.parts = None\n        self.clear_scene()\n        self.selected_rows = []\n        self.__columns_cache.clear()\n        self.__rows_cache.clear()\n        self.__update_clustering_enable_state(None)\n\n    def clear_scene(self):\n        if self.scene.widget is not None:\n            self.scene.widget.layoutDidActivate.disconnect(\n                self.__on_layout_activate\n            )\n            self.scene.widget.selectionFinished.disconnect(\n                self.on_selection_finished\n            )\n        self.scene.widget = None\n        self.scene.clear()\n\n        self.view.setSceneRect(QRectF())\n        self.view.setHeaderSceneRect(QRectF())\n        self.view.setFooterSceneRect(QRectF())\n\n    @Inputs.data\n    def set_dataset(self, data=None):\n        \"\"\"Set the input dataset to display.\"\"\"\n        self.closeContext()\n        self.clear()\n        self.clear_messages()\n        self._set_input_summary(data)\n\n        if isinstance(data, SqlTable):\n            if data.approx_len() < 4000:\n                data = Table(data)\n            else:\n                self.Information.sampled()\n                data_sample = data.sample_time(1, no_cache=True)\n                data_sample.download_data(2000, partial=True)\n                data = Table(data_sample)\n\n        if data is not None and not len(data):\n            data = None\n\n        if data is not None and sp.issparse(data.X):\n            try:\n                data = data.to_dense()\n            except MemoryError:\n                data = None\n                self.Error.not_enough_memory()\n            else:\n                self.Information.sparse_densified()\n\n        input_data = data\n\n        # Data contains no attributes or meta attributes only\n        if data is not None and len(data.domain.attributes) == 0:\n            self.Error.no_continuous()\n            input_data = data = None\n\n        # Data contains some discrete attributes which must be filtered\n        if data is not None and \\\n                any(var.is_discrete for var in data.domain.attributes):\n            ndisc = sum(var.is_discrete for var in data.domain.attributes)\n            data = data.transform(\n                Domain([var for var in data.domain.attributes\n                        if var.is_continuous],\n                       data.domain.class_vars,\n                       data.domain.metas))\n            if not data.domain.attributes:\n                self.Error.no_continuous()\n                input_data = data = None\n            else:\n                self.Information.discrete_ignored(\n                    ndisc, \"s\" if ndisc > 1 else \"\")\n\n        self.data = data\n        self.input_data = input_data\n\n        if data is not None:\n            self.annotation_model.set_domain(self.input_data.domain)\n            self.row_side_color_model.set_domain(self.input_data.domain)\n            self.annotation_var = None\n            self.annotation_color_var = None\n            self.row_split_model.set_domain(data.domain)\n            if data.domain.has_discrete_class:\n                self.split_by_var = data.domain.class_var\n            else:\n                self.split_by_var = None\n            self.openContext(self.input_data)\n            if self.split_by_var not in self.row_split_model:\n                self.split_by_var = None\n\n        self.update_heatmaps()\n        if data is not None and self.__pending_selection is not None:\n            assert self.scene.widget is not None\n            self.scene.widget.selectRows(self.__pending_selection)\n            self.selected_rows = self.__pending_selection\n            self.__pending_selection = None\n\n        self.unconditional_commit()\n\n    def _set_input_summary(self, data):\n        summary = len(data) if data else self.info.NoInput\n        details = format_summary_details(data) if data else \"\"\n        self.info.set_input_summary(summary, details)\n\n    def __on_split_rows_activated(self):\n        self.set_split_variable(self.row_split_cb.currentData(Qt.EditRole))\n\n    def set_split_variable(self, var):\n        if var != self.split_by_var:\n            self.split_by_var = var\n            self.update_heatmaps()\n\n    def update_heatmaps(self):\n        if self.data is not None:\n            self.clear_scene()\n            self.clear_messages()\n            if self.col_clustering != Clustering.None_ and \\\n                    len(self.data.domain.attributes) < 2:\n                self.Error.not_enough_features()\n            elif (self.col_clustering != Clustering.None_ or\n                  self.row_clustering != Clustering.None_) and \\\n                    len(self.data) < 2:\n                self.Error.not_enough_instances()\n            elif self.merge_kmeans and len(self.data) < 3:\n                self.Error.not_enough_instances_k_means()\n            else:\n                parts = self.construct_heatmaps(self.data, self.split_by_var)\n                self.construct_heatmaps_scene(parts, self.effective_data)\n                self.selected_rows = []\n        else:\n            self.clear()\n        if self.parts is not None and self.effective_data is not None:\n            nrows = sum(len(p.indices) for p in self.parts.rows)\n            self.Warning.missing_split_values(\n                shown=nrows < len(self.effective_data))\n        else:\n            self.Warning.missing_split_values.clear()\n\n    def update_merge(self):\n        self.kmeans_model = None\n        self.merge_indices = None\n        if self.data is not None and self.merge_kmeans:\n            self.update_heatmaps()\n            self.commit()\n\n    def _make_parts(self, data, group_var=None):\n        \"\"\"\n        Make initial `Parts` for data, split by group_var, group_key\n        \"\"\"\n        if group_var is not None:\n            assert group_var.is_discrete\n            _col_data = table_column_data(data, group_var)\n            row_indices = [np.flatnonzero(_col_data == i)\n                           for i in range(len(group_var.values))]\n            row_groups = [RowPart(title=name, indices=ind,\n                                  cluster=None, cluster_ordered=None)\n                          for name, ind in zip(group_var.values, row_indices)]\n        else:\n            row_groups = [RowPart(title=None, indices=range(0, len(data)),\n                                  cluster=None, cluster_ordered=None)]\n\n        col_groups = [\n            ColumnPart(\n                title=None, indices=range(0, len(data.domain.attributes)),\n                domain=data.domain, cluster=None, cluster_ordered=None)\n        ]\n\n        minv, maxv = np.nanmin(data.X), np.nanmax(data.X)\n        return Parts(row_groups, col_groups, span=(minv, maxv))\n\n    def cluster_rows(self, data: Table, parts: 'Parts', ordered=False) -> 'Parts':\n        row_groups = []\n        for row in parts.rows:\n            if row.cluster is not None:\n                cluster = row.cluster\n            else:\n                cluster = None\n            if row.cluster_ordered is not None:\n                cluster_ord = row.cluster_ordered\n            else:\n                cluster_ord = None\n\n            if row.can_cluster:\n                matrix = None\n                need_dist = cluster is None or (ordered and cluster_ord is None)\n                if need_dist:\n                    subset = data[row.indices]\n                    matrix = Orange.distance.Euclidean(subset)\n\n                if cluster is None:\n                    assert len(matrix) < self.MaxClustering\n                    cluster = hierarchical.dist_matrix_clustering(\n                        matrix, linkage=hierarchical.WARD\n                    )\n                if ordered and cluster_ord is None:\n                    assert len(matrix) < self.MaxOrderedClustering\n                    cluster_ord = hierarchical.optimal_leaf_ordering(\n                        cluster, matrix,\n                    )\n            row_groups.append(row._replace(cluster=cluster, cluster_ordered=cluster_ord))\n\n        return parts._replace(rows=row_groups)\n\n    def cluster_columns(self, data, parts, ordered=False):\n        assert len(parts.columns) == 1, \"columns split is no longer supported\"\n        assert all(var.is_continuous for var in data.domain.attributes)\n\n        col0 = parts.columns[0]\n        if col0.cluster is not None:\n            cluster = col0.cluster\n        else:\n            cluster = None\n        if col0.cluster_ordered is not None:\n            cluster_ord = col0.cluster_ordered\n        else:\n            cluster_ord = None\n        need_dist = cluster is None or (ordered and cluster_ord is None)\n        matrix = None\n        if need_dist:\n            data = Orange.distance._preprocess(data)\n            matrix = np.asarray(Orange.distance.PearsonR(data, axis=0))\n            # nan values break clustering below\n            matrix = np.nan_to_num(matrix)\n\n        if cluster is None:\n            assert matrix is not None\n            assert len(matrix) < self.MaxClustering\n            cluster = hierarchical.dist_matrix_clustering(\n                matrix, linkage=hierarchical.WARD\n            )\n        if ordered and cluster_ord is None:\n            assert len(matrix) < self.MaxOrderedClustering\n            cluster_ord = hierarchical.optimal_leaf_ordering(cluster, matrix)\n\n        col_groups = [col._replace(cluster=cluster, cluster_ordered=cluster_ord)\n                      for col in parts.columns]\n        return parts._replace(columns=col_groups)\n\n    def construct_heatmaps(self, data, group_var=None) -> 'Parts':\n        if self.merge_kmeans:\n            if self.kmeans_model is None:\n                effective_data = self.input_data.transform(\n                    Orange.data.Domain(\n                        [var for var in self.input_data.domain.attributes\n                         if var.is_continuous],\n                        self.input_data.domain.class_vars,\n                        self.input_data.domain.metas))\n                nclust = min(self.merge_kmeans_k, len(effective_data) - 1)\n                self.kmeans_model = kmeans_compress(effective_data, k=nclust)\n                effective_data.domain = self.kmeans_model.domain\n                merge_indices = [np.flatnonzero(self.kmeans_model.labels == ind)\n                                 for ind in range(nclust)]\n                not_empty_indices = [i for i, x in enumerate(merge_indices)\n                                     if len(x) > 0]\n                self.merge_indices = \\\n                    [merge_indices[i] for i in not_empty_indices]\n                if len(merge_indices) != len(self.merge_indices):\n                    self.Warning.empty_clusters()\n                effective_data = Orange.data.Table(\n                    Orange.data.Domain(effective_data.domain.attributes),\n                    self.kmeans_model.centroids[not_empty_indices]\n                )\n            else:\n                effective_data = self.effective_data\n\n            group_var = None\n        else:\n            self.kmeans_model = None\n            self.merge_indices = None\n            effective_data = data\n\n        self.effective_data = effective_data\n\n        self.__update_clustering_enable_state(effective_data)\n\n        parts = self._make_parts(effective_data, group_var)\n        # Restore/update the row/columns items descriptions from cache if\n        # available\n        rows_cache_key = (group_var,\n                          self.merge_kmeans_k if self.merge_kmeans else None)\n        if rows_cache_key in self.__rows_cache:\n            parts = parts._replace(rows=self.__rows_cache[rows_cache_key].rows)\n\n        if self.row_clustering != Clustering.None_:\n            parts = self.cluster_rows(\n                effective_data, parts,\n                ordered=self.row_clustering == Clustering.OrderedClustering\n            )\n        if self.col_clustering != Clustering.None_:\n            parts = self.cluster_columns(\n                effective_data, parts,\n                ordered=self.col_clustering == Clustering.OrderedClustering\n            )\n\n        # Cache the updated parts\n        self.__rows_cache[rows_cache_key] = parts\n        return parts\n\n    def construct_heatmaps_scene(self, parts: 'Parts', data: Table) -> None:\n        _T = TypeVar(\"_T\", bound=Union[RowPart, ColumnPart])\n\n        def select_cluster(clustering: Clustering, item: _T) -> _T:\n            if clustering == Clustering.None_:\n                return item._replace(cluster=None, cluster_ordered=None)\n            elif clustering == Clustering.Clustering:\n                return item._replace(cluster=item.cluster, cluster_ordered=None)\n            elif clustering == Clustering.OrderedClustering:\n                return item._replace(cluster=item.cluster_ordered, cluster_ordered=None)\n            else:  # pragma: no cover\n                raise TypeError()\n\n        rows = [select_cluster(self.row_clustering, rowitem)\n                for rowitem in parts.rows]\n        cols = [select_cluster(self.col_clustering, colitem)\n                for colitem in parts.columns]\n        parts = Parts(columns=cols, rows=rows, span=parts.span)\n\n        self.setup_scene(parts, data)\n\n    def setup_scene(self, parts, data):\n        # type: (Parts, Table) -> None\n        widget = HeatmapGridWidget()\n        widget.setColorMap(self.color_map())\n        self.scene.addItem(widget)\n        self.scene.widget = widget\n        columns = [v.name for v in data.domain.attributes]\n        parts = HeatmapGridWidget.Parts(\n            rows=[\n                HeatmapGridWidget.RowItem(r.title, r.indices, r.cluster)\n                for r in parts.rows\n            ],\n            columns=[\n                HeatmapGridWidget.ColumnItem(c.title, c.indices, c.cluster)\n                for c in parts.columns\n            ],\n            data=data.X,\n            span=parts.span,\n            row_names=None,\n            col_names=columns,\n        )\n        widget.setHeatmaps(parts)\n        side = self.row_side_colors()\n        if side is not None:\n            widget.setRowSideColorAnnotations(side[0], side[1], name=side[2].name)\n        widget.setColumnLabelsPosition(self._column_label_pos)\n        widget.setAspectRatioMode(\n            Qt.KeepAspectRatio if self.keep_aspect else Qt.IgnoreAspectRatio\n        )\n        widget.setShowAverages(self.averages)\n        widget.setLegendVisible(self.legend)\n\n        widget.layoutDidActivate.connect(self.__on_layout_activate)\n        widget.selectionFinished.connect(self.on_selection_finished)\n\n        self.update_annotations()\n        self.view.setCentralWidget(widget)\n        self.parts = parts\n\n    def __update_scene_rects(self):\n        widget = self.scene.widget\n        if widget is None:\n            return\n        rect = widget.geometry()\n        self.scene.setSceneRect(rect)\n        self.view.setSceneRect(rect)\n        self.view.setHeaderSceneRect(widget.headerGeometry())\n        self.view.setFooterSceneRect(widget.footerGeometry())\n\n    def __on_layout_activate(self):\n        self.__update_scene_rects()\n\n    def __aspect_mode_changed(self):\n        widget = self.scene.widget\n        if widget is None:\n            return\n        widget.setAspectRatioMode(\n            Qt.KeepAspectRatio if self.keep_aspect else Qt.IgnoreAspectRatio\n        )\n        # when aspect fixed the vertical sh is fixex, when not, it can\n        # shrink vertically\n        sp = widget.sizePolicy()\n        if self.keep_aspect:\n            sp.setVerticalPolicy(QSizePolicy.Fixed)\n        else:\n            sp.setVerticalPolicy(QSizePolicy.Preferred)\n        widget.setSizePolicy(sp)\n\n    def __update_clustering_enable_state(self, data):\n        if data is not None:\n            N = len(data)\n            M = len(data.domain.attributes)\n        else:\n            N = M = 0\n\n        rc_enabled = N <= self.MaxClustering\n        rco_enabled = N <= self.MaxOrderedClustering\n        cc_enabled = M <= self.MaxClustering\n        cco_enabled = M <= self.MaxOrderedClustering\n        row_clust, col_clust = self.row_clustering, self.col_clustering\n\n        row_clust_msg = \"\"\n        col_clust_msg = \"\"\n\n        if not rco_enabled and row_clust == Clustering.OrderedClustering:\n            row_clust = Clustering.Clustering\n            row_clust_msg = \"Row cluster ordering was disabled due to the \" \\\n                            \"input matrix being to big\"\n        if not rc_enabled and row_clust == Clustering.Clustering:\n            row_clust = Clustering.None_\n            row_clust_msg = \"Row clustering was was disabled due to the \" \\\n                            \"input matrix being to big\"\n\n        if not cco_enabled and col_clust == Clustering.OrderedClustering:\n            col_clust = Clustering.Clustering\n            col_clust_msg = \"Column cluster ordering was disabled due to \" \\\n                            \"the input matrix being to big\"\n        if not cc_enabled and col_clust == Clustering.Clustering:\n            col_clust = Clustering.None_\n            col_clust_msg = \"Column clustering was disabled due to the \" \\\n                            \"input matrix being to big\"\n\n        self.col_clustering = col_clust\n        self.row_clustering = row_clust\n\n        self.Information.row_clust(row_clust_msg, shown=bool(row_clust_msg))\n        self.Information.col_clust(col_clust_msg, shown=bool(col_clust_msg))\n\n        # Disable/enable the combobox items for the clustering methods\n        def setenabled(cb: QComboBox, clu: bool, clu_op: bool):\n            model = cb.model()\n            assert isinstance(model, QStandardItemModel)\n            idx = cb.findData(Clustering.OrderedClustering, ClusteringRole)\n            assert idx != -1\n            model.item(idx).setEnabled(clu_op)\n            idx = cb.findData(Clustering.Clustering, ClusteringRole)\n            assert idx != -1\n            model.item(idx).setEnabled(clu)\n\n        setenabled(self.row_cluster_cb, rc_enabled, rco_enabled)\n        setenabled(self.col_cluster_cb, cc_enabled, cco_enabled)\n\n    def update_averages_stripe(self):\n        \"\"\"Update the visibility of the averages stripe.\n        \"\"\"\n        widget = self.scene.widget\n        if widget is not None:\n            widget.setShowAverages(self.averages)\n\n    def update_lowslider(self):\n        low, high = self.controls.threshold_low, self.controls.threshold_high\n        if low.value() >= high.value():\n            low.setSliderPosition(high.value() - 1)\n        self.update_color_schema()\n\n    def update_highslider(self):\n        low, high = self.controls.threshold_low, self.controls.threshold_high\n        if low.value() >= high.value():\n            high.setSliderPosition(low.value() + 1)\n        self.update_color_schema()\n\n    def update_color_schema(self):\n        self.palette_name = self.color_cb.currentData().name\n        w = self.scene.widget\n        if w is not None:\n            w.setColorMap(self.color_map())\n\n    def __update_column_clustering(self):\n        self.update_heatmaps()\n        self.commit()\n\n    def __update_row_clustering(self):\n        self.update_heatmaps()\n        self.commit()\n\n    def update_legend(self):\n        widget = self.scene.widget\n        if widget is not None:\n            widget.setLegendVisible(self.legend)\n\n    def row_annotation_var(self):\n        return self.annotation_var\n\n    def row_annotation_data(self):\n        var = self.row_annotation_var()\n        if var is None:\n            return None\n        return column_str_from_table(self.input_data, var)\n\n    def _merge_row_indices(self):\n        if self.merge_kmeans and self.kmeans_model is not None:\n            return self.merge_indices\n        else:\n            return None\n\n    def set_annotation_var(self, var: Union[None, Variable, int]):\n        if isinstance(var, int):\n            var = self.annotation_model[var]\n        if self.annotation_var != var:\n            self.annotation_var = var\n            self.update_annotations()\n\n    def update_annotations(self):\n        widget = self.scene.widget\n        if widget is not None:\n            annot_col = self.row_annotation_data()\n            merge_indices = self._merge_row_indices()\n            if merge_indices is not None and annot_col is not None:\n                join = lambda _1: join_elided(\", \", 42, _1, \" ({} more)\")\n                annot_col = aggregate_apply(join, annot_col, merge_indices)\n            if annot_col is not None:\n                widget.setRowLabels(annot_col)\n                widget.setRowLabelsVisible(True)\n            else:\n                widget.setRowLabelsVisible(False)\n                widget.setRowLabels(None)\n\n    def row_side_colors(self):\n        var = self.annotation_color_var\n        if var is None:\n            return None\n        column_data = column_data_from_table(self.input_data, var)\n        span = (np.nanmin(column_data), np.nanmax(column_data))\n        merges = self._merge_row_indices()\n        if merges is not None:\n            column_data = aggregate(var, column_data, merges)\n        data, colormap = self._colorize(var, column_data)\n        if var.is_continuous:\n            colormap.span = span\n        return data, colormap, var\n\n    def set_annotation_color_var(self, var: Union[None, Variable, int]):\n        \"\"\"Set the current side color annotation variable.\"\"\"\n        if isinstance(var, int):\n            var = self.row_side_color_model[var]\n        if self.annotation_color_var != var:\n            self.annotation_color_var = var\n            self.update_row_side_colors()\n\n    def update_row_side_colors(self):\n        widget = self.scene.widget\n        if widget is None:\n            return\n        colors = self.row_side_colors()\n        if colors is None:\n            widget.setRowSideColorAnnotations(None)\n        else:\n            widget.setRowSideColorAnnotations(colors[0], colors[1], colors[2].name)\n\n    def _colorize(self, var: Variable, data: np.ndarray) -> Tuple[np.ndarray, ColorMap]:\n        palette = var.palette  # type: Palette\n        colors = np.array(\n            [[c.red(), c.green(), c.blue()] for c in palette.qcolors_w_nan],\n            dtype=np.uint8,\n        )\n        if var.is_discrete:\n            mask = np.isnan(data)\n            data[mask] = -1\n            data = data.astype(int)\n            if mask.any():\n                values = (*var.values, \"N/A\")\n            else:\n                values = var.values\n                colors = colors[: -1]\n            return data, CategoricalColorMap(colors, values)\n        elif var.is_continuous:\n            cmap = GradientColorMap(colors[:-1])\n            return data, cmap\n        else:\n            raise TypeError\n\n    def update_column_annotations(self):\n        widget = self.scene.widget\n        if self.data is not None and widget is not None:\n            widget.setColumnLabelsPosition(self._column_label_pos)\n\n    def __adjust_font_size(self, diff):\n        widget = self.scene.widget\n        if widget is None:\n            return\n        curr = widget.font().pointSizeF()\n        new = curr + diff\n\n        self.__font_dec.setEnabled(new > 1.0)\n        self.__font_inc.setEnabled(new <= 32)\n        if new > 1.0:\n            font = QFont()\n            font.setPointSizeF(new)\n            widget.setFont(font)\n\n    def _on_view_context_menu(self, pos):\n        widget = self.scene.widget\n        if widget is None:\n            return\n        assert isinstance(widget, HeatmapGridWidget)\n        menu = QMenu(self.view.viewport())\n        menu.setAttribute(Qt.WA_DeleteOnClose)\n        menu.addActions(self.view.actions())\n        menu.addSeparator()\n        menu.addActions([self.__font_inc, self.__font_dec])\n        menu.addSeparator()\n        a = QAction(\"Keep aspect ratio\", menu, checkable=True)\n        a.setChecked(self.keep_aspect)\n\n        def ontoggled(state):\n            self.keep_aspect = state\n            self.__aspect_mode_changed()\n        a.toggled.connect(ontoggled)\n        menu.addAction(a)\n        menu.popup(self.view.viewport().mapToGlobal(pos))\n\n    def on_selection_finished(self):\n        if self.scene.widget is not None:\n            self.selected_rows = list(self.scene.widget.selectedRows())\n        else:\n            self.selected_rows = []\n        self.commit()\n\n    def commit(self):\n        data = None\n        indices = None\n        if self.merge_kmeans:\n            merge_indices = self.merge_indices\n        else:\n            merge_indices = None\n\n        if self.input_data is not None and self.selected_rows:\n            indices = self.selected_rows\n            if merge_indices is not None:\n                # expand merged indices\n                indices = np.hstack([merge_indices[i] for i in indices])\n\n            data = self.input_data[indices]\n\n        summary = len(data) if data else self.info.NoOutput\n        details = format_summary_details(data) if data else \"\"\n        self.info.set_output_summary(summary, details)\n        self.Outputs.selected_data.send(data)\n        self.Outputs.annotated_data.send(create_annotated_table(self.input_data, indices))\n\n    def onDeleteWidget(self):\n        self.clear()\n        super().onDeleteWidget()\n\n    def send_report(self):\n        self.report_items((\n            (\"Columns:\", \"Clustering\" if self.col_clustering else \"No sorting\"),\n            (\"Rows:\", \"Clustering\" if self.row_clustering else \"No sorting\"),\n            (\"Split:\",\n             self.split_by_var is not None and self.split_by_var.name),\n            (\"Row annotation\",\n             self.annotation_var is not None and self.annotation_var.name),\n        ))\n        self.report_plot()\n\n    @classmethod\n    def migrate_settings(cls, settings, version):\n        if version is not None and version < 3:\n            def st2cl(state: bool) -> Clustering:\n                return Clustering.OrderedClustering if state else \\\n                    Clustering.None_\n\n            rc = settings.pop(\"row_clustering\", False)\n            cc = settings.pop(\"col_clustering\", False)\n            settings[\"row_clustering_method\"] = st2cl(rc).name\n            settings[\"col_clustering_method\"] = st2cl(cc).name\n\n\n# If StickyGraphicsView ever defines qt signals/slots/properties this will\n# break\nclass GraphicsView(GraphicsWidgetView, StickyGraphicsView):\n    pass\n\n\nclass RowPart(NamedTuple):\n    \"\"\"\n    A row group\n\n    Attributes\n    ----------\n    title: str\n        Group title\n    indices : (N, ) Sequence[int]\n        Indices in the input data to retrieve the row subset for the group.\n    cluster : hierarchical.Tree optional\n    cluster_ordered : hierarchical.Tree optional\n    \"\"\"\n    title: str\n    indices: Sequence[int]\n    cluster: Optional[hierarchical.Tree] = None\n    cluster_ordered: Optional[hierarchical.Tree] = None\n\n    @property\n    def can_cluster(self) -> bool:\n        if isinstance(self.indices, slice):\n            return (self.indices.stop - self.indices.start) > 1\n        else:\n            return len(self.indices) > 1\n\n\nclass ColumnPart(NamedTuple):\n    \"\"\"\n    A column group\n\n    Attributes\n    ----------\n    title : str\n        Column group title\n    indices : (N, ) int ndarray\n        Indexes the input data to retrieve the column subset for the group.\n    domain : List[Variable]\n        List of variables in the group.\n    cluster : hierarchical.Tree optional\n    cluster_ordered : hierarchical.Tree optional\n    \"\"\"\n    title: str\n    indices: Sequence[int]\n    domain: Sequence[int]\n    cluster: Optional[hierarchical.Tree] = None\n    cluster_ordered: Optional[hierarchical.Tree] = None\n\n\nclass Parts(NamedTuple):\n    rows: Sequence[RowPart]\n    columns: Sequence[ColumnPart]\n    span: Tuple[float, float]\n\n\ndef join_elided(sep, maxlen, values, elidetemplate=\"...\"):\n    def generate(sep, ellidetemplate, values):\n        count = len(values)\n        length = 0\n        parts = []\n        for i, val in enumerate(values):\n            elide = ellidetemplate.format(count - i) if count - i > 1 else \"\"\n            parts.append(val)\n            length += len(val) + (len(sep) if parts else 0)\n            yield i, islice(parts, i + 1), length, elide\n\n    best = None\n    for _, parts, length, elide in generate(sep, elidetemplate, values):\n        if length > maxlen:\n            if best is None:\n                best = sep.join(parts) + elide\n            return best\n        fulllen = length + len(elide)\n        if fulllen < maxlen or best is None:\n            best = sep.join(parts) + elide\n    return best\n\n\ndef column_str_from_table(\n        table: Orange.data.Table,\n        column: Union[int, Orange.data.Variable],\n) -> np.ndarray:\n    var = table.domain[column]\n    data, _ = table.get_column_view(column)\n    return np.asarray([var.str_val(v) for v in data], dtype=object)\n\n\ndef column_data_from_table(\n        table: Orange.data.Table,\n        column: Union[int, Orange.data.Variable],\n) -> np.ndarray:\n    var = table.domain[column]\n    data, _ = table.get_column_view(column)\n    if var.is_primitive() and data.dtype.kind != \"f\":\n        data = data.astype(float)\n    return data\n\n\ndef aggregate(\n        var: Variable, data: np.ndarray, groupindices: Sequence[Sequence[int]],\n) -> np.ndarray:\n    if var.is_string:\n        join = lambda values: (join_elided(\", \", 42, values, \" ({} more)\"))\n        # collect all original labels for every merged row\n        values = [data[indices] for indices in groupindices]\n        data = [join(list(map(var.str_val, vals))) for vals in values]\n        return np.array(data, dtype=object)\n    elif var.is_continuous:\n        data = [np.nanmean(data[indices]) if len(indices) else np.nan\n                for indices in groupindices]\n        return np.array(data, dtype=float)\n    elif var.is_discrete:\n        from Orange.statistics.util import nanmode\n        data = [nanmode(data[indices])[0] if len(indices) else np.nan\n                for indices in groupindices]\n        return np.asarray(data, dtype=float)\n    else:\n        raise TypeError(type(var))\n\n\ndef agg_join_str(var, data, groupindices, maxlen=50, elidetemplate=\" ({} more)\"):\n    join_s = lambda values: (\n        join_elided(\", \", maxlen, values, elidetemplate=elidetemplate)\n    )\n    join = lambda values: join_s(map(var.str_val, values))\n    return aggregate_apply(join, data, groupindices)\n\n\n_T = TypeVar(\"_T\")\n\n\ndef aggregate_apply(\n        f: Callable[[Sequence], _T],\n        data: np.ndarray,\n        groupindices: Sequence[Sequence[int]]\n) -> Sequence[_T]:\n    return [f(data[indices]) for indices in groupindices]\n\n\nif __name__ == \"__main__\":  # pragma: no cover\n    WidgetPreview(OWHeatMap).run(Table(\"brown-selected.tab\"))\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:** Missing Split Variable Values Not Displayed as Separate "N/A" Group in HeatMap Widget

**Description:**
When using the HeatMap widget with a split variable that contains missing values (`NaN`), the widget fails to display these instances as a separate "N/A" group. Instead, instances with undefined split variable values are excluded from the visualization, leading to incomplete data representation.

**Example:**
Suppose you have a dataset where the split variable has some missing values:

```python
data = self.brown_selected[::3].copy()
data.Y[::5] = np.nan  # Introduce missing values
self.send_signal(self.widget.Inputs.data, data, widget=w)
```

In this scenario, the HeatMap should create an additional group labeled "N/A" to represent the instances with missing split variable values. However, currently, the widget only displays groups for the defined split variable values and omits the "N/A" group.

**Expected Behavior:**
The HeatMap widget should include an "N/A" group that aggregates all instances with undefined split variable values, ensuring that all data is represented accurately in the visualization.

**Actual Behavior:**
Instances with missing split variable values are not displayed in the HeatMap, and no "N/A" group is present. This results in an incomplete visualization where some data points are excluded without indication.

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

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

--- BEGIN FILE ---
```
### Orange/widgets/visualize/owheatmap.py
import enum
from itertools import islice
from typing import (
    Iterable, Mapping, Any, TypeVar, Type, NamedTuple, Sequence, Optional,
    Union, Tuple, List, Callable
)

import numpy as np
import scipy.sparse as sp

from AnyQt.QtWidgets import (
    QGraphicsScene, QGraphicsView, QFormLayout, QComboBox, QGroupBox,
    QMenu, QAction, QSizePolicy
)
from AnyQt.QtGui import QStandardItemModel, QStandardItem, QFont, QKeySequence
from AnyQt.QtCore import Qt, QSize, QRectF, QObject

from orangewidget.utils.combobox import ComboBox, ComboBoxSearch
from Orange.data import Domain, Table, Variable, DiscreteVariable
from Orange.data.sql.table import SqlTable
import Orange.distance

from Orange.clustering import hierarchical, kmeans
from Orange.widgets.utils import colorpalettes, apply_all
from Orange.widgets.utils.itemmodels import DomainModel
from Orange.widgets.utils.stickygraphicsview import StickyGraphicsView
from Orange.widgets.utils.graphicsview import GraphicsWidgetView
from Orange.widgets.utils.colorpalettes import DiscretePalette, Palette

from Orange.widgets.utils.annotated_data import (create_annotated_table,
                                                 ANNOTATED_DATA_SIGNAL_NAME)
from Orange.widgets import widget, gui, settings
from Orange.widgets.widget import Msg, Input, Output

from Orange.widgets.data.oweditdomain import table_column_data
from Orange.widgets.visualize.utils.heatmap import HeatmapGridWidget, \
    ColorMap, CategoricalColorMap, GradientColorMap
from Orange.widgets.utils.widgetpreview import WidgetPreview
from Orange.widgets.utils.state_summary import format_summary_details


__all__ = []


def kmeans_compress(X, k=50):
    km = kmeans.KMeans(n_clusters=k, n_init=5, random_state=42)
    return km.get_model(X)


def cbselect(cb: QComboBox, value, role: Qt.ItemDataRole = Qt.EditRole) -> None:
    """
    Find and select the `value` in the `cb` QComboBox.

    Parameters
    ----------
    cb: QComboBox
    value: Any
    role: Qt.ItemDataRole
        The data role in the combo box model to match value against
    """
    cb.setCurrentIndex(cb.findData(value, role))


class Clustering(enum.IntEnum):
    #: No clustering
    None_ = 0
    #: Hierarchical clustering
    Clustering = 1
    #: Hierarchical clustering with optimal leaf ordering
    OrderedClustering = 2


ClusteringRole = Qt.UserRole + 13
#: Item data for clustering method selection models
ClusteringModelData = [
    {
        Qt.DisplayRole: "None",
        Qt.ToolTipRole: "No clustering",
        ClusteringRole: Clustering.None_,
    }, {
        Qt.DisplayRole: "Clustering",
        Qt.ToolTipRole: "Apply hierarchical clustering",
        ClusteringRole: Clustering.Clustering,
    }, {
        Qt.DisplayRole: "Clustering (opt. ordering)",
        Qt.ToolTipRole: "Apply hierarchical clustering with optimal leaf "
                        "ordering.",
        ClusteringRole: Clustering.OrderedClustering,
    }
]

ColumnLabelsPosData = [
    {Qt.DisplayRole: name, Qt.UserRole: value}
    for name, value in [
        ("None", HeatmapGridWidget.NoPosition),
        ("Top", HeatmapGridWidget.PositionTop),
        ("Bottom", HeatmapGridWidget.PositionBottom),
        ("Top and Bottom", (HeatmapGridWidget.PositionTop |
                            HeatmapGridWidget.PositionBottom)),
    ]
]


def create_list_model(
        items: Iterable[Mapping[Qt.ItemDataRole, Any]],
        parent: Optional[QObject] = None,
) -> QStandardItemModel:
    """Create list model from an item date iterable."""
    model = QStandardItemModel(parent)
    for item in items:
        sitem = QStandardItem()
        for role, value in item.items():
            sitem.setData(value, role)
        model.appendRow([sitem])
    return model


E = TypeVar("E", bound=enum.Enum)  # pylint: disable=invalid-name


def enum_get(etype: Type[E], name: str, default: E) -> E:
    """
    Return an Enum member by `name`. If no such member exists in `etype`
    return `default`.
    """
    try:
        return etype[name]
    except LookupError:
        return default


class OWHeatMap(widget.OWWidget):
    name = "Heat Map"
    description = "Plot a data matrix heatmap."
    icon = "icons/Heatmap.svg"
    priority = 260
    keywords = []

    class Inputs:
        data = Input("Data", Table)

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

    settings_version = 3

    settingsHandler = settings.DomainContextHandler()

    # Disable clustering for inputs bigger than this
    MaxClustering = 25000
    # Disable cluster leaf ordering for inputs bigger than this
    MaxOrderedClustering = 1000

    threshold_low = settings.Setting(0.0)
    threshold_high = settings.Setting(1.0)

    merge_kmeans = settings.Setting(False)
    merge_kmeans_k = settings.Setting(50)

    # Display column with averages
    averages: bool = settings.Setting(True)
    # Display legend
    legend: bool = settings.Setting(True)
    # Annotations
    #: text row annotation (row names)
    annotation_var = settings.ContextSetting(None)
    #: color row annotation
    annotation_color_var = settings.ContextSetting(None)
    # Discrete variable used to split that data/heatmaps (vertically)
    split_by_var = settings.ContextSetting(None)
    # Selected row/column clustering method (name)
    col_clustering_method: str = settings.Setting(Clustering.None_.name)
    row_clustering_method: str = settings.Setting(Clustering.None_.name)

    palette_name = settings.Setting(colorpalettes.DefaultContinuousPaletteName)
    column_label_pos: int = settings.Setting(1)
    selected_rows: List[int] = settings.Setting(None, schema_only=True)

    auto_commit = settings.Setting(True)

    graph_name = "scene"

    left_side_scrolling = True

    class Information(widget.OWWidget.Information):
        sampled = Msg("Data has been sampled")
        discrete_ignored = Msg("{} categorical feature{} ignored")
        row_clust = Msg("{}")
        col_clust = Msg("{}")
        sparse_densified = Msg("Showing this data may require a lot of memory")

    class Error(widget.OWWidget.Error):
        no_continuous = Msg("No numeric features")
        not_enough_features = Msg("Not enough features for column clustering")
        not_enough_instances = Msg("Not enough instances for clustering")
        not_enough_instances_k_means = Msg(
            "Not enough instances for k-means merging")
        not_enough_memory = Msg("Not enough memory to show this data")

    class Warning(widget.OWWidget.Warning):
        empty_clusters = Msg("Empty clusters were removed")
        missing_split_values = Msg(
            "Instances with undefined split variable values are not shown")

    def __init__(self):
        super().__init__()
        self.__pending_selection = self.selected_rows

        # A kingdom for a save_state/restore_state
        self.col_clustering = enum_get(
            Clustering, self.col_clustering_method, Clustering.None_)
        self.row_clustering = enum_get(
            Clustering, self.row_clustering_method, Clustering.None_)

        @self.settingsAboutToBePacked.connect
        def _():
            self.col_clustering_method = self.col_clustering.name
            self.row_clustering_method = self.row_clustering.name

        self.keep_aspect = False

        #: The original data with all features (retained to
        #: preserve the domain on the output)
        self.input_data = None
        #: The effective data striped of discrete features, and often
        #: merged using k-means
        self.data = None
        self.effective_data = None
        #: kmeans model used to merge rows of input_data
        self.kmeans_model = None
        #: merge indices derived from kmeans
        #: a list (len==k) of int ndarray where the i-th item contains
        #: the indices which merge the input_data into the heatmap row i
        self.merge_indices = None
        self.parts: Optional[Parts] = None
        self.__rows_cache = {}
        self.__columns_cache = {}

        self.info.set_input_summary(self.info.NoInput)
        self.info.set_output_summary(self.info.NoOutput)

        # GUI definition
        colorbox = gui.vBox(self.controlArea, "Color")
        self.color_cb = gui.palette_combo_box(self.palette_name)
        self.color_cb.currentIndexChanged.connect(self.update_color_schema)
        colorbox.layout().addWidget(self.color_cb)

        form = QFormLayout(
            formAlignment=Qt.AlignLeft,
            labelAlignment=Qt.AlignLeft,
            fieldGrowthPolicy=QFormLayout.AllNonFixedFieldsGrow
        )
        lowslider = gui.hSlider(
            colorbox, self, "threshold_low", minValue=0.0, maxValue=1.0,
            step=0.05, ticks=True, intOnly=False,
            createLabel=False, callback=self.update_lowslider)
        highslider = gui.hSlider(
            colorbox, self, "threshold_high", minValue=0.0, maxValue=1.0,
            step=0.05, ticks=True, intOnly=False,
            createLabel=False, callback=self.update_highslider)

        form.addRow("Low:", lowslider)
        form.addRow("High:", highslider)

        colorbox.layout().addLayout(form)

        mergebox = gui.vBox(self.controlArea, "Merge",)
        gui.checkBox(mergebox, self, "merge_kmeans", "Merge by k-means",
                     callback=self.__update_row_clustering)
        ibox = gui.indentedBox(mergebox)
        gui.spin(ibox, self, "merge_kmeans_k", minv=5, maxv=500,
                 label="Clusters:", keyboardTracking=False,
                 callbackOnReturn=True, callback=self.update_merge)

        cluster_box = gui.vBox(self.controlArea, "Clustering")
        # Row clustering
        self.row_cluster_cb = cb = ComboBox()
        cb.setModel(create_list_model(ClusteringModelData, self))
        cbselect(cb, self.row_clustering, ClusteringRole)
        self.connect_control(
            "row_clustering",
            lambda value, cb=cb: cbselect(cb, value, ClusteringRole)
        )
        @cb.activated.connect
        def _(idx, cb=cb):
            self.set_row_clustering(cb.itemData(idx, ClusteringRole))

        # Column clustering
        self.col_cluster_cb = cb = ComboBox()
        cb.setModel(create_list_model(ClusteringModelData, self))
        cbselect(cb, self.col_clustering, ClusteringRole)
        self.connect_control(
            "col_clustering",
            lambda value, cb=cb: cbselect(cb, value, ClusteringRole)
        )
        @cb.activated.connect
        def _(idx, cb=cb):
            self.set_col_clustering(cb.itemData(idx, ClusteringRole))

        form = QFormLayout(
            labelAlignment=Qt.AlignLeft, formAlignment=Qt.AlignLeft,
            fieldGrowthPolicy=QFormLayout.AllNonFixedFieldsGrow,
        )
        form.addRow("Rows:", self.row_cluster_cb)
        form.addRow("Columns:", self.col_cluster_cb)
        cluster_box.layout().addLayout(form)
        box = gui.vBox(self.controlArea, "Split By")

        self.row_split_model = DomainModel(
            placeholder="(None)",
            valid_types=(Orange.data.DiscreteVariable,),
            parent=self,
        )
        self.row_split_cb = cb = ComboBoxSearch(
            enabled=not self.merge_kmeans,
            sizeAdjustPolicy=ComboBox.AdjustToMinimumContentsLengthWithIcon,
            minimumContentsLength=14,
            toolTip="Split the heatmap vertically by a categorical column"
        )
        self.row_split_cb.setModel(self.row_split_model)
        self.connect_control(
            "split_by_var", lambda value, cb=cb: cbselect(cb, value)
        )
        self.connect_control(
            "merge_kmeans", self.row_split_cb.setDisabled
        )
        self.split_by_var = None

        self.row_split_cb.activated.connect(
            self.__on_split_rows_activated
        )
        box.layout().addWidget(self.row_split_cb)

        box = gui.vBox(self.controlArea, 'Annotation && Legends')

        gui.checkBox(box, self, 'legend', 'Show legend',
                     callback=self.update_legend)

        gui.checkBox(box, self, 'averages', 'Stripes with averages',
                     callback=self.update_averages_stripe)
        annotbox = QGroupBox("Row Annotations", flat=True)
        form = QFormLayout(
            annotbox,
            formAlignment=Qt.AlignLeft,
            labelAlignment=Qt.AlignLeft,
            fieldGrowthPolicy=QFormLayout.AllNonFixedFieldsGrow
        )
        self.annotation_model = DomainModel(placeholder="(None)")
        self.annotation_text_cb = ComboBoxSearch(
            minimumContentsLength=12,
            sizeAdjustPolicy=QComboBox.AdjustToMinimumContentsLength
        )
        self.annotation_text_cb.setModel(self.annotation_model)
        self.annotation_text_cb.activated.connect(self.set_annotation_var)
        self.connect_control("annotation_var", self.annotation_var_changed)

        self.row_side_color_model = DomainModel(
            order=(DomainModel.CLASSES, DomainModel.Separator,
                   DomainModel.METAS),
            placeholder="(None)", valid_types=DomainModel.PRIMITIVE,
            flags=Qt.ItemIsSelectable | Qt.ItemIsEnabled,
            parent=self,
        )
        self.row_side_color_cb = ComboBoxSearch(
            sizeAdjustPolicy=QComboBox.AdjustToMinimumContentsLength,
            minimumContentsLength=12
        )
        self.row_side_color_cb.setModel(self.row_side_color_model)
        self.row_side_color_cb.activated.connect(self.set_annotation_color_var)
        self.connect_control("annotation_color_var", self.annotation_color_var_changed)
        form.addRow("Text", self.annotation_text_cb)
        form.addRow("Color", self.row_side_color_cb)
        box.layout().addWidget(annotbox)
        posbox = gui.vBox(box, "Column Labels Position", addSpace=False)
        posbox.setFlat(True)
        cb = gui.comboBox(
            posbox, self, "column_label_pos",
            callback=self.update_column_annotations)
        cb.setModel(create_list_model(ColumnLabelsPosData, parent=self))
        cb.setCurrentIndex(self.column_label_pos)
        gui.checkBox(self.controlArea, self, "keep_aspect",
                     "Keep aspect ratio", box="Resize",
                     callback=self.__aspect_mode_changed)

        gui.rubber(self.controlArea)
        gui.auto_send(self.controlArea, self, "auto_commit")

        # Scene with heatmap
        class HeatmapScene(QGraphicsScene):
            widget: Optional[HeatmapGridWidget] = None

        self.scene = self.scene = HeatmapScene(parent=self)
        self.view = GraphicsView(
            self.scene,
            verticalScrollBarPolicy=Qt.ScrollBarAlwaysOn,
            horizontalScrollBarPolicy=Qt.ScrollBarAlwaysOn,
            viewportUpdateMode=QGraphicsView.FullViewportUpdate,
            widgetResizable=True,
        )
        self.view.setContextMenuPolicy(Qt.CustomContextMenu)
        self.view.customContextMenuRequested.connect(
            self._on_view_context_menu
        )
        self.mainArea.layout().addWidget(self.view)
        self.selected_rows = []
        self.__font_inc = QAction(
            "Increase Font", self, shortcut=QKeySequence("ctrl+>"))
        self.__font_dec = QAction(
            "Decrease Font", self, shortcut=QKeySequence("ctrl+<"))
        self.__font_inc.triggered.connect(lambda: self.__adjust_font_size(1))
        self.__font_dec.triggered.connect(lambda: self.__adjust_font_size(-1))
        if hasattr(QAction, "setShortcutVisibleInContextMenu"):
            apply_all(
                [self.__font_inc, self.__font_dec],
                lambda a: a.setShortcutVisibleInContextMenu(True)
            )
        self.addActions([self.__font_inc, self.__font_dec])

    @property
    def center_palette(self):
        palette = self.color_cb.currentData()
        return bool(palette.flags & palette.Diverging)

    @property
    def _column_label_pos(self) -> HeatmapGridWidget.Position:
        return ColumnLabelsPosData[self.column_label_pos][Qt.UserRole]

    def annotation_color_var_changed(self, value):
        cbselect(self.row_side_color_cb, value, Qt.EditRole)

    def annotation_var_changed(self, value):
        cbselect(self.annotation_text_cb, value, Qt.EditRole)

    def set_row_clustering(self, method: Clustering) -> None:
        assert isinstance(method, Clustering)
        if self.row_clustering != method:
            self.row_clustering = method
            cbselect(self.row_cluster_cb, method, ClusteringRole)
            self.__update_row_clustering()

    def set_col_clustering(self, method: Clustering) -> None:
        assert isinstance(method, Clustering)
        if self.col_clustering != method:
            self.col_clustering = method
            cbselect(self.col_cluster_cb, method, ClusteringRole)
            self.__update_column_clustering()

    def sizeHint(self) -> QSize:
        return super().sizeHint().expandedTo(QSize(900, 700))

    def color_palette(self):
        return self.color_cb.currentData().lookup_table()

    def color_map(self) -> GradientColorMap:
        return GradientColorMap(
            self.color_palette(), (self.threshold_low, self.threshold_high),
            0 if self.center_palette else None
        )

    def clear(self):
        self.data = None
        self.input_data = None
        self.effective_data = None
        self.kmeans_model = None
        self.merge_indices = None
        self.annotation_model.set_domain(None)
        self.annotation_var = None
        self.row_side_color_model.set_domain(None)
        self.annotation_color_var = None
        self.row_split_model.set_domain(None)
        self.split_by_var = None
        self.parts = None
        self.clear_scene()
        self.selected_rows = []
        self.__columns_cache.clear()
        self.__rows_cache.clear()
        self.__update_clustering_enable_state(None)

    def clear_scene(self):
        if self.scene.widget is not None:
            self.scene.widget.layoutDidActivate.disconnect(
                self.__on_layout_activate
            )
            self.scene.widget.selectionFinished.disconnect(
                self.on_selection_finished
            )
        self.scene.widget = None
        self.scene.clear()

        self.view.setSceneRect(QRectF())
        self.view.setHeaderSceneRect(QRectF())
        self.view.setFooterSceneRect(QRectF())

    @Inputs.data
    def set_dataset(self, data=None):
        """Set the input dataset to display."""
        self.closeContext()
        self.clear()
        self.clear_messages()
        self._set_input_summary(data)

        if isinstance(data, SqlTable):
            if data.approx_len() < 4000:
                data = Table(data)
            else:
                self.Information.sampled()
                data_sample = data.sample_time(1, no_cache=True)
                data_sample.download_data(2000, partial=True)
                data = Table(data_sample)

        if data is not None and not len(data):
            data = None

        if data is not None and sp.issparse(data.X):
            try:
                data = data.to_dense()
            except MemoryError:
                data = None
                self.Error.not_enough_memory()
            else:
                self.Information.sparse_densified()

        input_data = data

        # Data contains no attributes or meta attributes only
        if data is not None and len(data.domain.attributes) == 0:
            self.Error.no_continuous()
            input_data = data = None

        # Data contains some discrete attributes which must be filtered
        if data is not None and \
                any(var.is_discrete for var in data.domain.attributes):
            ndisc = sum(var.is_discrete for var in data.domain.attributes)
            data = data.transform(
                Domain([var for var in data.domain.attributes
                        if var.is_continuous],
                       data.domain.class_vars,
                       data.domain.metas))
            if not data.domain.attributes:
                self.Error.no_continuous()
                input_data = data = None
            else:
                self.Information.discrete_ignored(
                    ndisc, "s" if ndisc > 1 else "")

        self.data = data
        self.input_data = input_data

        if data is not None:
            self.annotation_model.set_domain(self.input_data.domain)
            self.row_side_color_model.set_domain(self.input_data.domain)
            self.annotation_var = None
            self.annotation_color_var = None
            self.row_split_model.set_domain(data.domain)
            if data.domain.has_discrete_class:
                self.split_by_var = data.domain.class_var
            else:
                self.split_by_var = None
            self.openContext(self.input_data)
            if self.split_by_var not in self.row_split_model:
                self.split_by_var = None

        self.update_heatmaps()
        if data is not None and self.__pending_selection is not None:
            assert self.scene.widget is not None
            self.scene.widget.selectRows(self.__pending_selection)
            self.selected_rows = self.__pending_selection
            self.__pending_selection = None

        self.unconditional_commit()

    def _set_input_summary(self, data):
        summary = len(data) if data else self.info.NoInput
        details = format_summary_details(data) if data else ""
        self.info.set_input_summary(summary, details)

    def __on_split_rows_activated(self):
        self.set_split_variable(self.row_split_cb.currentData(Qt.EditRole))

    def set_split_variable(self, var):
        if var != self.split_by_var:
            self.split_by_var = var
            self.update_heatmaps()

    def update_heatmaps(self):
        if self.data is not None:
            self.clear_scene()
            self.clear_messages()
            if self.col_clustering != Clustering.None_ and \
                    len(self.data.domain.attributes) < 2:
                self.Error.not_enough_features()
            elif (self.col_clustering != Clustering.None_ or
                  self.row_clustering != Clustering.None_) and \
                    len(self.data) < 2:
                self.Error.not_enough_instances()
            elif self.merge_kmeans and len(self.data) < 3:
                self.Error.not_enough_instances_k_means()
            else:
                parts = self.construct_heatmaps(self.data, self.split_by_var)
                self.construct_heatmaps_scene(parts, self.effective_data)
                self.selected_rows = []
        else:
            self.clear()
        if self.parts is not None and self.effective_data is not None:
            nrows = sum(len(p.indices) for p in self.parts.rows)
            self.Warning.missing_split_values(
                shown=nrows < len(self.effective_data))
        else:
            self.Warning.missing_split_values.clear()

    def update_merge(self):
        self.kmeans_model = None
        self.merge_indices = None
        if self.data is not None and self.merge_kmeans:
            self.update_heatmaps()
            self.commit()

    def _make_parts(self, data, group_var=None):
        """
        Make initial `Parts` for data, split by group_var, group_key
        """
        if group_var is not None:
            assert group_var.is_discrete
            _col_data = table_column_data(data, group_var)
            row_indices = [np.flatnonzero(_col_data == i)
                           for i in range(len(group_var.values))]
            row_groups = [RowPart(title=name, indices=ind,
                                  cluster=None, cluster_ordered=None)
                          for name, ind in zip(group_var.values, row_indices)]
        else:
            row_groups = [RowPart(title=None, indices=range(0, len(data)),
                                  cluster=None, cluster_ordered=None)]

        col_groups = [
            ColumnPart(
                title=None, indices=range(0, len(data.domain.attributes)),
                domain=data.domain, cluster=None, cluster_ordered=None)
        ]

        minv, maxv = np.nanmin(data.X), np.nanmax(data.X)
        return Parts(row_groups, col_groups, span=(minv, maxv))

    def cluster_rows(self, data: Table, parts: 'Parts', ordered=False) -> 'Parts':
        row_groups = []
        for row in parts.rows:
            if row.cluster is not None:
                cluster = row.cluster
            else:
                cluster = None
            if row.cluster_ordered is not None:
                cluster_ord = row.cluster_ordered
            else:
                cluster_ord = None

            if row.can_cluster:
                matrix = None
                need_dist = cluster is None or (ordered and cluster_ord is None)
                if need_dist:
                    subset = data[row.indices]
                    matrix = Orange.distance.Euclidean(subset)

                if cluster is None:
                    assert len(matrix) < self.MaxClustering
                    cluster = hierarchical.dist_matrix_clustering(
                        matrix, linkage=hierarchical.WARD
                    )
                if ordered and cluster_ord is None:
                    assert len(matrix) < self.MaxOrderedClustering
                    cluster_ord = hierarchical.optimal_leaf_ordering(
                        cluster, matrix,
                    )
            row_groups.append(row._replace(cluster=cluster, cluster_ordered=cluster_ord))

        return parts._replace(rows=row_groups)

    def cluster_columns(self, data, parts, ordered=False):
        assert len(parts.columns) == 1, "columns split is no longer supported"
        assert all(var.is_continuous for var in data.domain.attributes)

        col0 = parts.columns[0]
        if col0.cluster is not None:
            cluster = col0.cluster
        else:
            cluster = None
        if col0.cluster_ordered is not None:
            cluster_ord = col0.cluster_ordered
        else:
            cluster_ord = None
        need_dist = cluster is None or (ordered and cluster_ord is None)
        matrix = None
        if need_dist:
            data = Orange.distance._preprocess(data)
            matrix = np.asarray(Orange.distance.PearsonR(data, axis=0))
            # nan values break clustering below
            matrix = np.nan_to_num(matrix)

        if cluster is None:
            assert matrix is not None
            assert len(matrix) < self.MaxClustering
            cluster = hierarchical.dist_matrix_clustering(
                matrix, linkage=hierarchical.WARD
            )
        if ordered and cluster_ord is None:
            assert len(matrix) < self.MaxOrderedClustering
            cluster_ord = hierarchical.optimal_leaf_ordering(cluster, matrix)

        col_groups = [col._replace(cluster=cluster, cluster_ordered=cluster_ord)
                      for col in parts.columns]
        return parts._replace(columns=col_groups)

    def construct_heatmaps(self, data, group_var=None) -> 'Parts':
        if self.merge_kmeans:
            if self.kmeans_model is None:
                effective_data = self.input_data.transform(
                    Orange.data.Domain(
                        [var for var in self.input_data.domain.attributes
                         if var.is_continuous],
                        self.input_data.domain.class_vars,
                        self.input_data.domain.metas))
                nclust = min(self.merge_kmeans_k, len(effective_data) - 1)
                self.kmeans_model = kmeans_compress(effective_data, k=nclust)
                effective_data.domain = self.kmeans_model.domain
                merge_indices = [np.flatnonzero(self.kmeans_model.labels == ind)
                                 for ind in range(nclust)]
                not_empty_indices = [i for i, x in enumerate(merge_indices)
                                     if len(x) > 0]
                self.merge_indices = \
                    [merge_indices[i] for i in not_empty_indices]
                if len(merge_indices) != len(self.merge_indices):
                    self.Warning.empty_clusters()
                effective_data = Orange.data.Table(
                    Orange.data.Domain(effective_data.domain.attributes),
                    self.kmeans_model.centroids[not_empty_indices]
                )
            else:
                effective_data = self.effective_data

            group_var = None
        else:
            self.kmeans_model = None
            self.merge_indices = None
            effective_data = data

        self.effective_data = effective_data

        self.__update_clustering_enable_state(effective_data)

        parts = self._make_parts(effective_data, group_var)
        # Restore/update the row/columns items descriptions from cache if
        # available
        rows_cache_key = (group_var,
                          self.merge_kmeans_k if self.merge_kmeans else None)
        if rows_cache_key in self.__rows_cache:
            parts = parts._replace(rows=self.__rows_cache[rows_cache_key].rows)

        if self.row_clustering != Clustering.None_:
            parts = self.cluster_rows(
                effective_data, parts,
                ordered=self.row_clustering == Clustering.OrderedClustering
            )
        if self.col_clustering != Clustering.None_:
            parts = self.cluster_columns(
                effective_data, parts,
                ordered=self.col_clustering == Clustering.OrderedClustering
            )

        # Cache the updated parts
        self.__rows_cache[rows_cache_key] = parts
        return parts

    def construct_heatmaps_scene(self, parts: 'Parts', data: Table) -> None:
        _T = TypeVar("_T", bound=Union[RowPart, ColumnPart])

        def select_cluster(clustering: Clustering, item: _T) -> _T:
            if clustering == Clustering.None_:
                return item._replace(cluster=None, cluster_ordered=None)
            elif clustering == Clustering.Clustering:
                return item._replace(cluster=item.cluster, cluster_ordered=None)
            elif clustering == Clustering.OrderedClustering:
                return item._replace(cluster=item.cluster_ordered, cluster_ordered=None)
            else:  # pragma: no cover
                raise TypeError()

        rows = [select_cluster(self.row_clustering, rowitem)
                for rowitem in parts.rows]
        cols = [select_cluster(self.col_clustering, colitem)
                for colitem in parts.columns]
        parts = Parts(columns=cols, rows=rows, span=parts.span)

        self.setup_scene(parts, data)

    def setup_scene(self, parts, data):
        # type: (Parts, Table) -> None
        widget = HeatmapGridWidget()
        widget.setColorMap(self.color_map())
        self.scene.addItem(widget)
        self.scene.widget = widget
        columns = [v.name for v in data.domain.attributes]
        parts = HeatmapGridWidget.Parts(
            rows=[
                HeatmapGridWidget.RowItem(r.title, r.indices, r.cluster)
                for r in parts.rows
            ],
            columns=[
                HeatmapGridWidget.ColumnItem(c.title, c.indices, c.cluster)
                for c in parts.columns
            ],
            data=data.X,
            span=parts.span,
            row_names=None,
            col_names=columns,
        )
        widget.setHeatmaps(parts)
        side = self.row_side_colors()
        if side is not None:
            widget.setRowSideColorAnnotations(side[0], side[1], name=side[2].name)
        widget.setColumnLabelsPosition(self._column_label_pos)
        widget.setAspectRatioMode(
            Qt.KeepAspectRatio if self.keep_aspect else Qt.IgnoreAspectRatio
        )
        widget.setShowAverages(self.averages)
        widget.setLegendVisible(self.legend)

        widget.layoutDidActivate.connect(self.__on_layout_activate)
        widget.selectionFinished.connect(self.on_selection_finished)

        self.update_annotations()
        self.view.setCentralWidget(widget)
        self.parts = parts

    def __update_scene_rects(self):
        widget = self.scene.widget
        if widget is None:
            return
        rect = widget.geometry()
        self.scene.setSceneRect(rect)
        self.view.setSceneRect(rect)
        self.view.setHeaderSceneRect(widget.headerGeometry())
        self.view.setFooterSceneRect(widget.footerGeometry())

    def __on_layout_activate(self):
        self.__update_scene_rects()

    def __aspect_mode_changed(self):
        widget = self.scene.widget
        if widget is None:
            return
        widget.setAspectRatioMode(
            Qt.KeepAspectRatio if self.keep_aspect else Qt.IgnoreAspectRatio
        )
        # when aspect fixed the vertical sh is fixex, when not, it can
        # shrink vertically
        sp = widget.sizePolicy()
        if self.keep_aspect:
            sp.setVerticalPolicy(QSizePolicy.Fixed)
        else:
            sp.setVerticalPolicy(QSizePolicy.Preferred)
        widget.setSizePolicy(sp)

    def __update_clustering_enable_state(self, data):
        if data is not None:
            N = len(data)
            M = len(data.domain.attributes)
        else:
            N = M = 0

        rc_enabled = N <= self.MaxClustering
        rco_enabled = N <= self.MaxOrderedClustering
        cc_enabled = M <= self.MaxClustering
        cco_enabled = M <= self.MaxOrderedClustering
        row_clust, col_clust = self.row_clustering, self.col_clustering

        row_clust_msg = ""
        col_clust_msg = ""

        if not rco_enabled and row_clust == Clustering.OrderedClustering:
            row_clust = Clustering.Clustering
            row_clust_msg = "Row cluster ordering was disabled due to the " \
                            "input matrix being to big"
        if not rc_enabled and row_clust == Clustering.Clustering:
            row_clust = Clustering.None_
            row_clust_msg = "Row clustering was was disabled due to the " \
                            "input matrix being to big"

        if not cco_enabled and col_clust == Clustering.OrderedClustering:
            col_clust = Clustering.Clustering
            col_clust_msg = "Column cluster ordering was disabled due to " \
                            "the input matrix being to big"
        if not cc_enabled and col_clust == Clustering.Clustering:
            col_clust = Clustering.None_
            col_clust_msg = "Column clustering was disabled due to the " \
                            "input matrix being to big"

        self.col_clustering = col_clust
        self.row_clustering = row_clust

        self.Information.row_clust(row_clust_msg, shown=bool(row_clust_msg))
        self.Information.col_clust(col_clust_msg, shown=bool(col_clust_msg))

        # Disable/enable the combobox items for the clustering methods
        def setenabled(cb: QComboBox, clu: bool, clu_op: bool):
            model = cb.model()
            assert isinstance(model, QStandardItemModel)
            idx = cb.findData(Clustering.OrderedClustering, ClusteringRole)
            assert idx != -1
            model.item(idx).setEnabled(clu_op)
            idx = cb.findData(Clustering.Clustering, ClusteringRole)
            assert idx != -1
            model.item(idx).setEnabled(clu)

        setenabled(self.row_cluster_cb, rc_enabled, rco_enabled)
        setenabled(self.col_cluster_cb, cc_enabled, cco_enabled)

    def update_averages_stripe(self):
        """Update the visibility of the averages stripe.
        """
        widget = self.scene.widget
        if widget is not None:
            widget.setShowAverages(self.averages)

    def update_lowslider(self):
        low, high = self.controls.threshold_low, self.controls.threshold_high
        if low.value() >= high.value():
            low.setSliderPosition(high.value() - 1)
        self.update_color_schema()

    def update_highslider(self):
        low, high = self.controls.threshold_low, self.controls.threshold_high
        if low.value() >= high.value():
            high.setSliderPosition(low.value() + 1)
        self.update_color_schema()

    def update_color_schema(self):
        self.palette_name = self.color_cb.currentData().name
        w = self.scene.widget
        if w is not None:
            w.setColorMap(self.color_map())

    def __update_column_clustering(self):
        self.update_heatmaps()
        self.commit()

    def __update_row_clustering(self):
        self.update_heatmaps()
        self.commit()

    def update_legend(self):
        widget = self.scene.widget
        if widget is not None:
            widget.setLegendVisible(self.legend)

    def row_annotation_var(self):
        return self.annotation_var

    def row_annotation_data(self):
        var = self.row_annotation_var()
        if var is None:
            return None
        return column_str_from_table(self.input_data, var)

    def _merge_row_indices(self):
        if self.merge_kmeans and self.kmeans_model is not None:
            return self.merge_indices
        else:
            return None

    def set_annotation_var(self, var: Union[None, Variable, int]):
        if isinstance(var, int):
            var = self.annotation_model[var]
        if self.annotation_var != var:
            self.annotation_var = var
            self.update_annotations()

    def update_annotations(self):
        widget = self.scene.widget
        if widget is not None:
            annot_col = self.row_annotation_data()
            merge_indices = self._merge_row_indices()
            if merge_indices is not None and annot_col is not None:
                join = lambda _1: join_elided(", ", 42, _1, " ({} more)")
                annot_col = aggregate_apply(join, annot_col, merge_indices)
            if annot_col is not None:
                widget.setRowLabels(annot_col)
                widget.setRowLabelsVisible(True)
            else:
                widget.setRowLabelsVisible(False)
                widget.setRowLabels(None)

    def row_side_colors(self):
        var = self.annotation_color_var
        if var is None:
            return None
        column_data = column_data_from_table(self.input_data, var)
        span = (np.nanmin(column_data), np.nanmax(column_data))
        merges = self._merge_row_indices()
        if merges is not None:
            column_data = aggregate(var, column_data, merges)
        data, colormap = self._colorize(var, column_data)
        if var.is_continuous:
            colormap.span = span
        return data, colormap, var

    def set_annotation_color_var(self, var: Union[None, Variable, int]):
        """Set the current side color annotation variable."""
        if isinstance(var, int):
            var = self.row_side_color_model[var]
        if self.annotation_color_var != var:
            self.annotation_color_var = var
            self.update_row_side_colors()

    def update_row_side_colors(self):
        widget = self.scene.widget
        if widget is None:
            return
        colors = self.row_side_colors()
        if colors is None:
            widget.setRowSideColorAnnotations(None)
        else:
            widget.setRowSideColorAnnotations(colors[0], colors[1], colors[2].name)

    def _colorize(self, var: Variable, data: np.ndarray) -> Tuple[np.ndarray, ColorMap]:
        palette = var.palette  # type: Palette
        colors = np.array(
            [[c.red(), c.green(), c.blue()] for c in palette.qcolors_w_nan],
            dtype=np.uint8,
        )
        if var.is_discrete:
            mask = np.isnan(data)
            data[mask] = -1
            data = data.astype(int)
            if mask.any():
                values = (*var.values, "N/A")
            else:
                values = var.values
                colors = colors[: -1]
            return data, CategoricalColorMap(colors, values)
        elif var.is_continuous:
            cmap = GradientColorMap(colors[:-1])
            return data, cmap
        else:
            raise TypeError

    def update_column_annotations(self):
        widget = self.scene.widget
        if self.data is not None and widget is not None:
            widget.setColumnLabelsPosition(self._column_label_pos)

    def __adjust_font_size(self, diff):
        widget = self.scene.widget
        if widget is None:
            return
        curr = widget.font().pointSizeF()
        new = curr + diff

        self.__font_dec.setEnabled(new > 1.0)
        self.__font_inc.setEnabled(new <= 32)
        if new > 1.0:
            font = QFont()
            font.setPointSizeF(new)
            widget.setFont(font)

    def _on_view_context_menu(self, pos):
        widget = self.scene.widget
        if widget is None:
            return
        assert isinstance(widget, HeatmapGridWidget)
        menu = QMenu(self.view.viewport())
        menu.setAttribute(Qt.WA_DeleteOnClose)
        menu.addActions(self.view.actions())
        menu.addSeparator()
        menu.addActions([self.__font_inc, self.__font_dec])
        menu.addSeparator()
        a = QAction("Keep aspect ratio", menu, checkable=True)
        a.setChecked(self.keep_aspect)

        def ontoggled(state):
            self.keep_aspect = state
            self.__aspect_mode_changed()
        a.toggled.connect(ontoggled)
        menu.addAction(a)
        menu.popup(self.view.viewport().mapToGlobal(pos))

    def on_selection_finished(self):
        if self.scene.widget is not None:
            self.selected_rows = list(self.scene.widget.selectedRows())
        else:
            self.selected_rows = []
        self.commit()

    def commit(self):
        data = None
        indices = None
        if self.merge_kmeans:
            merge_indices = self.merge_indices
        else:
            merge_indices = None

        if self.input_data is not None and self.selected_rows:
            indices = self.selected_rows
            if merge_indices is not None:
                # expand merged indices
                indices = np.hstack([merge_indices[i] for i in indices])

            data = self.input_data[indices]

        summary = len(data) if data else self.info.NoOutput
        details = format_summary_details(data) if data else ""
        self.info.set_output_summary(summary, details)
        self.Outputs.selected_data.send(data)
        self.Outputs.annotated_data.send(create_annotated_table(self.input_data, indices))

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

    def send_report(self):
        self.report_items((
            ("Columns:", "Clustering" if self.col_clustering else "No sorting"),
            ("Rows:", "Clustering" if self.row_clustering else "No sorting"),
            ("Split:",
             self.split_by_var is not None and self.split_by_var.name),
            ("Row annotation",
             self.annotation_var is not None and self.annotation_var.name),
        ))
        self.report_plot()

    @classmethod
    def migrate_settings(cls, settings, version):
        if version is not None and version < 3:
            def st2cl(state: bool) -> Clustering:
                return Clustering.OrderedClustering if state else \
                    Clustering.None_

            rc = settings.pop("row_clustering", False)
            cc = settings.pop("col_clustering", False)
            settings["row_clustering_method"] = st2cl(rc).name
            settings["col_clustering_method"] = st2cl(cc).name


# If StickyGraphicsView ever defines qt signals/slots/properties this will
# break
class GraphicsView(GraphicsWidgetView, StickyGraphicsView):
    pass


class RowPart(NamedTuple):
    """
    A row group

    Attributes
    ----------
    title: str
        Group title
    indices : (N, ) Sequence[int]
        Indices in the input data to retrieve the row subset for the group.
    cluster : hierarchical.Tree optional
    cluster_ordered : hierarchical.Tree optional
    """
    title: str
    indices: Sequence[int]
    cluster: Optional[hierarchical.Tree] = None
    cluster_ordered: Optional[hierarchical.Tree] = None

    @property
    def can_cluster(self) -> bool:
        if isinstance(self.indices, slice):
            return (self.indices.stop - self.indices.start) > 1
        else:
            return len(self.indices) > 1


class ColumnPart(NamedTuple):
    """
    A column group

    Attributes
    ----------
    title : str
        Column group title
    indices : (N, ) int ndarray
        Indexes the input data to retrieve the column subset for the group.
    domain : List[Variable]
        List of variables in the group.
    cluster : hierarchical.Tree optional
    cluster_ordered : hierarchical.Tree optional
    """
    title: str
    indices: Sequence[int]
    domain: Sequence[int]
    cluster: Optional[hierarchical.Tree] = None
    cluster_ordered: Optional[hierarchical.Tree] = None


class Parts(NamedTuple):
    rows: Sequence[RowPart]
    columns: Sequence[ColumnPart]
    span: Tuple[float, float]


def join_elided(sep, maxlen, values, elidetemplate="..."):
    def generate(sep, ellidetemplate, values):
        count = len(values)
        length = 0
        parts = []
        for i, val in enumerate(values):
            elide = ellidetemplate.format(count - i) if count - i > 1 else ""
            parts.append(val)
            length += len(val) + (len(sep) if parts else 0)
            yield i, islice(parts, i + 1), length, elide

    best = None
    for _, parts, length, elide in generate(sep, elidetemplate, values):
        if length > maxlen:
            if best is None:
                best = sep.join(parts) + elide
            return best
        fulllen = length + len(elide)
        if fulllen < maxlen or best is None:
            best = sep.join(parts) + elide
    return best


def column_str_from_table(
        table: Orange.data.Table,
        column: Union[int, Orange.data.Variable],
) -> np.ndarray:
    var = table.domain[column]
    data, _ = table.get_column_view(column)
    return np.asarray([var.str_val(v) for v in data], dtype=object)


def column_data_from_table(
        table: Orange.data.Table,
        column: Union[int, Orange.data.Variable],
) -> np.ndarray:
    var = table.domain[column]
    data, _ = table.get_column_view(column)
    if var.is_primitive() and data.dtype.kind != "f":
        data = data.astype(float)
    return data


def aggregate(
        var: Variable, data: np.ndarray, groupindices: Sequence[Sequence[int]],
) -> np.ndarray:
    if var.is_string:
        join = lambda values: (join_elided(", ", 42, values, " ({} more)"))
        # collect all original labels for every merged row
        values = [data[indices] for indices in groupindices]
        data = [join(list(map(var.str_val, vals))) for vals in values]
        return np.array(data, dtype=object)
    elif var.is_continuous:
        data = [np.nanmean(data[indices]) if len(indices) else np.nan
                for indices in groupindices]
        return np.array(data, dtype=float)
    elif var.is_discrete:
        from Orange.statistics.util import nanmode
        data = [nanmode(data[indices])[0] if len(indices) else np.nan
                for indices in groupindices]
        return np.asarray(data, dtype=float)
    else:
        raise TypeError(type(var))


def agg_join_str(var, data, groupindices, maxlen=50, elidetemplate=" ({} more)"):
    join_s = lambda values: (
        join_elided(", ", maxlen, values, elidetemplate=elidetemplate)
    )
    join = lambda values: join_s(map(var.str_val, values))
    return aggregate_apply(join, data, groupindices)


_T = TypeVar("_T")


def aggregate_apply(
        f: Callable[[Sequence], _T],
        data: np.ndarray,
        groupindices: Sequence[Sequence[int]]
) -> Sequence[_T]:
    return [f(data[indices]) for indices in groupindices]


if __name__ == "__main__":  # pragma: no cover
    WidgetPreview(OWHeatMap).run(Table("brown-selected.tab"))

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