{
    "file_diffs": [
        {
            "old_file_content": "import enum\nfrom collections import defaultdict\nfrom itertools import islice\nfrom typing import (\n    Iterable, Mapping, Any, TypeVar, 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    QGraphicsView, QFormLayout, QComboBox, QGroupBox, QMenu, QAction,\n    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, \\\n    ContinuousVariable\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, enum_get, itemmodels\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.graphicsscene import GraphicsScene\nfrom Orange.widgets.utils.colorpalettes import 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.colorgradientselection import ColorGradientSelection\nfrom Orange.widgets.utils.widgetpreview import WidgetPreview\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 split_domain(domain: Domain, split_label: str):\n    \"\"\"Split the domain based on values of `split_label` value.\n    \"\"\"\n    groups = defaultdict(list)\n    for var in domain.attributes:\n        val = var.attributes.get(split_label)\n        groups[val].append(var)\n    if None in groups:\n        na = groups.pop(None)\n        return [*groups.items(), (\"N/A\", na)]\n    else:\n        return list(groups.items())\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\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    color_center = settings.Setting(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    column_annotation_color_key: Optional[Tuple[str, str]] = settings.ContextSetting(None)\n\n    # Discrete variable used to split that data/heatmaps (vertically)\n    split_by_var = settings.ContextSetting(None)\n    # Split heatmap columns by 'key' (horizontal)\n    split_columns_key: Optional[Tuple[str, str]] = 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    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    UserAdviceMessages = [\n        widget.Message(\n            \"For data with a meaningful mid-point, \"\n            \"choose one of diverging palettes.\",\n            \"diverging_palette\")]\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(self._save_state_for_serialization)\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 stripped of discrete features and hidden\n        #: attributes, and often merged using k-means\n        self.data = None\n        self.effective_data = None\n        #: Source of column annotations (derived from self.data)\n        self.col_annot_data: Optional[Table] = 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        # GUI definition\n        colorbox = gui.vBox(self.controlArea, \"Color\")\n\n        self.color_map_widget = cmw = ColorGradientSelection(\n            thresholds=(self.threshold_low, self.threshold_high),\n            center=self.color_center\n        )\n        model = itemmodels.ContinuousPalettesModel(parent=self)\n        cmw.setModel(model)\n        idx = cmw.findData(self.palette_name, model.KeyRole)\n        if idx != -1:\n            cmw.setCurrentIndex(idx)\n\n        cmw.activated.connect(self.update_color_schema)\n\n        def _set_thresholds(low, high):\n            self.threshold_low, self.threshold_high = low, high\n            self.update_color_schema()\n        cmw.thresholdsChanged.connect(_set_thresholds)\n\n        def _set_centering(center):\n            self.color_center = center\n            self.update_color_schema()\n        cmw.centerChanged.connect(_set_centering)\n\n        colorbox.layout().addWidget(self.color_map_widget)\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        form = QFormLayout(\n            formAlignment=Qt.AlignLeft, labelAlignment=Qt.AlignLeft,\n            fieldGrowthPolicy=QFormLayout.AllNonFixedFieldsGrow,\n        )\n        box.layout().addLayout(form)\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        self.col_split_model = DomainModel(\n            placeholder=\"(None)\",\n            order=DomainModel.MIXED,\n            valid_types=(Orange.data.DiscreteVariable,),\n            parent=self,\n        )\n        self.col_split_cb = cb = ComboBoxSearch(\n            sizeAdjustPolicy=ComboBox.AdjustToMinimumContentsLengthWithIcon,\n            minimumContentsLength=14,\n            toolTip=\"Split the heatmap horizontally by column annotation\"\n        )\n        self.col_split_cb.setModel(self.col_split_model)\n        self.connect_control(\n            \"split_columns_var\", lambda value, cb=cb: cbselect(cb, value)\n        )\n        self.split_columns_var = None\n        self.col_split_cb.activated.connect(self.__on_split_cols_activated)\n        form.addRow(\"Rows:\", self.row_split_cb)\n        form.addRow(\"Columns:\", self.col_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        gui.separator(box)\n        annotbox = QGroupBox(\"Row Annotations\")\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.AdjustToMinimumContentsLengthWithIcon\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.AdjustToMinimumContentsLengthWithIcon,\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        annotbox = QGroupBox(\"Column annotations\")\n        form = QFormLayout(\n            annotbox,\n            formAlignment=Qt.AlignLeft,\n            labelAlignment=Qt.AlignLeft,\n            fieldGrowthPolicy=QFormLayout.AllNonFixedFieldsGrow\n        )\n        self.col_side_color_model = DomainModel(\n            placeholder=\"(None)\",\n            valid_types=(DiscreteVariable, ContinuousVariable),\n            parent=self\n        )\n        self.col_side_color_cb = cb = ComboBoxSearch(\n            sizeAdjustPolicy=QComboBox.AdjustToMinimumContentsLengthWithIcon,\n            minimumContentsLength=12\n        )\n        self.col_side_color_cb.setModel(self.col_side_color_model)\n        self.connect_control(\n            \"column_annotation_color_var\", self.column_annotation_color_var_changed,\n        )\n        self.column_annotation_color_var = None\n        self.col_side_color_cb.activated.connect(\n            self.__set_column_annotation_color_var_index)\n\n        cb = gui.comboBox(\n            None, 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        form.addRow(\"Position\", cb)\n        form.addRow(\"Color\", self.col_side_color_cb)\n        box.layout().addWidget(annotbox)\n\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\n        gui.auto_send(self.buttonsArea, self, \"auto_commit\")\n\n        # Scene with heatmap\n        class HeatmapScene(GraphicsScene):\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    def _save_state_for_serialization(self):\n        def desc(var: Optional[Variable]) -> Optional[Tuple[str, str]]:\n            if var is not None:\n                return type(var).__name__, var.name\n            else:\n                return None\n\n        self.col_clustering_method = self.col_clustering.name\n        self.row_clustering_method = self.row_clustering.name\n\n        self.column_annotation_color_key = desc(self.column_annotation_color_var)\n        self.split_columns_key = desc(self.split_columns_var)\n\n    @property\n    def center_palette(self):\n        palette = self.color_map_widget.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_map_widget.currentData().lookup_table()\n\n    def color_map(self) -> GradientColorMap:\n        return GradientColorMap(\n            self.color_palette(), (self.threshold_low, self.threshold_high),\n            self.color_map_widget.center() 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.col_side_color_model.set_domain(None)\n        self.annotation_color_var = None\n        self.column_annotation_color_var = None\n        self.row_split_model.set_domain(None)\n        self.col_split_model.set_domain(None)\n        self.split_by_var = None\n        self.split_columns_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\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 or hidden attributes which must be\n        # filtered\n        if data is not None and \\\n                any(var.is_discrete or var.attributes.get('hidden', False)\n                    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 and\n                        not var.attributes.get('hidden', False)],\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            self.col_annot_data = data.transpose(data[:0].transform(Domain(data.domain.attributes)))\n            self.col_split_model.set_domain(self.col_annot_data.domain)\n            self.col_side_color_model.set_domain(self.col_annot_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.split_columns_var = None\n            self.column_annotation_color_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            def match(desc: Tuple[str, str], source: Iterable[Variable]):\n                for v in source:\n                    if desc == (type(v).__name__, v.name):\n                        return v\n                return None\n\n            def is_variable(obj):\n                return isinstance(obj, Variable)\n\n            if self.split_columns_key is not None:\n                self.split_columns_var = match(\n                    self.split_columns_key,\n                    filter(is_variable, self.col_split_model)\n                )\n\n            if self.column_annotation_color_key is not None:\n                self.column_annotation_color_var = match(\n                    self.column_annotation_color_key,\n                    filter(is_variable, self.col_side_color_model)\n                )\n\n        self.update_heatmaps()\n        if data is not None and self.__pending_selection is not None:\n            if 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.commit.now()\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 is not self.split_by_var:\n            self.split_by_var = var\n            self.update_heatmaps()\n\n    def __on_split_cols_activated(self):\n        self.set_column_split_var(self.col_split_cb.currentData(Qt.EditRole))\n\n    def set_column_split_var(self, var: Optional[Variable]):\n        if var is not self.split_columns_var:\n            self.split_columns_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, self.split_columns_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.deferred()\n\n    def _make_parts(self, data, group_var=None, column_split_key=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        if column_split_key is not None:\n            col_groups = split_domain(data.domain, column_split_key)\n            assert len(col_groups) > 0\n            col_indices = [np.array([data.domain.index(var) for var in group])\n                           for _, group in col_groups]\n            col_groups = [ColumnPart(title=str(name), domain=d, indices=ind,\n                                     cluster=None, cluster_ordered=None)\n                          for (name, d), ind in zip(col_groups, col_indices)]\n        else:\n            col_groups = [\n                ColumnPart(\n                    title=None, indices=range(0, len(data.domain.attributes)),\n                    domain=data.domain.attributes, 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                    cluster = hierarchical.dist_matrix_clustering(\n                        matrix, linkage=hierarchical.WARD\n                    )\n                if ordered and cluster_ord is None:\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: 'Parts', ordered=False):\n        assert all(var.is_continuous for var in data.domain.attributes)\n        col_groups = []\n        for col in parts.columns:\n            if col.cluster is not None:\n                cluster = col.cluster\n            else:\n                cluster = None\n            if col.cluster_ordered is not None:\n                cluster_ord = col.cluster_ordered\n            else:\n                cluster_ord = None\n            if col.can_cluster:\n                need_dist = cluster is None or (ordered and cluster_ord is None)\n                matrix = None\n                if need_dist:\n                    subset = data.transform(Domain(col.domain))\n                    subset = Orange.distance._preprocess(subset)\n                    matrix = np.asarray(Orange.distance.PearsonR(subset, 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                    cluster = hierarchical.dist_matrix_clustering(\n                        matrix, linkage=hierarchical.WARD\n                    )\n                if ordered and cluster_ord is None:\n                    cluster_ord = hierarchical.optimal_leaf_ordering(cluster, matrix)\n\n            col_groups.append(col._replace(cluster=cluster, cluster_ordered=cluster_ord))\n        return parts._replace(columns=col_groups)\n\n    def construct_heatmaps(self, data, group_var=None, column_split_key=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        parts = self._make_parts(\n            effective_data, group_var,\n            column_split_key.name if column_split_key is not None else None)\n\n        self.__update_clustering_enable_state(parts)\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 column_split_key in self.__columns_cache:\n            parts = parts._replace(\n                columns=self.__columns_cache[column_split_key].columns)\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\n        side = self.row_side_colors()\n        if side is not None:\n            widget.setRowSideColorAnnotations(side[0], side[1], name=side[2].name)\n\n        side = self.column_side_colors()\n        if side is not None:\n            widget.setColumnSideColorAnnotations(side[0], side[1], name=side[2].name)\n\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, parts: Optional['Parts']):\n        def c_cost(sizes: Iterable[int]) -> int:\n            \"\"\"Estimated cost for clustering of `sizes`\"\"\"\n            return sum(n ** 2 for n in sizes)\n\n        def co_cost(sizes: Iterable[int]) -> int:\n            \"\"\"Estimated cost for cluster ordering of `sizes`\"\"\"\n            # ~O(N ** 3) but O(N ** 4) worst case.\n            return sum(n ** 4 for n in sizes)\n\n        if parts is not None:\n            Ns = [len(p.indices) for p in parts.rows]\n            Ms = [len(p.indices) for p in parts.columns]\n        else:\n            Ns = Ms = [0]\n\n        rc_enabled = c_cost(Ns) <= c_cost([self.MaxClustering])\n        rco_enabled = co_cost(Ns) <= co_cost([self.MaxOrderedClustering])\n        cc_enabled = c_cost(Ms) <= c_cost([self.MaxClustering])\n        cco_enabled = co_cost(Ms) <= co_cost([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                            \"estimated runtime cost\"\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                            \"estimated runtime cost\"\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                            \"estimated runtime cost\"\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                            \"estimated runtime cost\"\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_color_schema(self):\n        self.palette_name = self.color_map_widget.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.deferred()\n\n    def __update_row_clustering(self):\n        self.update_heatmaps()\n        self.commit.deferred()\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 is not 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        merges = self._merge_row_indices()\n        if merges is not None:\n            column_data = aggregate(var, column_data, merges)\n        data, colormap = colorize(var, column_data)\n        if var.is_continuous:\n            span = (np.nanmin(column_data), np.nanmax(column_data))\n            if np.any(np.isnan(span)):\n                span = 0., 1.\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 is not 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 __set_column_annotation_color_var_index(self, index: int):\n        key = self.col_side_color_cb.itemData(index, Qt.EditRole)\n        self.set_column_annotation_color_var(key)\n\n    def column_annotation_color_var_changed(self, value):\n        cbselect(self.col_side_color_cb, value, Qt.EditRole)\n\n    def set_column_annotation_color_var(self, var):\n        if self.column_annotation_color_var is not var:\n            self.column_annotation_color_var = var\n            colors = self.column_side_colors()\n            if colors is not None:\n                self.scene.widget.setColumnSideColorAnnotations(\n                    colors[0], colors[1], colors[2].name,\n                )\n            else:\n                self.scene.widget.setColumnSideColorAnnotations(None)\n\n    def column_side_colors(self):\n        var = self.column_annotation_color_var\n        if var is None:\n            return None\n        table = self.col_annot_data\n        return color_annotation_data(table, var)\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.deferred()\n\n    @gui.deferred\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        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    @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 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 color_annotation_data(\n        table: Table, var: Union[int, str, Variable]\n) -> Tuple[np.ndarray, ColorMap, Variable]:\n    var = table.domain[var]\n    column_data = column_data_from_table(table, var)\n    data, colormap = colorize(var, column_data)\n    return data, colormap, var\n\n\ndef colorize(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 = data.astype(int)\n        data[mask] = -1\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        span = np.nanmin(data), np.nanmax(data)\n        if np.any(np.isnan(span)):\n            span = 0, 1.\n        return data, GradientColorMap(colors[:-1], span=span)\n    else:\n        raise TypeError\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 collections import defaultdict\nfrom itertools import islice\nfrom typing import (\n    Iterable, Mapping, Any, TypeVar, 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    QGraphicsView, QFormLayout, QComboBox, QGroupBox, QMenu, QAction,\n    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, \\\n    ContinuousVariable\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, enum_get, itemmodels\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.graphicsscene import GraphicsScene\nfrom Orange.widgets.utils.colorpalettes import 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.colorgradientselection import ColorGradientSelection\nfrom Orange.widgets.utils.widgetpreview import WidgetPreview\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 split_domain(domain: Domain, split_label: str):\n    \"\"\"Split the domain based on values of `split_label` value.\n    \"\"\"\n    groups = defaultdict(list)\n    for var in domain.attributes:\n        val = var.attributes.get(split_label)\n        groups[val].append(var)\n    if None in groups:\n        na = groups.pop(None)\n        return [*groups.items(), (\"N/A\", na)]\n    else:\n        return list(groups.items())\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\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    color_center = settings.Setting(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    column_annotation_color_key: Optional[Tuple[str, str]] = settings.ContextSetting(None)\n\n    # Discrete variable used to split that data/heatmaps (vertically)\n    split_by_var = settings.ContextSetting(None)\n    # Split heatmap columns by 'key' (horizontal)\n    split_columns_key: Optional[Tuple[str, str]] = 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    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    UserAdviceMessages = [\n        widget.Message(\n            \"For data with a meaningful mid-point, \"\n            \"choose one of diverging palettes.\",\n            \"diverging_palette\")]\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(self._save_state_for_serialization)\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 stripped of discrete features and hidden\n        #: attributes, and often merged using k-means\n        self.data = None\n        self.effective_data = None\n        #: Source of column annotations (derived from self.data)\n        self.col_annot_data: Optional[Table] = 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        # GUI definition\n        colorbox = gui.vBox(self.controlArea, \"Color\")\n\n        self.color_map_widget = cmw = ColorGradientSelection(\n            thresholds=(self.threshold_low, self.threshold_high),\n            center=self.color_center\n        )\n        model = itemmodels.ContinuousPalettesModel(parent=self)\n        cmw.setModel(model)\n        idx = cmw.findData(self.palette_name, model.KeyRole)\n        if idx != -1:\n            cmw.setCurrentIndex(idx)\n\n        cmw.activated.connect(self.update_color_schema)\n\n        def _set_thresholds(low, high):\n            self.threshold_low, self.threshold_high = low, high\n            self.update_color_schema()\n        cmw.thresholdsChanged.connect(_set_thresholds)\n\n        def _set_centering(center):\n            self.color_center = center\n            self.update_color_schema()\n        cmw.centerChanged.connect(_set_centering)\n\n        colorbox.layout().addWidget(self.color_map_widget)\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        form = QFormLayout(\n            formAlignment=Qt.AlignLeft, labelAlignment=Qt.AlignLeft,\n            fieldGrowthPolicy=QFormLayout.AllNonFixedFieldsGrow,\n        )\n        box.layout().addLayout(form)\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        self.col_split_model = DomainModel(\n            placeholder=\"(None)\",\n            order=DomainModel.MIXED,\n            valid_types=(Orange.data.DiscreteVariable,),\n            parent=self,\n        )\n        self.col_split_cb = cb = ComboBoxSearch(\n            sizeAdjustPolicy=ComboBox.AdjustToMinimumContentsLengthWithIcon,\n            minimumContentsLength=14,\n            toolTip=\"Split the heatmap horizontally by column annotation\"\n        )\n        self.col_split_cb.setModel(self.col_split_model)\n        self.connect_control(\n            \"split_columns_var\", lambda value, cb=cb: cbselect(cb, value)\n        )\n        self.split_columns_var = None\n        self.col_split_cb.activated.connect(self.__on_split_cols_activated)\n        form.addRow(\"Rows:\", self.row_split_cb)\n        form.addRow(\"Columns:\", self.col_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        gui.separator(box)\n        annotbox = QGroupBox(\"Row Annotations\")\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.AdjustToMinimumContentsLengthWithIcon\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.AdjustToMinimumContentsLengthWithIcon,\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        annotbox = QGroupBox(\"Column annotations\")\n        form = QFormLayout(\n            annotbox,\n            formAlignment=Qt.AlignLeft,\n            labelAlignment=Qt.AlignLeft,\n            fieldGrowthPolicy=QFormLayout.AllNonFixedFieldsGrow\n        )\n        self.col_side_color_model = DomainModel(\n            placeholder=\"(None)\",\n            valid_types=(DiscreteVariable, ContinuousVariable),\n            parent=self\n        )\n        self.col_side_color_cb = cb = ComboBoxSearch(\n            sizeAdjustPolicy=QComboBox.AdjustToMinimumContentsLengthWithIcon,\n            minimumContentsLength=12\n        )\n        self.col_side_color_cb.setModel(self.col_side_color_model)\n        self.connect_control(\n            \"column_annotation_color_var\", self.column_annotation_color_var_changed,\n        )\n        self.column_annotation_color_var = None\n        self.col_side_color_cb.activated.connect(\n            self.__set_column_annotation_color_var_index)\n\n        cb = gui.comboBox(\n            None, 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        form.addRow(\"Position\", cb)\n        form.addRow(\"Color\", self.col_side_color_cb)\n        box.layout().addWidget(annotbox)\n\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\n        gui.auto_send(self.buttonsArea, self, \"auto_commit\")\n\n        # Scene with heatmap\n        class HeatmapScene(GraphicsScene):\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    def _save_state_for_serialization(self):\n        def desc(var: Optional[Variable]) -> Optional[Tuple[str, str]]:\n            if var is not None:\n                return type(var).__name__, var.name\n            else:\n                return None\n\n        self.col_clustering_method = self.col_clustering.name\n        self.row_clustering_method = self.row_clustering.name\n\n        self.column_annotation_color_key = desc(self.column_annotation_color_var)\n        self.split_columns_key = desc(self.split_columns_var)\n\n    @property\n    def center_palette(self):\n        palette = self.color_map_widget.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_map_widget.currentData().lookup_table()\n\n    def color_map(self) -> GradientColorMap:\n        return GradientColorMap(\n            self.color_palette(), (self.threshold_low, self.threshold_high),\n            self.color_map_widget.center() 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.col_side_color_model.set_domain(None)\n        self.annotation_color_var = None\n        self.column_annotation_color_var = None\n        self.row_split_model.set_domain(None)\n        self.col_split_model.set_domain(None)\n        self.split_by_var = None\n        self.split_columns_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\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 or hidden attributes which must be\n        # filtered\n        if data is not None and \\\n                any(var.is_discrete or var.attributes.get('hidden', False)\n                    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 and\n                        not var.attributes.get('hidden', False)],\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            self.col_annot_data = data.transpose(data[:0].transform(Domain(data.domain.attributes)))\n            self.col_split_model.set_domain(self.col_annot_data.domain)\n            self.col_side_color_model.set_domain(self.col_annot_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.split_columns_var = None\n            self.column_annotation_color_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            def match(desc: Tuple[str, str], source: Iterable[Variable]):\n                for v in source:\n                    if desc == (type(v).__name__, v.name):\n                        return v\n                return None\n\n            def is_variable(obj):\n                return isinstance(obj, Variable)\n\n            if self.split_columns_key is not None:\n                self.split_columns_var = match(\n                    self.split_columns_key,\n                    filter(is_variable, self.col_split_model)\n                )\n\n            if self.column_annotation_color_key is not None:\n                self.column_annotation_color_var = match(\n                    self.column_annotation_color_key,\n                    filter(is_variable, self.col_side_color_model)\n                )\n\n        self.update_heatmaps()\n        if data is not None and self.__pending_selection is not None:\n            if 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.commit.now()\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 is not self.split_by_var:\n            self.split_by_var = var\n            self.update_heatmaps()\n\n    def __on_split_cols_activated(self):\n        self.set_column_split_var(self.col_split_cb.currentData(Qt.EditRole))\n\n    def set_column_split_var(self, var: Optional[Variable]):\n        if var is not self.split_columns_var:\n            self.split_columns_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.Error.clear()\n            self.Warning.clear()\n            self.Information.row_clust.clear()\n            self.Information.col_clust.clear()\n            self.Information.sampled.clear()\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, self.split_columns_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.deferred()\n\n    def _make_parts(self, data, group_var=None, column_split_key=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        if column_split_key is not None:\n            col_groups = split_domain(data.domain, column_split_key)\n            assert len(col_groups) > 0\n            col_indices = [np.array([data.domain.index(var) for var in group])\n                           for _, group in col_groups]\n            col_groups = [ColumnPart(title=str(name), domain=d, indices=ind,\n                                     cluster=None, cluster_ordered=None)\n                          for (name, d), ind in zip(col_groups, col_indices)]\n        else:\n            col_groups = [\n                ColumnPart(\n                    title=None, indices=range(0, len(data.domain.attributes)),\n                    domain=data.domain.attributes, 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                    cluster = hierarchical.dist_matrix_clustering(\n                        matrix, linkage=hierarchical.WARD\n                    )\n                if ordered and cluster_ord is None:\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: 'Parts', ordered=False):\n        assert all(var.is_continuous for var in data.domain.attributes)\n        col_groups = []\n        for col in parts.columns:\n            if col.cluster is not None:\n                cluster = col.cluster\n            else:\n                cluster = None\n            if col.cluster_ordered is not None:\n                cluster_ord = col.cluster_ordered\n            else:\n                cluster_ord = None\n            if col.can_cluster:\n                need_dist = cluster is None or (ordered and cluster_ord is None)\n                matrix = None\n                if need_dist:\n                    subset = data.transform(Domain(col.domain))\n                    subset = Orange.distance._preprocess(subset)\n                    matrix = np.asarray(Orange.distance.PearsonR(subset, 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                    cluster = hierarchical.dist_matrix_clustering(\n                        matrix, linkage=hierarchical.WARD\n                    )\n                if ordered and cluster_ord is None:\n                    cluster_ord = hierarchical.optimal_leaf_ordering(cluster, matrix)\n\n            col_groups.append(col._replace(cluster=cluster, cluster_ordered=cluster_ord))\n        return parts._replace(columns=col_groups)\n\n    def construct_heatmaps(self, data, group_var=None, column_split_key=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        parts = self._make_parts(\n            effective_data, group_var,\n            column_split_key.name if column_split_key is not None else None)\n\n        self.__update_clustering_enable_state(parts)\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 column_split_key in self.__columns_cache:\n            parts = parts._replace(\n                columns=self.__columns_cache[column_split_key].columns)\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\n        side = self.row_side_colors()\n        if side is not None:\n            widget.setRowSideColorAnnotations(side[0], side[1], name=side[2].name)\n\n        side = self.column_side_colors()\n        if side is not None:\n            widget.setColumnSideColorAnnotations(side[0], side[1], name=side[2].name)\n\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, parts: Optional['Parts']):\n        def c_cost(sizes: Iterable[int]) -> int:\n            \"\"\"Estimated cost for clustering of `sizes`\"\"\"\n            return sum(n ** 2 for n in sizes)\n\n        def co_cost(sizes: Iterable[int]) -> int:\n            \"\"\"Estimated cost for cluster ordering of `sizes`\"\"\"\n            # ~O(N ** 3) but O(N ** 4) worst case.\n            return sum(n ** 4 for n in sizes)\n\n        if parts is not None:\n            Ns = [len(p.indices) for p in parts.rows]\n            Ms = [len(p.indices) for p in parts.columns]\n        else:\n            Ns = Ms = [0]\n\n        rc_enabled = c_cost(Ns) <= c_cost([self.MaxClustering])\n        rco_enabled = co_cost(Ns) <= co_cost([self.MaxOrderedClustering])\n        cc_enabled = c_cost(Ms) <= c_cost([self.MaxClustering])\n        cco_enabled = co_cost(Ms) <= co_cost([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                            \"estimated runtime cost\"\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                            \"estimated runtime cost\"\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                            \"estimated runtime cost\"\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                            \"estimated runtime cost\"\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_color_schema(self):\n        self.palette_name = self.color_map_widget.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.deferred()\n\n    def __update_row_clustering(self):\n        self.update_heatmaps()\n        self.commit.deferred()\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 is not 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        merges = self._merge_row_indices()\n        if merges is not None:\n            column_data = aggregate(var, column_data, merges)\n        data, colormap = colorize(var, column_data)\n        if var.is_continuous:\n            span = (np.nanmin(column_data), np.nanmax(column_data))\n            if np.any(np.isnan(span)):\n                span = 0., 1.\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 is not 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 __set_column_annotation_color_var_index(self, index: int):\n        key = self.col_side_color_cb.itemData(index, Qt.EditRole)\n        self.set_column_annotation_color_var(key)\n\n    def column_annotation_color_var_changed(self, value):\n        cbselect(self.col_side_color_cb, value, Qt.EditRole)\n\n    def set_column_annotation_color_var(self, var):\n        if self.column_annotation_color_var is not var:\n            self.column_annotation_color_var = var\n            colors = self.column_side_colors()\n            if colors is not None:\n                self.scene.widget.setColumnSideColorAnnotations(\n                    colors[0], colors[1], colors[2].name,\n                )\n            else:\n                self.scene.widget.setColumnSideColorAnnotations(None)\n\n    def column_side_colors(self):\n        var = self.column_annotation_color_var\n        if var is None:\n            return None\n        table = self.col_annot_data\n        return color_annotation_data(table, var)\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.deferred()\n\n    @gui.deferred\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        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    @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 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 color_annotation_data(\n        table: Table, var: Union[int, str, Variable]\n) -> Tuple[np.ndarray, ColorMap, Variable]:\n    var = table.domain[var]\n    column_data = column_data_from_table(table, var)\n    data, colormap = colorize(var, column_data)\n    return data, colormap, var\n\n\ndef colorize(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 = data.astype(int)\n        data[mask] = -1\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        span = np.nanmin(data), np.nanmax(data)\n        if np.any(np.isnan(span)):\n            span = 0, 1.\n        return data, GradientColorMap(colors[:-1], span=span)\n    else:\n        raise TypeError\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": "81b04bd6a",
                "new_commit_hash": "fac4bd15a",
                "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": 692,
                            "length": 7
                        },
                        "new_range": {
                            "start": 692,
                            "length": 11
                        },
                        "section": "class OWHeatMap(widget.OWWidget):"
                    },
                    "line_group": {
                        "all_lines": [
                            {
                                "content": "    def update_heatmaps(self):",
                                "type": "context"
                            },
                            {
                                "content": "        if self.data is not None:",
                                "type": "context"
                            },
                            {
                                "content": "            self.clear_scene()",
                                "type": "context"
                            },
                            {
                                "content": "            self.clear_messages()",
                                "type": "deleted"
                            },
                            {
                                "content": "            self.Error.clear()",
                                "type": "added"
                            },
                            {
                                "content": "            self.Warning.clear()",
                                "type": "added"
                            },
                            {
                                "content": "            self.Information.row_clust.clear()",
                                "type": "added"
                            },
                            {
                                "content": "            self.Information.col_clust.clear()",
                                "type": "added"
                            },
                            {
                                "content": "            self.Information.sampled.clear()",
                                "type": "added"
                            },
                            {
                                "content": "            if self.col_clustering != Clustering.None_ and \\",
                                "type": "context"
                            },
                            {
                                "content": "                    len(self.data.domain.attributes) < 2:",
                                "type": "context"
                            },
                            {
                                "content": "                self.Error.not_enough_features()",
                                "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    color_center = settings.Setting(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    column_annotation_color_key: Optional[Tuple[str, str]] = settings.ContextSetting(None)\n\n    # Discrete variable used to split that data/heatmaps (vertically)\n    split_by_var = settings.ContextSetting(None)\n    # Split heatmap columns by 'key' (horizontal)\n    split_columns_key: Optional[Tuple[str, str]] = 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    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    UserAdviceMessages = [\n        widget.Message(\n            \"For data with a meaningful mid-point, \"\n            \"choose one of diverging palettes.\",\n            \"diverging_palette\")]\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(self._save_state_for_serialization)\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 stripped of discrete features and hidden\n        #: attributes, and often merged using k-means\n        self.data = None\n        self.effective_data = None\n        #: Source of column annotations (derived from self.data)\n        self.col_annot_data: Optional[Table] = 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        # GUI definition\n        colorbox = gui.vBox(self.controlArea, \"Color\")\n\n        self.color_map_widget = cmw = ColorGradientSelection(\n            thresholds=(self.threshold_low, self.threshold_high),\n            center=self.color_center\n        )\n        model = itemmodels.ContinuousPalettesModel(parent=self)\n        cmw.setModel(model)\n        idx = cmw.findData(self.palette_name, model.KeyRole)\n        if idx != -1:\n            cmw.setCurrentIndex(idx)\n\n        cmw.activated.connect(self.update_color_schema)\n\n        def _set_thresholds(low, high):\n            self.threshold_low, self.threshold_high = low, high\n            self.update_color_schema()\n        cmw.thresholdsChanged.connect(_set_thresholds)\n\n        def _set_centering(center):\n            self.color_center = center\n            self.update_color_schema()\n        cmw.centerChanged.connect(_set_centering)\n\n        colorbox.layout().addWidget(self.color_map_widget)\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        form = QFormLayout(\n            formAlignment=Qt.AlignLeft, labelAlignment=Qt.AlignLeft,\n            fieldGrowthPolicy=QFormLayout.AllNonFixedFieldsGrow,\n        )\n        box.layout().addLayout(form)\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        self.col_split_model = DomainModel(\n            placeholder=\"(None)\",\n            order=DomainModel.MIXED,\n            valid_types=(Orange.data.DiscreteVariable,),\n            parent=self,\n        )\n        self.col_split_cb = cb = ComboBoxSearch(\n            sizeAdjustPolicy=ComboBox.AdjustToMinimumContentsLengthWithIcon,\n            minimumContentsLength=14,\n            toolTip=\"Split the heatmap horizontally by column annotation\"\n        )\n        self.col_split_cb.setModel(self.col_split_model)\n        self.connect_control(\n            \"split_columns_var\", lambda value, cb=cb: cbselect(cb, value)\n        )\n        self.split_columns_var = None\n        self.col_split_cb.activated.connect(self.__on_split_cols_activated)\n        form.addRow(\"Rows:\", self.row_split_cb)\n        form.addRow(\"Columns:\", self.col_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        gui.separator(box)\n        annotbox = QGroupBox(\"Row Annotations\")\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.AdjustToMinimumContentsLengthWithIcon\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.AdjustToMinimumContentsLengthWithIcon,\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        annotbox = QGroupBox(\"Column annotations\")\n        form = QFormLayout(\n            annotbox,\n            formAlignment=Qt.AlignLeft,\n            labelAlignment=Qt.AlignLeft,\n            fieldGrowthPolicy=QFormLayout.AllNonFixedFieldsGrow\n        )\n        self.col_side_color_model = DomainModel(\n            placeholder=\"(None)\",\n            valid_types=(DiscreteVariable, ContinuousVariable),\n            parent=self\n        )\n        self.col_side_color_cb = cb = ComboBoxSearch(\n            sizeAdjustPolicy=QComboBox.AdjustToMinimumContentsLengthWithIcon,\n            minimumContentsLength=12\n        )\n        self.col_side_color_cb.setModel(self.col_side_color_model)\n        self.connect_control(\n            \"column_annotation_color_var\", self.column_annotation_color_var_changed,\n        )\n        self.column_annotation_color_var = None\n        self.col_side_color_cb.activated.connect(\n            self.__set_column_annotation_color_var_index)\n\n        cb = gui.comboBox(\n            None, 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        form.addRow(\"Position\", cb)\n        form.addRow(\"Color\", self.col_side_color_cb)\n        box.layout().addWidget(annotbox)\n\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\n        gui.auto_send(self.buttonsArea, self, \"auto_commit\")\n\n        # Scene with heatmap\n        class HeatmapScene(GraphicsScene):\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    def _save_state_for_serialization(self):\n        def desc(var: Optional[Variable]) -> Optional[Tuple[str, str]]:\n            if var is not None:\n                return type(var).__name__, var.name\n            else:\n                return None\n\n        self.col_clustering_method = self.col_clustering.name\n        self.row_clustering_method = self.row_clustering.name\n\n        self.column_annotation_color_key = desc(self.column_annotation_color_var)\n        self.split_columns_key = desc(self.split_columns_var)\n\n    @property\n    def center_palette(self):\n        palette = self.color_map_widget.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_map_widget.currentData().lookup_table()\n\n    def color_map(self) -> GradientColorMap:\n        return GradientColorMap(\n            self.color_palette(), (self.threshold_low, self.threshold_high),\n            self.color_map_widget.center() 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.col_side_color_model.set_domain(None)\n        self.annotation_color_var = None\n        self.column_annotation_color_var = None\n        self.row_split_model.set_domain(None)\n        self.col_split_model.set_domain(None)\n        self.split_by_var = None\n        self.split_columns_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\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 or hidden attributes which must be\n        # filtered\n        if data is not None and \\\n                any(var.is_discrete or var.attributes.get('hidden', False)\n                    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 and\n                        not var.attributes.get('hidden', False)],\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            self.col_annot_data = data.transpose(data[:0].transform(Domain(data.domain.attributes)))\n            self.col_split_model.set_domain(self.col_annot_data.domain)\n            self.col_side_color_model.set_domain(self.col_annot_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.split_columns_var = None\n            self.column_annotation_color_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            def match(desc: Tuple[str, str], source: Iterable[Variable]):\n                for v in source:\n                    if desc == (type(v).__name__, v.name):\n                        return v\n                return None\n\n            def is_variable(obj):\n                return isinstance(obj, Variable)\n\n            if self.split_columns_key is not None:\n                self.split_columns_var = match(\n                    self.split_columns_key,\n                    filter(is_variable, self.col_split_model)\n                )\n\n            if self.column_annotation_color_key is not None:\n                self.column_annotation_color_var = match(\n                    self.column_annotation_color_key,\n                    filter(is_variable, self.col_side_color_model)\n                )\n\n        self.update_heatmaps()\n        if data is not None and self.__pending_selection is not None:\n            if 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.commit.now()\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 is not self.split_by_var:\n            self.split_by_var = var\n            self.update_heatmaps()\n\n    def __on_split_cols_activated(self):\n        self.set_column_split_var(self.col_split_cb.currentData(Qt.EditRole))\n\n    def set_column_split_var(self, var: Optional[Variable]):\n        if var is not self.split_columns_var:\n            self.split_columns_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, self.split_columns_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.deferred()\n\n    def _make_parts(self, data, group_var=None, column_split_key=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        if column_split_key is not None:\n            col_groups = split_domain(data.domain, column_split_key)\n            assert len(col_groups) > 0\n            col_indices = [np.array([data.domain.index(var) for var in group])\n                           for _, group in col_groups]\n            col_groups = [ColumnPart(title=str(name), domain=d, indices=ind,\n                                     cluster=None, cluster_ordered=None)\n                          for (name, d), ind in zip(col_groups, col_indices)]\n        else:\n            col_groups = [\n                ColumnPart(\n                    title=None, indices=range(0, len(data.domain.attributes)),\n                    domain=data.domain.attributes, 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                    cluster = hierarchical.dist_matrix_clustering(\n                        matrix, linkage=hierarchical.WARD\n                    )\n                if ordered and cluster_ord is None:\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: 'Parts', ordered=False):\n        assert all(var.is_continuous for var in data.domain.attributes)\n        col_groups = []\n        for col in parts.columns:\n            if col.cluster is not None:\n                cluster = col.cluster\n            else:\n                cluster = None\n            if col.cluster_ordered is not None:\n                cluster_ord = col.cluster_ordered\n            else:\n                cluster_ord = None\n            if col.can_cluster:\n                need_dist = cluster is None or (ordered and cluster_ord is None)\n                matrix = None\n                if need_dist:\n                    subset = data.transform(Domain(col.domain))\n                    subset = Orange.distance._preprocess(subset)\n                    matrix = np.asarray(Orange.distance.PearsonR(subset, 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                    cluster = hierarchical.dist_matrix_clustering(\n                        matrix, linkage=hierarchical.WARD\n                    )\n                if ordered and cluster_ord is None:\n                    cluster_ord = hierarchical.optimal_leaf_ordering(cluster, matrix)\n\n            col_groups.append(col._replace(cluster=cluster, cluster_ordered=cluster_ord))\n        return parts._replace(columns=col_groups)\n\n    def construct_heatmaps(self, data, group_var=None, column_split_key=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        parts = self._make_parts(\n            effective_data, group_var,\n            column_split_key.name if column_split_key is not None else None)\n\n        self.__update_clustering_enable_state(parts)\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 column_split_key in self.__columns_cache:\n            parts = parts._replace(\n                columns=self.__columns_cache[column_split_key].columns)\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\n        side = self.row_side_colors()\n        if side is not None:\n            widget.setRowSideColorAnnotations(side[0], side[1], name=side[2].name)\n\n        side = self.column_side_colors()\n        if side is not None:\n            widget.setColumnSideColorAnnotations(side[0], side[1], name=side[2].name)\n\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, parts: Optional['Parts']):\n        def c_cost(sizes: Iterable[int]) -> int:\n            \"\"\"Estimated cost for clustering of `sizes`\"\"\"\n            return sum(n ** 2 for n in sizes)\n\n        def co_cost(sizes: Iterable[int]) -> int:\n            \"\"\"Estimated cost for cluster ordering of `sizes`\"\"\"\n            # ~O(N ** 3) but O(N ** 4) worst case.\n            return sum(n ** 4 for n in sizes)\n\n        if parts is not None:\n            Ns = [len(p.indices) for p in parts.rows]\n            Ms = [len(p.indices) for p in parts.columns]\n        else:\n            Ns = Ms = [0]\n\n        rc_enabled = c_cost(Ns) <= c_cost([self.MaxClustering])\n        rco_enabled = co_cost(Ns) <= co_cost([self.MaxOrderedClustering])\n        cc_enabled = c_cost(Ms) <= c_cost([self.MaxClustering])\n        cco_enabled = co_cost(Ms) <= co_cost([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                            \"estimated runtime cost\"\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                            \"estimated runtime cost\"\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                            \"estimated runtime cost\"\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                            \"estimated runtime cost\"\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_color_schema(self):\n        self.palette_name = self.color_map_widget.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.deferred()\n\n    def __update_row_clustering(self):\n        self.update_heatmaps()\n        self.commit.deferred()\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 is not 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        merges = self._merge_row_indices()\n        if merges is not None:\n            column_data = aggregate(var, column_data, merges)\n        data, colormap = colorize(var, column_data)\n        if var.is_continuous:\n            span = (np.nanmin(column_data), np.nanmax(column_data))\n            if np.any(np.isnan(span)):\n                span = 0., 1.\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 is not 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 __set_column_annotation_color_var_index(self, index: int):\n        key = self.col_side_color_cb.itemData(index, Qt.EditRole)\n        self.set_column_annotation_color_var(key)\n\n    def column_annotation_color_var_changed(self, value):\n        cbselect(self.col_side_color_cb, value, Qt.EditRole)\n\n    def set_column_annotation_color_var(self, var):\n        if self.column_annotation_color_var is not var:\n            self.column_annotation_color_var = var\n            colors = self.column_side_colors()\n            if colors is not None:\n                self.scene.widget.setColumnSideColorAnnotations(\n                    colors[0], colors[1], colors[2].name,\n                )\n            else:\n                self.scene.widget.setColumnSideColorAnnotations(None)\n\n    def column_side_colors(self):\n        var = self.column_annotation_color_var\n        if var is None:\n            return None\n        table = self.col_annot_data\n        return color_annotation_data(table, var)\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.deferred()\n\n    @gui.deferred\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        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": 692,
                            "end_lineno": 710,
                            "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    color_center = settings.Setting(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    column_annotation_color_key: Optional[Tuple[str, str]] = settings.ContextSetting(None)\n\n    # Discrete variable used to split that data/heatmaps (vertically)\n    split_by_var = settings.ContextSetting(None)\n    # Split heatmap columns by 'key' (horizontal)\n    split_columns_key: Optional[Tuple[str, str]] = 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    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    UserAdviceMessages = [\n        widget.Message(\n            \"For data with a meaningful mid-point, \"\n            \"choose one of diverging palettes.\",\n            \"diverging_palette\")]\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(self._save_state_for_serialization)\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 stripped of discrete features and hidden\n        #: attributes, and often merged using k-means\n        self.data = None\n        self.effective_data = None\n        #: Source of column annotations (derived from self.data)\n        self.col_annot_data: Optional[Table] = 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        # GUI definition\n        colorbox = gui.vBox(self.controlArea, \"Color\")\n\n        self.color_map_widget = cmw = ColorGradientSelection(\n            thresholds=(self.threshold_low, self.threshold_high),\n            center=self.color_center\n        )\n        model = itemmodels.ContinuousPalettesModel(parent=self)\n        cmw.setModel(model)\n        idx = cmw.findData(self.palette_name, model.KeyRole)\n        if idx != -1:\n            cmw.setCurrentIndex(idx)\n\n        cmw.activated.connect(self.update_color_schema)\n\n        def _set_thresholds(low, high):\n            self.threshold_low, self.threshold_high = low, high\n            self.update_color_schema()\n        cmw.thresholdsChanged.connect(_set_thresholds)\n\n        def _set_centering(center):\n            self.color_center = center\n            self.update_color_schema()\n        cmw.centerChanged.connect(_set_centering)\n\n        colorbox.layout().addWidget(self.color_map_widget)\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        form = QFormLayout(\n            formAlignment=Qt.AlignLeft, labelAlignment=Qt.AlignLeft,\n            fieldGrowthPolicy=QFormLayout.AllNonFixedFieldsGrow,\n        )\n        box.layout().addLayout(form)\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        self.col_split_model = DomainModel(\n            placeholder=\"(None)\",\n            order=DomainModel.MIXED,\n            valid_types=(Orange.data.DiscreteVariable,),\n            parent=self,\n        )\n        self.col_split_cb = cb = ComboBoxSearch(\n            sizeAdjustPolicy=ComboBox.AdjustToMinimumContentsLengthWithIcon,\n            minimumContentsLength=14,\n            toolTip=\"Split the heatmap horizontally by column annotation\"\n        )\n        self.col_split_cb.setModel(self.col_split_model)\n        self.connect_control(\n            \"split_columns_var\", lambda value, cb=cb: cbselect(cb, value)\n        )\n        self.split_columns_var = None\n        self.col_split_cb.activated.connect(self.__on_split_cols_activated)\n        form.addRow(\"Rows:\", self.row_split_cb)\n        form.addRow(\"Columns:\", self.col_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        gui.separator(box)\n        annotbox = QGroupBox(\"Row Annotations\")\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.AdjustToMinimumContentsLengthWithIcon\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.AdjustToMinimumContentsLengthWithIcon,\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        annotbox = QGroupBox(\"Column annotations\")\n        form = QFormLayout(\n            annotbox,\n            formAlignment=Qt.AlignLeft,\n            labelAlignment=Qt.AlignLeft,\n            fieldGrowthPolicy=QFormLayout.AllNonFixedFieldsGrow\n        )\n        self.col_side_color_model = DomainModel(\n            placeholder=\"(None)\",\n            valid_types=(DiscreteVariable, ContinuousVariable),\n            parent=self\n        )\n        self.col_side_color_cb = cb = ComboBoxSearch(\n            sizeAdjustPolicy=QComboBox.AdjustToMinimumContentsLengthWithIcon,\n            minimumContentsLength=12\n        )\n        self.col_side_color_cb.setModel(self.col_side_color_model)\n        self.connect_control(\n            \"column_annotation_color_var\", self.column_annotation_color_var_changed,\n        )\n        self.column_annotation_color_var = None\n        self.col_side_color_cb.activated.connect(\n            self.__set_column_annotation_color_var_index)\n\n        cb = gui.comboBox(\n            None, 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        form.addRow(\"Position\", cb)\n        form.addRow(\"Color\", self.col_side_color_cb)\n        box.layout().addWidget(annotbox)\n\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\n        gui.auto_send(self.buttonsArea, self, \"auto_commit\")\n\n        # Scene with heatmap\n        class HeatmapScene(GraphicsScene):\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    def _save_state_for_serialization(self):\n        def desc(var: Optional[Variable]) -> Optional[Tuple[str, str]]:\n            if var is not None:\n                return type(var).__name__, var.name\n            else:\n                return None\n\n        self.col_clustering_method = self.col_clustering.name\n        self.row_clustering_method = self.row_clustering.name\n\n        self.column_annotation_color_key = desc(self.column_annotation_color_var)\n        self.split_columns_key = desc(self.split_columns_var)\n\n    @property\n    def center_palette(self):\n        palette = self.color_map_widget.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_map_widget.currentData().lookup_table()\n\n    def color_map(self) -> GradientColorMap:\n        return GradientColorMap(\n            self.color_palette(), (self.threshold_low, self.threshold_high),\n            self.color_map_widget.center() 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.col_side_color_model.set_domain(None)\n        self.annotation_color_var = None\n        self.column_annotation_color_var = None\n        self.row_split_model.set_domain(None)\n        self.col_split_model.set_domain(None)\n        self.split_by_var = None\n        self.split_columns_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\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 or hidden attributes which must be\n        # filtered\n        if data is not None and \\\n                any(var.is_discrete or var.attributes.get('hidden', False)\n                    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 and\n                        not var.attributes.get('hidden', False)],\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            self.col_annot_data = data.transpose(data[:0].transform(Domain(data.domain.attributes)))\n            self.col_split_model.set_domain(self.col_annot_data.domain)\n            self.col_side_color_model.set_domain(self.col_annot_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.split_columns_var = None\n            self.column_annotation_color_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            def match(desc: Tuple[str, str], source: Iterable[Variable]):\n                for v in source:\n                    if desc == (type(v).__name__, v.name):\n                        return v\n                return None\n\n            def is_variable(obj):\n                return isinstance(obj, Variable)\n\n            if self.split_columns_key is not None:\n                self.split_columns_var = match(\n                    self.split_columns_key,\n                    filter(is_variable, self.col_split_model)\n                )\n\n            if self.column_annotation_color_key is not None:\n                self.column_annotation_color_var = match(\n                    self.column_annotation_color_key,\n                    filter(is_variable, self.col_side_color_model)\n                )\n\n        self.update_heatmaps()\n        if data is not None and self.__pending_selection is not None:\n            if 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.commit.now()\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 is not self.split_by_var:\n            self.split_by_var = var\n            self.update_heatmaps()\n\n    def __on_split_cols_activated(self):\n        self.set_column_split_var(self.col_split_cb.currentData(Qt.EditRole))\n\n    def set_column_split_var(self, var: Optional[Variable]):\n        if var is not self.split_columns_var:\n            self.split_columns_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, self.split_columns_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.deferred()\n\n    def _make_parts(self, data, group_var=None, column_split_key=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        if column_split_key is not None:\n            col_groups = split_domain(data.domain, column_split_key)\n            assert len(col_groups) > 0\n            col_indices = [np.array([data.domain.index(var) for var in group])\n                           for _, group in col_groups]\n            col_groups = [ColumnPart(title=str(name), domain=d, indices=ind,\n                                     cluster=None, cluster_ordered=None)\n                          for (name, d), ind in zip(col_groups, col_indices)]\n        else:\n            col_groups = [\n                ColumnPart(\n                    title=None, indices=range(0, len(data.domain.attributes)),\n                    domain=data.domain.attributes, 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                    cluster = hierarchical.dist_matrix_clustering(\n                        matrix, linkage=hierarchical.WARD\n                    )\n                if ordered and cluster_ord is None:\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: 'Parts', ordered=False):\n        assert all(var.is_continuous for var in data.domain.attributes)\n        col_groups = []\n        for col in parts.columns:\n            if col.cluster is not None:\n                cluster = col.cluster\n            else:\n                cluster = None\n            if col.cluster_ordered is not None:\n                cluster_ord = col.cluster_ordered\n            else:\n                cluster_ord = None\n            if col.can_cluster:\n                need_dist = cluster is None or (ordered and cluster_ord is None)\n                matrix = None\n                if need_dist:\n                    subset = data.transform(Domain(col.domain))\n                    subset = Orange.distance._preprocess(subset)\n                    matrix = np.asarray(Orange.distance.PearsonR(subset, 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                    cluster = hierarchical.dist_matrix_clustering(\n                        matrix, linkage=hierarchical.WARD\n                    )\n                if ordered and cluster_ord is None:\n                    cluster_ord = hierarchical.optimal_leaf_ordering(cluster, matrix)\n\n            col_groups.append(col._replace(cluster=cluster, cluster_ordered=cluster_ord))\n        return parts._replace(columns=col_groups)\n\n    def construct_heatmaps(self, data, group_var=None, column_split_key=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        parts = self._make_parts(\n            effective_data, group_var,\n            column_split_key.name if column_split_key is not None else None)\n\n        self.__update_clustering_enable_state(parts)\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 column_split_key in self.__columns_cache:\n            parts = parts._replace(\n                columns=self.__columns_cache[column_split_key].columns)\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\n        side = self.row_side_colors()\n        if side is not None:\n            widget.setRowSideColorAnnotations(side[0], side[1], name=side[2].name)\n\n        side = self.column_side_colors()\n        if side is not None:\n            widget.setColumnSideColorAnnotations(side[0], side[1], name=side[2].name)\n\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, parts: Optional['Parts']):\n        def c_cost(sizes: Iterable[int]) -> int:\n            \"\"\"Estimated cost for clustering of `sizes`\"\"\"\n            return sum(n ** 2 for n in sizes)\n\n        def co_cost(sizes: Iterable[int]) -> int:\n            \"\"\"Estimated cost for cluster ordering of `sizes`\"\"\"\n            # ~O(N ** 3) but O(N ** 4) worst case.\n            return sum(n ** 4 for n in sizes)\n\n        if parts is not None:\n            Ns = [len(p.indices) for p in parts.rows]\n            Ms = [len(p.indices) for p in parts.columns]\n        else:\n            Ns = Ms = [0]\n\n        rc_enabled = c_cost(Ns) <= c_cost([self.MaxClustering])\n        rco_enabled = co_cost(Ns) <= co_cost([self.MaxOrderedClustering])\n        cc_enabled = c_cost(Ms) <= c_cost([self.MaxClustering])\n        cco_enabled = co_cost(Ms) <= co_cost([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                            \"estimated runtime cost\"\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                            \"estimated runtime cost\"\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                            \"estimated runtime cost\"\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                            \"estimated runtime cost\"\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_color_schema(self):\n        self.palette_name = self.color_map_widget.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.deferred()\n\n    def __update_row_clustering(self):\n        self.update_heatmaps()\n        self.commit.deferred()\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 is not 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        merges = self._merge_row_indices()\n        if merges is not None:\n            column_data = aggregate(var, column_data, merges)\n        data, colormap = colorize(var, column_data)\n        if var.is_continuous:\n            span = (np.nanmin(column_data), np.nanmax(column_data))\n            if np.any(np.isnan(span)):\n                span = 0., 1.\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 is not 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 __set_column_annotation_color_var_index(self, index: int):\n        key = self.col_side_color_cb.itemData(index, Qt.EditRole)\n        self.set_column_annotation_color_var(key)\n\n    def column_annotation_color_var_changed(self, value):\n        cbselect(self.col_side_color_cb, value, Qt.EditRole)\n\n    def set_column_annotation_color_var(self, var):\n        if self.column_annotation_color_var is not var:\n            self.column_annotation_color_var = var\n            colors = self.column_side_colors()\n            if colors is not None:\n                self.scene.widget.setColumnSideColorAnnotations(\n                    colors[0], colors[1], colors[2].name,\n                )\n            else:\n                self.scene.widget.setColumnSideColorAnnotations(None)\n\n    def column_side_colors(self):\n        var = self.column_annotation_color_var\n        if var is None:\n            return None\n        table = self.col_annot_data\n        return color_annotation_data(table, var)\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.deferred()\n\n    @gui.deferred\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        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": 135,
                                "end_lineno": 1261,
                                "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    color_center = settings.Setting(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    column_annotation_color_key: Optional[Tuple[str, str]] = settings.ContextSetting(None)\n\n    # Discrete variable used to split that data/heatmaps (vertically)\n    split_by_var = settings.ContextSetting(None)\n    # Split heatmap columns by 'key' (horizontal)\n    split_columns_key: Optional[Tuple[str, str]] = 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    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    UserAdviceMessages = [\n        widget.Message(\n            \"For data with a meaningful mid-point, \"\n            \"choose one of diverging palettes.\",\n            \"diverging_palette\")]\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(self._save_state_for_serialization)\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 stripped of discrete features and hidden\n        #: attributes, and often merged using k-means\n        self.data = None\n        self.effective_data = None\n        #: Source of column annotations (derived from self.data)\n        self.col_annot_data: Optional[Table] = 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        # GUI definition\n        colorbox = gui.vBox(self.controlArea, \"Color\")\n\n        self.color_map_widget = cmw = ColorGradientSelection(\n            thresholds=(self.threshold_low, self.threshold_high),\n            center=self.color_center\n        )\n        model = itemmodels.ContinuousPalettesModel(parent=self)\n        cmw.setModel(model)\n        idx = cmw.findData(self.palette_name, model.KeyRole)\n        if idx != -1:\n            cmw.setCurrentIndex(idx)\n\n        cmw.activated.connect(self.update_color_schema)\n\n        def _set_thresholds(low, high):\n            self.threshold_low, self.threshold_high = low, high\n            self.update_color_schema()\n        cmw.thresholdsChanged.connect(_set_thresholds)\n\n        def _set_centering(center):\n            self.color_center = center\n            self.update_color_schema()\n        cmw.centerChanged.connect(_set_centering)\n\n        colorbox.layout().addWidget(self.color_map_widget)\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        form = QFormLayout(\n            formAlignment=Qt.AlignLeft, labelAlignment=Qt.AlignLeft,\n            fieldGrowthPolicy=QFormLayout.AllNonFixedFieldsGrow,\n        )\n        box.layout().addLayout(form)\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        self.col_split_model = DomainModel(\n            placeholder=\"(None)\",\n            order=DomainModel.MIXED,\n            valid_types=(Orange.data.DiscreteVariable,),\n            parent=self,\n        )\n        self.col_split_cb = cb = ComboBoxSearch(\n            sizeAdjustPolicy=ComboBox.AdjustToMinimumContentsLengthWithIcon,\n            minimumContentsLength=14,\n            toolTip=\"Split the heatmap horizontally by column annotation\"\n        )\n        self.col_split_cb.setModel(self.col_split_model)\n        self.connect_control(\n            \"split_columns_var\", lambda value, cb=cb: cbselect(cb, value)\n        )\n        self.split_columns_var = None\n        self.col_split_cb.activated.connect(self.__on_split_cols_activated)\n        form.addRow(\"Rows:\", self.row_split_cb)\n        form.addRow(\"Columns:\", self.col_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        gui.separator(box)\n        annotbox = QGroupBox(\"Row Annotations\")\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.AdjustToMinimumContentsLengthWithIcon\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.AdjustToMinimumContentsLengthWithIcon,\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        annotbox = QGroupBox(\"Column annotations\")\n        form = QFormLayout(\n            annotbox,\n            formAlignment=Qt.AlignLeft,\n            labelAlignment=Qt.AlignLeft,\n            fieldGrowthPolicy=QFormLayout.AllNonFixedFieldsGrow\n        )\n        self.col_side_color_model = DomainModel(\n            placeholder=\"(None)\",\n            valid_types=(DiscreteVariable, ContinuousVariable),\n            parent=self\n        )\n        self.col_side_color_cb = cb = ComboBoxSearch(\n            sizeAdjustPolicy=QComboBox.AdjustToMinimumContentsLengthWithIcon,\n            minimumContentsLength=12\n        )\n        self.col_side_color_cb.setModel(self.col_side_color_model)\n        self.connect_control(\n            \"column_annotation_color_var\", self.column_annotation_color_var_changed,\n        )\n        self.column_annotation_color_var = None\n        self.col_side_color_cb.activated.connect(\n            self.__set_column_annotation_color_var_index)\n\n        cb = gui.comboBox(\n            None, 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        form.addRow(\"Position\", cb)\n        form.addRow(\"Color\", self.col_side_color_cb)\n        box.layout().addWidget(annotbox)\n\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\n        gui.auto_send(self.buttonsArea, self, \"auto_commit\")\n\n        # Scene with heatmap\n        class HeatmapScene(GraphicsScene):\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    def _save_state_for_serialization(self):\n        def desc(var: Optional[Variable]) -> Optional[Tuple[str, str]]:\n            if var is not None:\n                return type(var).__name__, var.name\n            else:\n                return None\n\n        self.col_clustering_method = self.col_clustering.name\n        self.row_clustering_method = self.row_clustering.name\n\n        self.column_annotation_color_key = desc(self.column_annotation_color_var)\n        self.split_columns_key = desc(self.split_columns_var)\n\n    @property\n    def center_palette(self):\n        palette = self.color_map_widget.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_map_widget.currentData().lookup_table()\n\n    def color_map(self) -> GradientColorMap:\n        return GradientColorMap(\n            self.color_palette(), (self.threshold_low, self.threshold_high),\n            self.color_map_widget.center() 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.col_side_color_model.set_domain(None)\n        self.annotation_color_var = None\n        self.column_annotation_color_var = None\n        self.row_split_model.set_domain(None)\n        self.col_split_model.set_domain(None)\n        self.split_by_var = None\n        self.split_columns_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\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 or hidden attributes which must be\n        # filtered\n        if data is not None and \\\n                any(var.is_discrete or var.attributes.get('hidden', False)\n                    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 and\n                        not var.attributes.get('hidden', False)],\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            self.col_annot_data = data.transpose(data[:0].transform(Domain(data.domain.attributes)))\n            self.col_split_model.set_domain(self.col_annot_data.domain)\n            self.col_side_color_model.set_domain(self.col_annot_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.split_columns_var = None\n            self.column_annotation_color_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            def match(desc: Tuple[str, str], source: Iterable[Variable]):\n                for v in source:\n                    if desc == (type(v).__name__, v.name):\n                        return v\n                return None\n\n            def is_variable(obj):\n                return isinstance(obj, Variable)\n\n            if self.split_columns_key is not None:\n                self.split_columns_var = match(\n                    self.split_columns_key,\n                    filter(is_variable, self.col_split_model)\n                )\n\n            if self.column_annotation_color_key is not None:\n                self.column_annotation_color_var = match(\n                    self.column_annotation_color_key,\n                    filter(is_variable, self.col_side_color_model)\n                )\n\n        self.update_heatmaps()\n        if data is not None and self.__pending_selection is not None:\n            if 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.commit.now()\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 is not self.split_by_var:\n            self.split_by_var = var\n            self.update_heatmaps()\n\n    def __on_split_cols_activated(self):\n        self.set_column_split_var(self.col_split_cb.currentData(Qt.EditRole))\n\n    def set_column_split_var(self, var: Optional[Variable]):\n        if var is not self.split_columns_var:\n            self.split_columns_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, self.split_columns_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.deferred()\n\n    def _make_parts(self, data, group_var=None, column_split_key=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        if column_split_key is not None:\n            col_groups = split_domain(data.domain, column_split_key)\n            assert len(col_groups) > 0\n            col_indices = [np.array([data.domain.index(var) for var in group])\n                           for _, group in col_groups]\n            col_groups = [ColumnPart(title=str(name), domain=d, indices=ind,\n                                     cluster=None, cluster_ordered=None)\n                          for (name, d), ind in zip(col_groups, col_indices)]\n        else:\n            col_groups = [\n                ColumnPart(\n                    title=None, indices=range(0, len(data.domain.attributes)),\n                    domain=data.domain.attributes, 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                    cluster = hierarchical.dist_matrix_clustering(\n                        matrix, linkage=hierarchical.WARD\n                    )\n                if ordered and cluster_ord is None:\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: 'Parts', ordered=False):\n        assert all(var.is_continuous for var in data.domain.attributes)\n        col_groups = []\n        for col in parts.columns:\n            if col.cluster is not None:\n                cluster = col.cluster\n            else:\n                cluster = None\n            if col.cluster_ordered is not None:\n                cluster_ord = col.cluster_ordered\n            else:\n                cluster_ord = None\n            if col.can_cluster:\n                need_dist = cluster is None or (ordered and cluster_ord is None)\n                matrix = None\n                if need_dist:\n                    subset = data.transform(Domain(col.domain))\n                    subset = Orange.distance._preprocess(subset)\n                    matrix = np.asarray(Orange.distance.PearsonR(subset, 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                    cluster = hierarchical.dist_matrix_clustering(\n                        matrix, linkage=hierarchical.WARD\n                    )\n                if ordered and cluster_ord is None:\n                    cluster_ord = hierarchical.optimal_leaf_ordering(cluster, matrix)\n\n            col_groups.append(col._replace(cluster=cluster, cluster_ordered=cluster_ord))\n        return parts._replace(columns=col_groups)\n\n    def construct_heatmaps(self, data, group_var=None, column_split_key=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        parts = self._make_parts(\n            effective_data, group_var,\n            column_split_key.name if column_split_key is not None else None)\n\n        self.__update_clustering_enable_state(parts)\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 column_split_key in self.__columns_cache:\n            parts = parts._replace(\n                columns=self.__columns_cache[column_split_key].columns)\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\n        side = self.row_side_colors()\n        if side is not None:\n            widget.setRowSideColorAnnotations(side[0], side[1], name=side[2].name)\n\n        side = self.column_side_colors()\n        if side is not None:\n            widget.setColumnSideColorAnnotations(side[0], side[1], name=side[2].name)\n\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, parts: Optional['Parts']):\n        def c_cost(sizes: Iterable[int]) -> int:\n            \"\"\"Estimated cost for clustering of `sizes`\"\"\"\n            return sum(n ** 2 for n in sizes)\n\n        def co_cost(sizes: Iterable[int]) -> int:\n            \"\"\"Estimated cost for cluster ordering of `sizes`\"\"\"\n            # ~O(N ** 3) but O(N ** 4) worst case.\n            return sum(n ** 4 for n in sizes)\n\n        if parts is not None:\n            Ns = [len(p.indices) for p in parts.rows]\n            Ms = [len(p.indices) for p in parts.columns]\n        else:\n            Ns = Ms = [0]\n\n        rc_enabled = c_cost(Ns) <= c_cost([self.MaxClustering])\n        rco_enabled = co_cost(Ns) <= co_cost([self.MaxOrderedClustering])\n        cc_enabled = c_cost(Ms) <= c_cost([self.MaxClustering])\n        cco_enabled = co_cost(Ms) <= co_cost([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                            \"estimated runtime cost\"\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                            \"estimated runtime cost\"\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                            \"estimated runtime cost\"\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                            \"estimated runtime cost\"\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_color_schema(self):\n        self.palette_name = self.color_map_widget.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.deferred()\n\n    def __update_row_clustering(self):\n        self.update_heatmaps()\n        self.commit.deferred()\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 is not 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        merges = self._merge_row_indices()\n        if merges is not None:\n            column_data = aggregate(var, column_data, merges)\n        data, colormap = colorize(var, column_data)\n        if var.is_continuous:\n            span = (np.nanmin(column_data), np.nanmax(column_data))\n            if np.any(np.isnan(span)):\n                span = 0., 1.\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 is not 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 __set_column_annotation_color_var_index(self, index: int):\n        key = self.col_side_color_cb.itemData(index, Qt.EditRole)\n        self.set_column_annotation_color_var(key)\n\n    def column_annotation_color_var_changed(self, value):\n        cbselect(self.col_side_color_cb, value, Qt.EditRole)\n\n    def set_column_annotation_color_var(self, var):\n        if self.column_annotation_color_var is not var:\n            self.column_annotation_color_var = var\n            colors = self.column_side_colors()\n            if colors is not None:\n                self.scene.widget.setColumnSideColorAnnotations(\n                    colors[0], colors[1], colors[2].name,\n                )\n            else:\n                self.scene.widget.setColumnSideColorAnnotations(None)\n\n    def column_side_colors(self):\n        var = self.column_annotation_color_var\n        if var is None:\n            return None\n        table = self.col_annot_data\n        return color_annotation_data(table, var)\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.deferred()\n\n    @gui.deferred\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        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": 135,
                            "end_lineno": 1261,
                            "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, Mock\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.widgets.utils import colorpalettes\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.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        self.housing = Table(\"housing\")\n        self.titanic = Table(\"titanic\")\n        self.brown_selected = Table(\"brown-selected\")\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        self.widget.MaxClustering = 20\n        self.widget.MaxOrderedClustering = 15\n        data = self.brown_selected[:, :10]\n        self.send_signal(self.widget.Inputs.data, data[:15])\n        self.assertFalse(self.widget.Information.active)\n        self.send_signal(self.widget.Inputs.data, data[:16])\n        self.assertTrue(self.widget.Information.active)\n        self.assertEqual(self.widget.row_clustering, Clustering.Clustering)\n        self.send_signal(self.widget.Inputs.data, data[:20])\n        self.assertFalse(self.widget.Information.active)\n        self.send_signal(self.widget.Inputs.data, data[:21])\n        self.assertTrue(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_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        with iris.unlocked():\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.commit, 'now') 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_saved_selection_when_not_possible(self):\n        # Has stored selection but ot enough columns for clustering.\n        iris = Table(\"iris\")[:, [\"petal width\"]]\n        w = self.create_widget(\n            OWHeatMap, stored_settings={\n                \"__version__\": 3,\n                \"col_clustering_method\": \"Clustering\",\n                \"selected_rows\": [1, 2, 3],\n            }\n        )\n        self.send_signal(w.Inputs.data, iris)\n        out = self.get_output(w.Outputs.selected_data)\n        self.assertSequenceEqual(list(out.ids), list(iris.ids[[1, 2, 3]]))\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        with data.unlocked():\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 _brown_selected_10(self):\n        data = self.brown_selected[::5]\n        data = data.transform(\n            Domain(data.domain.attributes[:10], data.domain.class_vars,\n                   data.domain.metas + (data.domain[\"diau g\"],)))\n        data.ensure_copy()\n        return data\n\n    def test_set_split_column_key(self):\n        data = self._brown_selected_10()\n        function = data.domain[\"function\"]\n        data_t = data.transpose(data)\n        w = self.widget\n        self.send_signal(self.widget.Inputs.data, data_t, widget=w)\n        w.set_column_split_var(function)\n        self.assertEqual(len(w.parts.columns), len(function.values))\n        w.set_column_split_var(None)\n        self.assertEqual(len(w.parts.columns), 1)\n\n    def test_set_split_column_key_missing(self):\n        data = self._brown_selected_10()\n        with data.unlocked():\n            data.Y[:5] = np.nan\n        data_t = data.transpose(data)\n        function = data.domain[\"function\"]\n        w = self.widget\n        self.send_signal(self.widget.Inputs.data, data_t, widget=w)\n        w.set_column_split_var(function)\n        self.assertEqual(len(w.parts.columns), len(function.values) + 1)\n        ncols = sum(len(p.indices) for p in w.parts.columns)\n        self.assertEqual(ncols, len(data_t.domain.attributes))\n        w.set_column_split_var(None)\n        self.assertEqual(len(w.parts.columns), 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_centering_threshold_change(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        cmw = self.widget.color_map_widget\n        palette_index = cmw.findData(\n            colorpalettes.ContinuousPalettes[\"diverging_bwr_40_95_c42\"],\n            Qt.UserRole)\n        cmw.setCurrentIndex(palette_index)\n\n        self.widget.update_color_schema = Mock()\n        cmw.centerChanged.emit(42)\n        self.widget.update_color_schema.assert_called()\n\n    def test_palette_center(self):\n        widget = self.widget\n        model = widget.color_map_widget.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_map_widget.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 = self.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_row_color_annotations_with_na(self):\n        widget = self.widget\n        data = self._brown_selected_10()\n        with data.unlocked():\n            data.Y[:3] = np.nan\n            data.metas[:3, -1] = np.nan\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(data.domain[\"diau g\"])\n        with data.unlocked():\n            data.Y[:] = np.nan\n            data.metas[:, -1] = np.nan\n        self.send_signal(widget.Inputs.data, data, widget=widget)\n        widget.set_annotation_color_var(data.domain[\"function\"])\n        widget.set_annotation_color_var(data.domain[\"diau g\"])\n        widget.set_annotation_color_var(None)\n        self.assertFalse(widget.scene.widget.right_side_colors[0].isVisible())\n\n    def test_col_color_annotations(self):\n        widget = self.widget\n        data = self._brown_selected_10()\n        data_t = data.transpose(data)\n        self.send_signal(widget.Inputs.data, data_t, widget=widget)\n        # discrete\n        widget.set_column_annotation_color_var(data.domain[\"function\"])\n        self.assertTrue(widget.scene.widget.top_side_colors[0].isVisible())\n        # continuous\n        widget.set_column_annotation_color_var(data.domain[\"diau g\"])\n        widget.set_column_annotation_color_var(None)\n        self.assertFalse(widget.scene.widget.top_side_colors[0].isVisible())\n\n    def test_col_color_annotations_with_na(self):\n        widget = self.widget\n        data = self._brown_selected_10()\n        with data.unlocked():\n            data.Y[:3] = np.nan\n            data.metas[:3, -1] = np.nan\n        data_t = data.transpose(data)\n        self.send_signal(widget.Inputs.data, data_t, widget=widget)\n        widget.set_column_annotation_color_var(data.domain[\"function\"])\n        self.assertTrue(widget.scene.widget.top_side_colors[0].isVisible())\n        widget.set_column_annotation_color_var(data.domain[\"diau g\"])\n        with data.unlocked():\n            data.Y[:] = np.nan\n            data.metas[:, -1] = np.nan\n        data_t = data.transpose(data)\n        self.send_signal(widget.Inputs.data, data_t, widget=widget)\n        widget.set_column_annotation_color_var(data.domain[\"function\"])\n        widget.set_column_annotation_color_var(data.domain[\"diau g\"])\n        widget.set_column_annotation_color_var(None)\n        self.assertFalse(widget.scene.widget.top_side_colors[0].isVisible())\n\n    def test_data_with_hidden(self):\n        w = self.widget\n        housing = self.housing.copy()\n        housing.domain.attributes[0].attributes[\"hidden\"] = True\n        self.send_signal(self.widget.Inputs.data, housing)\n        self.assertEqual(len(w.effective_data.domain.attributes),\n                         len(housing.domain.attributes) - 1)\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, Mock\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.widgets.utils import colorpalettes\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.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        self.housing = Table(\"housing\")\n        self.titanic = Table(\"titanic\")\n        self.brown_selected = Table(\"brown-selected\")\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        self.widget.MaxClustering = 20\n        self.widget.MaxOrderedClustering = 15\n        data = self.brown_selected[:, :10]\n        self.send_signal(self.widget.Inputs.data, data[:15])\n        self.assertFalse(self.widget.Information.active)\n        self.send_signal(self.widget.Inputs.data, data[:16])\n        self.assertTrue(self.widget.Information.active)\n        self.assertEqual(self.widget.row_clustering, Clustering.Clustering)\n        self.send_signal(self.widget.Inputs.data, data[:20])\n        self.assertFalse(self.widget.Information.active)\n        self.send_signal(self.widget.Inputs.data, data[:21])\n        self.assertTrue(self.widget.Information.active)\n        data = Table(\"heart_disease.tab\")[:10]\n        self.send_signal(self.widget.Inputs.data, data)\n        self.assertTrue(self.widget.Information.discrete_ignored.is_shown())\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_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        with iris.unlocked():\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.commit, 'now') 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_saved_selection_when_not_possible(self):\n        # Has stored selection but ot enough columns for clustering.\n        iris = Table(\"iris\")[:, [\"petal width\"]]\n        w = self.create_widget(\n            OWHeatMap, stored_settings={\n                \"__version__\": 3,\n                \"col_clustering_method\": \"Clustering\",\n                \"selected_rows\": [1, 2, 3],\n            }\n        )\n        self.send_signal(w.Inputs.data, iris)\n        out = self.get_output(w.Outputs.selected_data)\n        self.assertSequenceEqual(list(out.ids), list(iris.ids[[1, 2, 3]]))\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        with data.unlocked():\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 _brown_selected_10(self):\n        data = self.brown_selected[::5]\n        data = data.transform(\n            Domain(data.domain.attributes[:10], data.domain.class_vars,\n                   data.domain.metas + (data.domain[\"diau g\"],)))\n        data.ensure_copy()\n        return data\n\n    def test_set_split_column_key(self):\n        data = self._brown_selected_10()\n        function = data.domain[\"function\"]\n        data_t = data.transpose(data)\n        w = self.widget\n        self.send_signal(self.widget.Inputs.data, data_t, widget=w)\n        w.set_column_split_var(function)\n        self.assertEqual(len(w.parts.columns), len(function.values))\n        w.set_column_split_var(None)\n        self.assertEqual(len(w.parts.columns), 1)\n\n    def test_set_split_column_key_missing(self):\n        data = self._brown_selected_10()\n        with data.unlocked():\n            data.Y[:5] = np.nan\n        data_t = data.transpose(data)\n        function = data.domain[\"function\"]\n        w = self.widget\n        self.send_signal(self.widget.Inputs.data, data_t, widget=w)\n        w.set_column_split_var(function)\n        self.assertEqual(len(w.parts.columns), len(function.values) + 1)\n        ncols = sum(len(p.indices) for p in w.parts.columns)\n        self.assertEqual(ncols, len(data_t.domain.attributes))\n        w.set_column_split_var(None)\n        self.assertEqual(len(w.parts.columns), 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_centering_threshold_change(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        cmw = self.widget.color_map_widget\n        palette_index = cmw.findData(\n            colorpalettes.ContinuousPalettes[\"diverging_bwr_40_95_c42\"],\n            Qt.UserRole)\n        cmw.setCurrentIndex(palette_index)\n\n        self.widget.update_color_schema = Mock()\n        cmw.centerChanged.emit(42)\n        self.widget.update_color_schema.assert_called()\n\n    def test_palette_center(self):\n        widget = self.widget\n        model = widget.color_map_widget.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_map_widget.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 = self.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_row_color_annotations_with_na(self):\n        widget = self.widget\n        data = self._brown_selected_10()\n        with data.unlocked():\n            data.Y[:3] = np.nan\n            data.metas[:3, -1] = np.nan\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(data.domain[\"diau g\"])\n        with data.unlocked():\n            data.Y[:] = np.nan\n            data.metas[:, -1] = np.nan\n        self.send_signal(widget.Inputs.data, data, widget=widget)\n        widget.set_annotation_color_var(data.domain[\"function\"])\n        widget.set_annotation_color_var(data.domain[\"diau g\"])\n        widget.set_annotation_color_var(None)\n        self.assertFalse(widget.scene.widget.right_side_colors[0].isVisible())\n\n    def test_col_color_annotations(self):\n        widget = self.widget\n        data = self._brown_selected_10()\n        data_t = data.transpose(data)\n        self.send_signal(widget.Inputs.data, data_t, widget=widget)\n        # discrete\n        widget.set_column_annotation_color_var(data.domain[\"function\"])\n        self.assertTrue(widget.scene.widget.top_side_colors[0].isVisible())\n        # continuous\n        widget.set_column_annotation_color_var(data.domain[\"diau g\"])\n        widget.set_column_annotation_color_var(None)\n        self.assertFalse(widget.scene.widget.top_side_colors[0].isVisible())\n\n    def test_col_color_annotations_with_na(self):\n        widget = self.widget\n        data = self._brown_selected_10()\n        with data.unlocked():\n            data.Y[:3] = np.nan\n            data.metas[:3, -1] = np.nan\n        data_t = data.transpose(data)\n        self.send_signal(widget.Inputs.data, data_t, widget=widget)\n        widget.set_column_annotation_color_var(data.domain[\"function\"])\n        self.assertTrue(widget.scene.widget.top_side_colors[0].isVisible())\n        widget.set_column_annotation_color_var(data.domain[\"diau g\"])\n        with data.unlocked():\n            data.Y[:] = np.nan\n            data.metas[:, -1] = np.nan\n        data_t = data.transpose(data)\n        self.send_signal(widget.Inputs.data, data_t, widget=widget)\n        widget.set_column_annotation_color_var(data.domain[\"function\"])\n        widget.set_column_annotation_color_var(data.domain[\"diau g\"])\n        widget.set_column_annotation_color_var(None)\n        self.assertFalse(widget.scene.widget.top_side_colors[0].isVisible())\n\n    def test_data_with_hidden(self):\n        w = self.widget\n        housing = self.housing.copy()\n        housing.domain.attributes[0].attributes[\"hidden\"] = True\n        self.send_signal(self.widget.Inputs.data, housing)\n        self.assertEqual(len(w.effective_data.domain.attributes),\n                         len(housing.domain.attributes) - 1)\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": "53f4fd667",
                "new_commit_hash": "2f3eece77",
                "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": 72,
                            "length": 6
                        },
                        "new_range": {
                            "start": 72,
                            "length": 9
                        },
                        "section": "class TestOWHeatMap(WidgetTest, WidgetOutputsTestMixin):"
                    },
                    "line_group": {
                        "all_lines": [
                            {
                                "content": "        self.assertFalse(self.widget.Information.active)",
                                "type": "context"
                            },
                            {
                                "content": "        self.send_signal(self.widget.Inputs.data, data[:21])",
                                "type": "context"
                            },
                            {
                                "content": "        self.assertTrue(self.widget.Information.active)",
                                "type": "context"
                            },
                            {
                                "content": "        data = Table(\"heart_disease.tab\")[:10]",
                                "type": "added"
                            },
                            {
                                "content": "        self.send_signal(self.widget.Inputs.data, data)",
                                "type": "added"
                            },
                            {
                                "content": "        self.assertTrue(self.widget.Information.discrete_ignored.is_shown())",
                                "type": "added"
                            },
                            {
                                "content": "",
                                "type": "context"
                            },
                            {
                                "content": "    def test_settings_changed(self):",
                                "type": "context"
                            },
                            {
                                "content": "        self.send_signal(self.widget.Inputs.data, self.data)",
                                "type": "context"
                            },
                            {
                                "content": "",
                                "type": "context"
                            }
                        ]
                    },
                    "modified_entities": [
                        {
                            "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.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        self.housing = Table(\"housing\")\n        self.titanic = Table(\"titanic\")\n        self.brown_selected = Table(\"brown-selected\")\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        self.widget.MaxClustering = 20\n        self.widget.MaxOrderedClustering = 15\n        data = self.brown_selected[:, :10]\n        self.send_signal(self.widget.Inputs.data, data[:15])\n        self.assertFalse(self.widget.Information.active)\n        self.send_signal(self.widget.Inputs.data, data[:16])\n        self.assertTrue(self.widget.Information.active)\n        self.assertEqual(self.widget.row_clustering, Clustering.Clustering)\n        self.send_signal(self.widget.Inputs.data, data[:20])\n        self.assertFalse(self.widget.Information.active)\n        self.send_signal(self.widget.Inputs.data, data[:21])\n        self.assertTrue(self.widget.Information.active)\n        data = Table(\"heart_disease.tab\")[:10]\n        self.send_signal(self.widget.Inputs.data, data)\n        self.assertTrue(self.widget.Information.discrete_ignored.is_shown())\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_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        with iris.unlocked():\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.commit, 'now') 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_saved_selection_when_not_possible(self):\n        # Has stored selection but ot enough columns for clustering.\n        iris = Table(\"iris\")[:, [\"petal width\"]]\n        w = self.create_widget(\n            OWHeatMap, stored_settings={\n                \"__version__\": 3,\n                \"col_clustering_method\": \"Clustering\",\n                \"selected_rows\": [1, 2, 3],\n            }\n        )\n        self.send_signal(w.Inputs.data, iris)\n        out = self.get_output(w.Outputs.selected_data)\n        self.assertSequenceEqual(list(out.ids), list(iris.ids[[1, 2, 3]]))\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        with data.unlocked():\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 _brown_selected_10(self):\n        data = self.brown_selected[::5]\n        data = data.transform(\n            Domain(data.domain.attributes[:10], data.domain.class_vars,\n                   data.domain.metas + (data.domain[\"diau g\"],)))\n        data.ensure_copy()\n        return data\n\n    def test_set_split_column_key(self):\n        data = self._brown_selected_10()\n        function = data.domain[\"function\"]\n        data_t = data.transpose(data)\n        w = self.widget\n        self.send_signal(self.widget.Inputs.data, data_t, widget=w)\n        w.set_column_split_var(function)\n        self.assertEqual(len(w.parts.columns), len(function.values))\n        w.set_column_split_var(None)\n        self.assertEqual(len(w.parts.columns), 1)\n\n    def test_set_split_column_key_missing(self):\n        data = self._brown_selected_10()\n        with data.unlocked():\n            data.Y[:5] = np.nan\n        data_t = data.transpose(data)\n        function = data.domain[\"function\"]\n        w = self.widget\n        self.send_signal(self.widget.Inputs.data, data_t, widget=w)\n        w.set_column_split_var(function)\n        self.assertEqual(len(w.parts.columns), len(function.values) + 1)\n        ncols = sum(len(p.indices) for p in w.parts.columns)\n        self.assertEqual(ncols, len(data_t.domain.attributes))\n        w.set_column_split_var(None)\n        self.assertEqual(len(w.parts.columns), 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_centering_threshold_change(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        cmw = self.widget.color_map_widget\n        palette_index = cmw.findData(\n            colorpalettes.ContinuousPalettes[\"diverging_bwr_40_95_c42\"],\n            Qt.UserRole)\n        cmw.setCurrentIndex(palette_index)\n\n        self.widget.update_color_schema = Mock()\n        cmw.centerChanged.emit(42)\n        self.widget.update_color_schema.assert_called()\n\n    def test_palette_center(self):\n        widget = self.widget\n        model = widget.color_map_widget.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_map_widget.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 = self.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_row_color_annotations_with_na(self):\n        widget = self.widget\n        data = self._brown_selected_10()\n        with data.unlocked():\n            data.Y[:3] = np.nan\n            data.metas[:3, -1] = np.nan\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(data.domain[\"diau g\"])\n        with data.unlocked():\n            data.Y[:] = np.nan\n            data.metas[:, -1] = np.nan\n        self.send_signal(widget.Inputs.data, data, widget=widget)\n        widget.set_annotation_color_var(data.domain[\"function\"])\n        widget.set_annotation_color_var(data.domain[\"diau g\"])\n        widget.set_annotation_color_var(None)\n        self.assertFalse(widget.scene.widget.right_side_colors[0].isVisible())\n\n    def test_col_color_annotations(self):\n        widget = self.widget\n        data = self._brown_selected_10()\n        data_t = data.transpose(data)\n        self.send_signal(widget.Inputs.data, data_t, widget=widget)\n        # discrete\n        widget.set_column_annotation_color_var(data.domain[\"function\"])\n        self.assertTrue(widget.scene.widget.top_side_colors[0].isVisible())\n        # continuous\n        widget.set_column_annotation_color_var(data.domain[\"diau g\"])\n        widget.set_column_annotation_color_var(None)\n        self.assertFalse(widget.scene.widget.top_side_colors[0].isVisible())\n\n    def test_col_color_annotations_with_na(self):\n        widget = self.widget\n        data = self._brown_selected_10()\n        with data.unlocked():\n            data.Y[:3] = np.nan\n            data.metas[:3, -1] = np.nan\n        data_t = data.transpose(data)\n        self.send_signal(widget.Inputs.data, data_t, widget=widget)\n        widget.set_column_annotation_color_var(data.domain[\"function\"])\n        self.assertTrue(widget.scene.widget.top_side_colors[0].isVisible())\n        widget.set_column_annotation_color_var(data.domain[\"diau g\"])\n        with data.unlocked():\n            data.Y[:] = np.nan\n            data.metas[:, -1] = np.nan\n        data_t = data.transpose(data)\n        self.send_signal(widget.Inputs.data, data_t, widget=widget)\n        widget.set_column_annotation_color_var(data.domain[\"function\"])\n        widget.set_column_annotation_color_var(data.domain[\"diau g\"])\n        widget.set_column_annotation_color_var(None)\n        self.assertFalse(widget.scene.widget.top_side_colors[0].isVisible())\n\n    def test_data_with_hidden(self):\n        w = self.widget\n        housing = self.housing.copy()\n        housing.domain.attributes[0].attributes[\"hidden\"] = True\n        self.send_signal(self.widget.Inputs.data, housing)\n        self.assertEqual(len(w.effective_data.domain.attributes),\n                         len(housing.domain.attributes) - 1)",
                            "ast_type_str": "ClassDef",
                            "start_lineno": 26,
                            "end_lineno": 408,
                            "parent": null
                        },
                        {
                            "file_name": "Orange/widgets/visualize/tests/test_owheatmap.py",
                            "type": "method",
                            "name": "TestOWHeatMap.test_information_message",
                            "content": "class TestOWHeatMap(WidgetTest, WidgetOutputsTestMixin):\n    @classmethod\n    def setUpClass(cls):\n        super().setUpClass()\n        WidgetOutputsTestMixin.init(cls)\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        self.housing = Table(\"housing\")\n        self.titanic = Table(\"titanic\")\n        self.brown_selected = Table(\"brown-selected\")\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        self.widget.MaxClustering = 20\n        self.widget.MaxOrderedClustering = 15\n        data = self.brown_selected[:, :10]\n        self.send_signal(self.widget.Inputs.data, data[:15])\n        self.assertFalse(self.widget.Information.active)\n        self.send_signal(self.widget.Inputs.data, data[:16])\n        self.assertTrue(self.widget.Information.active)\n        self.assertEqual(self.widget.row_clustering, Clustering.Clustering)\n        self.send_signal(self.widget.Inputs.data, data[:20])\n        self.assertFalse(self.widget.Information.active)\n        self.send_signal(self.widget.Inputs.data, data[:21])\n        self.assertTrue(self.widget.Information.active)\n        data = Table(\"heart_disease.tab\")[:10]\n        self.send_signal(self.widget.Inputs.data, data)\n        self.assertTrue(self.widget.Information.discrete_ignored.is_shown())\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_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        with iris.unlocked():\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.commit, 'now') 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_saved_selection_when_not_possible(self):\n        # Has stored selection but ot enough columns for clustering.\n        iris = Table(\"iris\")[:, [\"petal width\"]]\n        w = self.create_widget(\n            OWHeatMap, stored_settings={\n                \"__version__\": 3,\n                \"col_clustering_method\": \"Clustering\",\n                \"selected_rows\": [1, 2, 3],\n            }\n        )\n        self.send_signal(w.Inputs.data, iris)\n        out = self.get_output(w.Outputs.selected_data)\n        self.assertSequenceEqual(list(out.ids), list(iris.ids[[1, 2, 3]]))\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        with data.unlocked():\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 _brown_selected_10(self):\n        data = self.brown_selected[::5]\n        data = data.transform(\n            Domain(data.domain.attributes[:10], data.domain.class_vars,\n                   data.domain.metas + (data.domain[\"diau g\"],)))\n        data.ensure_copy()\n        return data\n\n    def test_set_split_column_key(self):\n        data = self._brown_selected_10()\n        function = data.domain[\"function\"]\n        data_t = data.transpose(data)\n        w = self.widget\n        self.send_signal(self.widget.Inputs.data, data_t, widget=w)\n        w.set_column_split_var(function)\n        self.assertEqual(len(w.parts.columns), len(function.values))\n        w.set_column_split_var(None)\n        self.assertEqual(len(w.parts.columns), 1)\n\n    def test_set_split_column_key_missing(self):\n        data = self._brown_selected_10()\n        with data.unlocked():\n            data.Y[:5] = np.nan\n        data_t = data.transpose(data)\n        function = data.domain[\"function\"]\n        w = self.widget\n        self.send_signal(self.widget.Inputs.data, data_t, widget=w)\n        w.set_column_split_var(function)\n        self.assertEqual(len(w.parts.columns), len(function.values) + 1)\n        ncols = sum(len(p.indices) for p in w.parts.columns)\n        self.assertEqual(ncols, len(data_t.domain.attributes))\n        w.set_column_split_var(None)\n        self.assertEqual(len(w.parts.columns), 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_centering_threshold_change(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        cmw = self.widget.color_map_widget\n        palette_index = cmw.findData(\n            colorpalettes.ContinuousPalettes[\"diverging_bwr_40_95_c42\"],\n            Qt.UserRole)\n        cmw.setCurrentIndex(palette_index)\n\n        self.widget.update_color_schema = Mock()\n        cmw.centerChanged.emit(42)\n        self.widget.update_color_schema.assert_called()\n\n    def test_palette_center(self):\n        widget = self.widget\n        model = widget.color_map_widget.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_map_widget.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 = self.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_row_color_annotations_with_na(self):\n        widget = self.widget\n        data = self._brown_selected_10()\n        with data.unlocked():\n            data.Y[:3] = np.nan\n            data.metas[:3, -1] = np.nan\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(data.domain[\"diau g\"])\n        with data.unlocked():\n            data.Y[:] = np.nan\n            data.metas[:, -1] = np.nan\n        self.send_signal(widget.Inputs.data, data, widget=widget)\n        widget.set_annotation_color_var(data.domain[\"function\"])\n        widget.set_annotation_color_var(data.domain[\"diau g\"])\n        widget.set_annotation_color_var(None)\n        self.assertFalse(widget.scene.widget.right_side_colors[0].isVisible())\n\n    def test_col_color_annotations(self):\n        widget = self.widget\n        data = self._brown_selected_10()\n        data_t = data.transpose(data)\n        self.send_signal(widget.Inputs.data, data_t, widget=widget)\n        # discrete\n        widget.set_column_annotation_color_var(data.domain[\"function\"])\n        self.assertTrue(widget.scene.widget.top_side_colors[0].isVisible())\n        # continuous\n        widget.set_column_annotation_color_var(data.domain[\"diau g\"])\n        widget.set_column_annotation_color_var(None)\n        self.assertFalse(widget.scene.widget.top_side_colors[0].isVisible())\n\n    def test_col_color_annotations_with_na(self):\n        widget = self.widget\n        data = self._brown_selected_10()\n        with data.unlocked():\n            data.Y[:3] = np.nan\n            data.metas[:3, -1] = np.nan\n        data_t = data.transpose(data)\n        self.send_signal(widget.Inputs.data, data_t, widget=widget)\n        widget.set_column_annotation_color_var(data.domain[\"function\"])\n        self.assertTrue(widget.scene.widget.top_side_colors[0].isVisible())\n        widget.set_column_annotation_color_var(data.domain[\"diau g\"])\n        with data.unlocked():\n            data.Y[:] = np.nan\n            data.metas[:, -1] = np.nan\n        data_t = data.transpose(data)\n        self.send_signal(widget.Inputs.data, data_t, widget=widget)\n        widget.set_column_annotation_color_var(data.domain[\"function\"])\n        widget.set_column_annotation_color_var(data.domain[\"diau g\"])\n        widget.set_column_annotation_color_var(None)\n        self.assertFalse(widget.scene.widget.top_side_colors[0].isVisible())\n\n    def test_data_with_hidden(self):\n        w = self.widget\n        housing = self.housing.copy()\n        housing.domain.attributes[0].attributes[\"hidden\"] = True\n        self.send_signal(self.widget.Inputs.data, housing)\n        self.assertEqual(len(w.effective_data.domain.attributes),\n                         len(housing.domain.attributes) - 1)",
                            "ast_type_str": "ClassDef",
                            "start_lineno": 61,
                            "end_lineno": 77,
                            "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.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        self.housing = Table(\"housing\")\n        self.titanic = Table(\"titanic\")\n        self.brown_selected = Table(\"brown-selected\")\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        self.widget.MaxClustering = 20\n        self.widget.MaxOrderedClustering = 15\n        data = self.brown_selected[:, :10]\n        self.send_signal(self.widget.Inputs.data, data[:15])\n        self.assertFalse(self.widget.Information.active)\n        self.send_signal(self.widget.Inputs.data, data[:16])\n        self.assertTrue(self.widget.Information.active)\n        self.assertEqual(self.widget.row_clustering, Clustering.Clustering)\n        self.send_signal(self.widget.Inputs.data, data[:20])\n        self.assertFalse(self.widget.Information.active)\n        self.send_signal(self.widget.Inputs.data, data[:21])\n        self.assertTrue(self.widget.Information.active)\n        data = Table(\"heart_disease.tab\")[:10]\n        self.send_signal(self.widget.Inputs.data, data)\n        self.assertTrue(self.widget.Information.discrete_ignored.is_shown())\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_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        with iris.unlocked():\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.commit, 'now') 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_saved_selection_when_not_possible(self):\n        # Has stored selection but ot enough columns for clustering.\n        iris = Table(\"iris\")[:, [\"petal width\"]]\n        w = self.create_widget(\n            OWHeatMap, stored_settings={\n                \"__version__\": 3,\n                \"col_clustering_method\": \"Clustering\",\n                \"selected_rows\": [1, 2, 3],\n            }\n        )\n        self.send_signal(w.Inputs.data, iris)\n        out = self.get_output(w.Outputs.selected_data)\n        self.assertSequenceEqual(list(out.ids), list(iris.ids[[1, 2, 3]]))\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        with data.unlocked():\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 _brown_selected_10(self):\n        data = self.brown_selected[::5]\n        data = data.transform(\n            Domain(data.domain.attributes[:10], data.domain.class_vars,\n                   data.domain.metas + (data.domain[\"diau g\"],)))\n        data.ensure_copy()\n        return data\n\n    def test_set_split_column_key(self):\n        data = self._brown_selected_10()\n        function = data.domain[\"function\"]\n        data_t = data.transpose(data)\n        w = self.widget\n        self.send_signal(self.widget.Inputs.data, data_t, widget=w)\n        w.set_column_split_var(function)\n        self.assertEqual(len(w.parts.columns), len(function.values))\n        w.set_column_split_var(None)\n        self.assertEqual(len(w.parts.columns), 1)\n\n    def test_set_split_column_key_missing(self):\n        data = self._brown_selected_10()\n        with data.unlocked():\n            data.Y[:5] = np.nan\n        data_t = data.transpose(data)\n        function = data.domain[\"function\"]\n        w = self.widget\n        self.send_signal(self.widget.Inputs.data, data_t, widget=w)\n        w.set_column_split_var(function)\n        self.assertEqual(len(w.parts.columns), len(function.values) + 1)\n        ncols = sum(len(p.indices) for p in w.parts.columns)\n        self.assertEqual(ncols, len(data_t.domain.attributes))\n        w.set_column_split_var(None)\n        self.assertEqual(len(w.parts.columns), 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_centering_threshold_change(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        cmw = self.widget.color_map_widget\n        palette_index = cmw.findData(\n            colorpalettes.ContinuousPalettes[\"diverging_bwr_40_95_c42\"],\n            Qt.UserRole)\n        cmw.setCurrentIndex(palette_index)\n\n        self.widget.update_color_schema = Mock()\n        cmw.centerChanged.emit(42)\n        self.widget.update_color_schema.assert_called()\n\n    def test_palette_center(self):\n        widget = self.widget\n        model = widget.color_map_widget.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_map_widget.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 = self.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_row_color_annotations_with_na(self):\n        widget = self.widget\n        data = self._brown_selected_10()\n        with data.unlocked():\n            data.Y[:3] = np.nan\n            data.metas[:3, -1] = np.nan\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(data.domain[\"diau g\"])\n        with data.unlocked():\n            data.Y[:] = np.nan\n            data.metas[:, -1] = np.nan\n        self.send_signal(widget.Inputs.data, data, widget=widget)\n        widget.set_annotation_color_var(data.domain[\"function\"])\n        widget.set_annotation_color_var(data.domain[\"diau g\"])\n        widget.set_annotation_color_var(None)\n        self.assertFalse(widget.scene.widget.right_side_colors[0].isVisible())\n\n    def test_col_color_annotations(self):\n        widget = self.widget\n        data = self._brown_selected_10()\n        data_t = data.transpose(data)\n        self.send_signal(widget.Inputs.data, data_t, widget=widget)\n        # discrete\n        widget.set_column_annotation_color_var(data.domain[\"function\"])\n        self.assertTrue(widget.scene.widget.top_side_colors[0].isVisible())\n        # continuous\n        widget.set_column_annotation_color_var(data.domain[\"diau g\"])\n        widget.set_column_annotation_color_var(None)\n        self.assertFalse(widget.scene.widget.top_side_colors[0].isVisible())\n\n    def test_col_color_annotations_with_na(self):\n        widget = self.widget\n        data = self._brown_selected_10()\n        with data.unlocked():\n            data.Y[:3] = np.nan\n            data.metas[:3, -1] = np.nan\n        data_t = data.transpose(data)\n        self.send_signal(widget.Inputs.data, data_t, widget=widget)\n        widget.set_column_annotation_color_var(data.domain[\"function\"])\n        self.assertTrue(widget.scene.widget.top_side_colors[0].isVisible())\n        widget.set_column_annotation_color_var(data.domain[\"diau g\"])\n        with data.unlocked():\n            data.Y[:] = np.nan\n            data.metas[:, -1] = np.nan\n        data_t = data.transpose(data)\n        self.send_signal(widget.Inputs.data, data_t, widget=widget)\n        widget.set_column_annotation_color_var(data.domain[\"function\"])\n        widget.set_column_annotation_color_var(data.domain[\"diau g\"])\n        widget.set_column_annotation_color_var(None)\n        self.assertFalse(widget.scene.widget.top_side_colors[0].isVisible())\n\n    def test_data_with_hidden(self):\n        w = self.widget\n        housing = self.housing.copy()\n        housing.domain.attributes[0].attributes[\"hidden\"] = True\n        self.send_signal(self.widget.Inputs.data, housing)\n        self.assertEqual(len(w.effective_data.domain.attributes),\n                         len(housing.domain.attributes) - 1)",
                                "ast_type_str": "ClassDef",
                                "start_lineno": 26,
                                "end_lineno": 408,
                                "parent": null
                            }
                        }
                    ],
                    "added_entities": [],
                    "deleted_entities": []
                }
            ]
        }
    ],
    "old_commit_hash": "bf75c8c72a52b2b7eef46a75bd162e9fca112a51^",
    "new_commit_hash": "bf75c8c72a52b2b7eef46a75bd162e9fca112a51",
    "commit_message": "Issue a warning on discrete data",
    "commit_date": "2022-06-13T16:55:07+02:00",
    "metadata": {}
}
{
    "repo_name": "orange3",
    "new_commit_hash": "bf75c8c72a52b2b7eef46a75bd162e9fca112a51",
    "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, Mock\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.widgets.utils import colorpalettes\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.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        self.housing = Table(\"housing\")\n        self.titanic = Table(\"titanic\")\n        self.brown_selected = Table(\"brown-selected\")\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        self.widget.MaxClustering = 20\n        self.widget.MaxOrderedClustering = 15\n        data = self.brown_selected[:, :10]\n        self.send_signal(self.widget.Inputs.data, data[:15])\n        self.assertFalse(self.widget.Information.active)\n        self.send_signal(self.widget.Inputs.data, data[:16])\n        self.assertTrue(self.widget.Information.active)\n        self.assertEqual(self.widget.row_clustering, Clustering.Clustering)\n        self.send_signal(self.widget.Inputs.data, data[:20])\n        self.assertFalse(self.widget.Information.active)\n        self.send_signal(self.widget.Inputs.data, data[:21])\n        self.assertTrue(self.widget.Information.active)\n        data = Table(\"heart_disease.tab\")[:10]\n        self.send_signal(self.widget.Inputs.data, data)\n        self.assertTrue(self.widget.Information.discrete_ignored.is_shown())\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_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        with iris.unlocked():\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.commit, 'now') 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_saved_selection_when_not_possible(self):\n        # Has stored selection but ot enough columns for clustering.\n        iris = Table(\"iris\")[:, [\"petal width\"]]\n        w = self.create_widget(\n            OWHeatMap, stored_settings={\n                \"__version__\": 3,\n                \"col_clustering_method\": \"Clustering\",\n                \"selected_rows\": [1, 2, 3],\n            }\n        )\n        self.send_signal(w.Inputs.data, iris)\n        out = self.get_output(w.Outputs.selected_data)\n        self.assertSequenceEqual(list(out.ids), list(iris.ids[[1, 2, 3]]))\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        with data.unlocked():\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 _brown_selected_10(self):\n        data = self.brown_selected[::5]\n        data = data.transform(\n            Domain(data.domain.attributes[:10], data.domain.class_vars,\n                   data.domain.metas + (data.domain[\"diau g\"],)))\n        data.ensure_copy()\n        return data\n\n    def test_set_split_column_key(self):\n        data = self._brown_selected_10()\n        function = data.domain[\"function\"]\n        data_t = data.transpose(data)\n        w = self.widget\n        self.send_signal(self.widget.Inputs.data, data_t, widget=w)\n        w.set_column_split_var(function)\n        self.assertEqual(len(w.parts.columns), len(function.values))\n        w.set_column_split_var(None)\n        self.assertEqual(len(w.parts.columns), 1)\n\n    def test_set_split_column_key_missing(self):\n        data = self._brown_selected_10()\n        with data.unlocked():\n            data.Y[:5] = np.nan\n        data_t = data.transpose(data)\n        function = data.domain[\"function\"]\n        w = self.widget\n        self.send_signal(self.widget.Inputs.data, data_t, widget=w)\n        w.set_column_split_var(function)\n        self.assertEqual(len(w.parts.columns), len(function.values) + 1)\n        ncols = sum(len(p.indices) for p in w.parts.columns)\n        self.assertEqual(ncols, len(data_t.domain.attributes))\n        w.set_column_split_var(None)\n        self.assertEqual(len(w.parts.columns), 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_centering_threshold_change(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        cmw = self.widget.color_map_widget\n        palette_index = cmw.findData(\n            colorpalettes.ContinuousPalettes[\"diverging_bwr_40_95_c42\"],\n            Qt.UserRole)\n        cmw.setCurrentIndex(palette_index)\n\n        self.widget.update_color_schema = Mock()\n        cmw.centerChanged.emit(42)\n        self.widget.update_color_schema.assert_called()\n\n    def test_palette_center(self):\n        widget = self.widget\n        model = widget.color_map_widget.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_map_widget.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 = self.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_row_color_annotations_with_na(self):\n        widget = self.widget\n        data = self._brown_selected_10()\n        with data.unlocked():\n            data.Y[:3] = np.nan\n            data.metas[:3, -1] = np.nan\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(data.domain[\"diau g\"])\n        with data.unlocked():\n            data.Y[:] = np.nan\n            data.metas[:, -1] = np.nan\n        self.send_signal(widget.Inputs.data, data, widget=widget)\n        widget.set_annotation_color_var(data.domain[\"function\"])\n        widget.set_annotation_color_var(data.domain[\"diau g\"])\n        widget.set_annotation_color_var(None)\n        self.assertFalse(widget.scene.widget.right_side_colors[0].isVisible())\n\n    def test_col_color_annotations(self):\n        widget = self.widget\n        data = self._brown_selected_10()\n        data_t = data.transpose(data)\n        self.send_signal(widget.Inputs.data, data_t, widget=widget)\n        # discrete\n        widget.set_column_annotation_color_var(data.domain[\"function\"])\n        self.assertTrue(widget.scene.widget.top_side_colors[0].isVisible())\n        # continuous\n        widget.set_column_annotation_color_var(data.domain[\"diau g\"])\n        widget.set_column_annotation_color_var(None)\n        self.assertFalse(widget.scene.widget.top_side_colors[0].isVisible())\n\n    def test_col_color_annotations_with_na(self):\n        widget = self.widget\n        data = self._brown_selected_10()\n        with data.unlocked():\n            data.Y[:3] = np.nan\n            data.metas[:3, -1] = np.nan\n        data_t = data.transpose(data)\n        self.send_signal(widget.Inputs.data, data_t, widget=widget)\n        widget.set_column_annotation_color_var(data.domain[\"function\"])\n        self.assertTrue(widget.scene.widget.top_side_colors[0].isVisible())\n        widget.set_column_annotation_color_var(data.domain[\"diau g\"])\n        with data.unlocked():\n            data.Y[:] = np.nan\n            data.metas[:, -1] = np.nan\n        data_t = data.transpose(data)\n        self.send_signal(widget.Inputs.data, data_t, widget=widget)\n        widget.set_column_annotation_color_var(data.domain[\"function\"])\n        widget.set_column_annotation_color_var(data.domain[\"diau g\"])\n        widget.set_column_annotation_color_var(None)\n        self.assertFalse(widget.scene.widget.top_side_colors[0].isVisible())\n\n    def test_data_with_hidden(self):\n        w = self.widget\n        housing = self.housing.copy()\n        housing.domain.attributes[0].attributes[\"hidden\"] = True\n        self.send_signal(self.widget.Inputs.data, housing)\n        self.assertEqual(len(w.effective_data.domain.attributes),\n                         len(housing.domain.attributes) - 1)\n\n\nif __name__ == \"__main__\":\n    unittest.main()\n"
    ],
    "test_file_names": [
        "test_1.py"
    ],
    "setup_res_code": 0,
    "setup_res_stdout": "Starting Orange installation attempts...\nAttempting installation with Python 3.7...\nrunning build_ext\nCompiling Orange/classification/_tree_scorers.pyx because it changed.\nCompiling Orange/data/_contingency.pyx because it changed.\nCompiling Orange/data/_io.pyx because it changed.\nCompiling Orange/data/_valuecount.pyx because it changed.\nCompiling Orange/data/_variable.pyx because it changed.\nCompiling Orange/distance/_distance.pyx because it changed.\nCompiling Orange/preprocess/_discretize.pyx because it changed.\nCompiling Orange/preprocess/_relieff.pyx because it changed.\nCompiling Orange/projection/_som.pyx because it changed.\n[1/9] Cythonizing Orange/classification/_tree_scorers.pyx\n[2/9] Cythonizing Orange/data/_contingency.pyx\n[3/9] Cythonizing Orange/data/_io.pyx\n[4/9] Cythonizing Orange/data/_valuecount.pyx\n[5/9] Cythonizing Orange/data/_variable.pyx\n[6/9] Cythonizing Orange/distance/_distance.pyx\n[7/9] Cythonizing Orange/preprocess/_discretize.pyx\n[8/9] Cythonizing Orange/preprocess/_relieff.pyx\n[9/9] Cythonizing Orange/projection/_som.pyx\nbuilding 'Orange.classification._tree_scorers' extension\ncreating build\ncreating build/temp.linux-x86_64-3.7\ncreating build/temp.linux-x86_64-3.7/Orange\ncreating build/temp.linux-x86_64-3.7/Orange/classification\ncc -pthread -Wno-unused-result -Wsign-compare -Wunreachable-code -DNDEBUG -g -fwrapv -O3 -Wall -fPIC -I/tools/deps/include -I/tools/deps/include/ncursesw -fPIC -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_bf75c8c72a52b2b7eef46a75bd162e9fca112a51/.venv/lib/python3.7/site-packages/numpy/core/include -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_bf75c8c72a52b2b7eef46a75bd162e9fca112a51/.venv/include -I/home/gcpuser/.local/share/uv/python/cpython-3.7.9-linux-x86_64-gnu/include/python3.7m -c Orange/classification/_tree_scorers.c -o build/temp.linux-x86_64-3.7/Orange/classification/_tree_scorers.o\ncreating build/lib.linux-x86_64-3.7\ncreating build/lib.linux-x86_64-3.7/Orange\ncreating build/lib.linux-x86_64-3.7/Orange/classification\ncc -pthread -shared -L/tools/deps/lib build/temp.linux-x86_64-3.7/Orange/classification/_tree_scorers.o -L/home/gcpuser/.local/share/uv/python/cpython-3.7.9-linux-x86_64-gnu/lib -lm -lpython3.7m -o build/lib.linux-x86_64-3.7/Orange/classification/_tree_scorers.cpython-37m-x86_64-linux-gnu.so\nbuilding 'Orange.data._contingency' extension\ncreating build/temp.linux-x86_64-3.7/Orange/data\ncc -pthread -Wno-unused-result -Wsign-compare -Wunreachable-code -DNDEBUG -g -fwrapv -O3 -Wall -fPIC -I/tools/deps/include -I/tools/deps/include/ncursesw -fPIC -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_bf75c8c72a52b2b7eef46a75bd162e9fca112a51/.venv/lib/python3.7/site-packages/numpy/core/include -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_bf75c8c72a52b2b7eef46a75bd162e9fca112a51/.venv/include -I/home/gcpuser/.local/share/uv/python/cpython-3.7.9-linux-x86_64-gnu/include/python3.7m -c Orange/data/_contingency.c -o build/temp.linux-x86_64-3.7/Orange/data/_contingency.o\ncreating build/lib.linux-x86_64-3.7/Orange/data\ncc -pthread -shared -L/tools/deps/lib build/temp.linux-x86_64-3.7/Orange/data/_contingency.o -L/home/gcpuser/.local/share/uv/python/cpython-3.7.9-linux-x86_64-gnu/lib -lm -lpython3.7m -o build/lib.linux-x86_64-3.7/Orange/data/_contingency.cpython-37m-x86_64-linux-gnu.so\nbuilding 'Orange.data._io' extension\ncc -pthread -Wno-unused-result -Wsign-compare -Wunreachable-code -DNDEBUG -g -fwrapv -O3 -Wall -fPIC -I/tools/deps/include -I/tools/deps/include/ncursesw -fPIC -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_bf75c8c72a52b2b7eef46a75bd162e9fca112a51/.venv/lib/python3.7/site-packages/numpy/core/include -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_bf75c8c72a52b2b7eef46a75bd162e9fca112a51/.venv/include -I/home/gcpuser/.local/share/uv/python/cpython-3.7.9-linux-x86_64-gnu/include/python3.7m -c Orange/data/_io.c -o build/temp.linux-x86_64-3.7/Orange/data/_io.o\ncc -pthread -shared -L/tools/deps/lib build/temp.linux-x86_64-3.7/Orange/data/_io.o -L/home/gcpuser/.local/share/uv/python/cpython-3.7.9-linux-x86_64-gnu/lib -lm -lpython3.7m -o build/lib.linux-x86_64-3.7/Orange/data/_io.cpython-37m-x86_64-linux-gnu.so\nbuilding 'Orange.data._valuecount' extension\ncc -pthread -Wno-unused-result -Wsign-compare -Wunreachable-code -DNDEBUG -g -fwrapv -O3 -Wall -fPIC -I/tools/deps/include -I/tools/deps/include/ncursesw -fPIC -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_bf75c8c72a52b2b7eef46a75bd162e9fca112a51/.venv/lib/python3.7/site-packages/numpy/core/include -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_bf75c8c72a52b2b7eef46a75bd162e9fca112a51/.venv/include -I/home/gcpuser/.local/share/uv/python/cpython-3.7.9-linux-x86_64-gnu/include/python3.7m -c Orange/data/_valuecount.c -o build/temp.linux-x86_64-3.7/Orange/data/_valuecount.o\ncc -pthread -shared -L/tools/deps/lib build/temp.linux-x86_64-3.7/Orange/data/_valuecount.o -L/home/gcpuser/.local/share/uv/python/cpython-3.7.9-linux-x86_64-gnu/lib -lm -lpython3.7m -o build/lib.linux-x86_64-3.7/Orange/data/_valuecount.cpython-37m-x86_64-linux-gnu.so\nbuilding 'Orange.data._variable' extension\ncc -pthread -Wno-unused-result -Wsign-compare -Wunreachable-code -DNDEBUG -g -fwrapv -O3 -Wall -fPIC -I/tools/deps/include -I/tools/deps/include/ncursesw -fPIC -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_bf75c8c72a52b2b7eef46a75bd162e9fca112a51/.venv/lib/python3.7/site-packages/numpy/core/include -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_bf75c8c72a52b2b7eef46a75bd162e9fca112a51/.venv/include -I/home/gcpuser/.local/share/uv/python/cpython-3.7.9-linux-x86_64-gnu/include/python3.7m -c Orange/data/_variable.c -o build/temp.linux-x86_64-3.7/Orange/data/_variable.o\ncc -pthread -shared -L/tools/deps/lib build/temp.linux-x86_64-3.7/Orange/data/_variable.o -L/home/gcpuser/.local/share/uv/python/cpython-3.7.9-linux-x86_64-gnu/lib -lm -lpython3.7m -o build/lib.linux-x86_64-3.7/Orange/data/_variable.cpython-37m-x86_64-linux-gnu.so\nbuilding 'Orange.distance._distance' extension\ncreating build/temp.linux-x86_64-3.7/Orange/distance\ncc -pthread -Wno-unused-result -Wsign-compare -Wunreachable-code -DNDEBUG -g -fwrapv -O3 -Wall -fPIC -I/tools/deps/include -I/tools/deps/include/ncursesw -fPIC -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_bf75c8c72a52b2b7eef46a75bd162e9fca112a51/.venv/lib/python3.7/site-packages/numpy/core/include -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_bf75c8c72a52b2b7eef46a75bd162e9fca112a51/.venv/include -I/home/gcpuser/.local/share/uv/python/cpython-3.7.9-linux-x86_64-gnu/include/python3.7m -c Orange/distance/_distance.c -o build/temp.linux-x86_64-3.7/Orange/distance/_distance.o\ncreating build/lib.linux-x86_64-3.7/Orange/distance\ncc -pthread -shared -L/tools/deps/lib build/temp.linux-x86_64-3.7/Orange/distance/_distance.o -L/home/gcpuser/.local/share/uv/python/cpython-3.7.9-linux-x86_64-gnu/lib -lm -lpython3.7m -o build/lib.linux-x86_64-3.7/Orange/distance/_distance.cpython-37m-x86_64-linux-gnu.so\nbuilding 'Orange.preprocess._discretize' extension\ncreating build/temp.linux-x86_64-3.7/Orange/preprocess\ncc -pthread -Wno-unused-result -Wsign-compare -Wunreachable-code -DNDEBUG -g -fwrapv -O3 -Wall -fPIC -I/tools/deps/include -I/tools/deps/include/ncursesw -fPIC -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_bf75c8c72a52b2b7eef46a75bd162e9fca112a51/.venv/lib/python3.7/site-packages/numpy/core/include -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_bf75c8c72a52b2b7eef46a75bd162e9fca112a51/.venv/include -I/home/gcpuser/.local/share/uv/python/cpython-3.7.9-linux-x86_64-gnu/include/python3.7m -c Orange/preprocess/_discretize.c -o build/temp.linux-x86_64-3.7/Orange/preprocess/_discretize.o\ncreating build/lib.linux-x86_64-3.7/Orange/preprocess\ncc -pthread -shared -L/tools/deps/lib build/temp.linux-x86_64-3.7/Orange/preprocess/_discretize.o -L/home/gcpuser/.local/share/uv/python/cpython-3.7.9-linux-x86_64-gnu/lib -lm -lpython3.7m -o build/lib.linux-x86_64-3.7/Orange/preprocess/_discretize.cpython-37m-x86_64-linux-gnu.so\nbuilding 'Orange.preprocess._relieff' extension\ncc -pthread -Wno-unused-result -Wsign-compare -Wunreachable-code -DNDEBUG -g -fwrapv -O3 -Wall -fPIC -I/tools/deps/include -I/tools/deps/include/ncursesw -fPIC -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_bf75c8c72a52b2b7eef46a75bd162e9fca112a51/.venv/lib/python3.7/site-packages/numpy/core/include -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_bf75c8c72a52b2b7eef46a75bd162e9fca112a51/.venv/include -I/home/gcpuser/.local/share/uv/python/cpython-3.7.9-linux-x86_64-gnu/include/python3.7m -c Orange/preprocess/_relieff.cpp -o build/temp.linux-x86_64-3.7/Orange/preprocess/_relieff.o\nc++ -pthread -shared -L/tools/deps/lib build/temp.linux-x86_64-3.7/Orange/preprocess/_relieff.o -L/home/gcpuser/.local/share/uv/python/cpython-3.7.9-linux-x86_64-gnu/lib -lm -lpython3.7m -o build/lib.linux-x86_64-3.7/Orange/preprocess/_relieff.cpython-37m-x86_64-linux-gnu.so\nbuilding 'Orange.projection._som' extension\ncreating build/temp.linux-x86_64-3.7/Orange/projection\ncc -pthread -Wno-unused-result -Wsign-compare -Wunreachable-code -DNDEBUG -g -fwrapv -O3 -Wall -fPIC -I/tools/deps/include -I/tools/deps/include/ncursesw -fPIC -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_bf75c8c72a52b2b7eef46a75bd162e9fca112a51/.venv/lib/python3.7/site-packages/numpy/core/include -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_bf75c8c72a52b2b7eef46a75bd162e9fca112a51/.venv/include -I/home/gcpuser/.local/share/uv/python/cpython-3.7.9-linux-x86_64-gnu/include/python3.7m -c Orange/projection/_som.c -o build/temp.linux-x86_64-3.7/Orange/projection/_som.o\ncreating build/lib.linux-x86_64-3.7/Orange/projection\ncc -pthread -shared -L/tools/deps/lib build/temp.linux-x86_64-3.7/Orange/projection/_som.o -L/home/gcpuser/.local/share/uv/python/cpython-3.7.9-linux-x86_64-gnu/lib -lm -lpython3.7m -o build/lib.linux-x86_64-3.7/Orange/projection/_som.cpython-37m-x86_64-linux-gnu.so\nbuilding 'Orange.classification._simple_tree' extension\ncc -pthread -Wno-unused-result -Wsign-compare -Wunreachable-code -DNDEBUG -g -fwrapv -O3 -Wall -fPIC -I/tools/deps/include -I/tools/deps/include/ncursesw -fPIC -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_bf75c8c72a52b2b7eef46a75bd162e9fca112a51/.venv/lib/python3.7/site-packages/numpy/core/include -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_bf75c8c72a52b2b7eef46a75bd162e9fca112a51/.venv/include -I/home/gcpuser/.local/share/uv/python/cpython-3.7.9-linux-x86_64-gnu/include/python3.7m -c Orange/classification/_simple_tree.c -o build/temp.linux-x86_64-3.7/Orange/classification/_simple_tree.o\ncc -pthread -shared -L/tools/deps/lib build/temp.linux-x86_64-3.7/Orange/classification/_simple_tree.o -L/home/gcpuser/.local/share/uv/python/cpython-3.7.9-linux-x86_64-gnu/lib -lm -lpython3.7m -o build/lib.linux-x86_64-3.7/Orange/classification/_simple_tree.cpython-37m-x86_64-linux-gnu.so\nbuilding 'Orange.widgets.utils._grid_density' extension\ncreating build/temp.linux-x86_64-3.7/Orange/widgets\ncreating build/temp.linux-x86_64-3.7/Orange/widgets/utils\ncc -pthread -Wno-unused-result -Wsign-compare -Wunreachable-code -DNDEBUG -g -fwrapv -O3 -Wall -fPIC -I/tools/deps/include -I/tools/deps/include/ncursesw -fPIC -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_bf75c8c72a52b2b7eef46a75bd162e9fca112a51/.venv/lib/python3.7/site-packages/numpy/core/include -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_bf75c8c72a52b2b7eef46a75bd162e9fca112a51/.venv/include -I/home/gcpuser/.local/share/uv/python/cpython-3.7.9-linux-x86_64-gnu/include/python3.7m -c Orange/widgets/utils/_grid_density.cpp -o build/temp.linux-x86_64-3.7/Orange/widgets/utils/_grid_density.o\ncreating build/lib.linux-x86_64-3.7/Orange/widgets\ncreating build/lib.linux-x86_64-3.7/Orange/widgets/utils\nc++ -pthread -shared -L/tools/deps/lib build/temp.linux-x86_64-3.7/Orange/widgets/utils/_grid_density.o -L/home/gcpuser/.local/share/uv/python/cpython-3.7.9-linux-x86_64-gnu/lib -lm -lpython3.7m -o build/lib.linux-x86_64-3.7/Orange/widgets/utils/_grid_density.cpython-37m-x86_64-linux-gnu.so\ncopying build/lib.linux-x86_64-3.7/Orange/classification/_tree_scorers.cpython-37m-x86_64-linux-gnu.so -> Orange/classification\ncopying build/lib.linux-x86_64-3.7/Orange/data/_contingency.cpython-37m-x86_64-linux-gnu.so -> Orange/data\ncopying build/lib.linux-x86_64-3.7/Orange/data/_io.cpython-37m-x86_64-linux-gnu.so -> Orange/data\ncopying build/lib.linux-x86_64-3.7/Orange/data/_valuecount.cpython-37m-x86_64-linux-gnu.so -> Orange/data\ncopying build/lib.linux-x86_64-3.7/Orange/data/_variable.cpython-37m-x86_64-linux-gnu.so -> Orange/data\ncopying build/lib.linux-x86_64-3.7/Orange/distance/_distance.cpython-37m-x86_64-linux-gnu.so -> Orange/distance\ncopying build/lib.linux-x86_64-3.7/Orange/preprocess/_discretize.cpython-37m-x86_64-linux-gnu.so -> Orange/preprocess\ncopying build/lib.linux-x86_64-3.7/Orange/preprocess/_relieff.cpython-37m-x86_64-linux-gnu.so -> Orange/preprocess\ncopying build/lib.linux-x86_64-3.7/Orange/projection/_som.cpython-37m-x86_64-linux-gnu.so -> Orange/projection\ncopying build/lib.linux-x86_64-3.7/Orange/classification/_simple_tree.cpython-37m-x86_64-linux-gnu.so -> Orange/classification\ncopying build/lib.linux-x86_64-3.7/Orange/widgets/utils/_grid_density.cpython-37m-x86_64-linux-gnu.so -> Orange/widgets/utils\nrunning develop\nrunning egg_info\ncreating Orange3.egg-info\nwriting Orange3.egg-info/PKG-INFO\nwriting dependency_links to Orange3.egg-info/dependency_links.txt\nwriting entry points to Orange3.egg-info/entry_points.txt\nwriting requirements to Orange3.egg-info/requires.txt\nwriting top-level names to Orange3.egg-info/top_level.txt\nwriting manifest file 'Orange3.egg-info/SOURCES.txt'\nreading manifest file 'Orange3.egg-info/SOURCES.txt'\nreading manifest template 'MANIFEST.in'\nadding license file 'LICENSE'\nwriting manifest file 'Orange3.egg-info/SOURCES.txt'\nrunning build_ext\ncopying build/lib.linux-x86_64-3.7/Orange/classification/_tree_scorers.cpython-37m-x86_64-linux-gnu.so -> Orange/classification\ncopying build/lib.linux-x86_64-3.7/Orange/data/_contingency.cpython-37m-x86_64-linux-gnu.so -> Orange/data\ncopying build/lib.linux-x86_64-3.7/Orange/data/_io.cpython-37m-x86_64-linux-gnu.so -> Orange/data\ncopying build/lib.linux-x86_64-3.7/Orange/data/_valuecount.cpython-37m-x86_64-linux-gnu.so -> Orange/data\ncopying build/lib.linux-x86_64-3.7/Orange/data/_variable.cpython-37m-x86_64-linux-gnu.so -> Orange/data\ncopying build/lib.linux-x86_64-3.7/Orange/distance/_distance.cpython-37m-x86_64-linux-gnu.so -> Orange/distance\ncopying build/lib.linux-x86_64-3.7/Orange/preprocess/_discretize.cpython-37m-x86_64-linux-gnu.so -> Orange/preprocess\ncopying build/lib.linux-x86_64-3.7/Orange/preprocess/_relieff.cpython-37m-x86_64-linux-gnu.so -> Orange/preprocess\ncopying build/lib.linux-x86_64-3.7/Orange/projection/_som.cpython-37m-x86_64-linux-gnu.so -> Orange/projection\ncopying build/lib.linux-x86_64-3.7/Orange/classification/_simple_tree.cpython-37m-x86_64-linux-gnu.so -> Orange/classification\ncopying build/lib.linux-x86_64-3.7/Orange/widgets/utils/_grid_density.cpython-37m-x86_64-linux-gnu.so -> Orange/widgets/utils\nCreating /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_bf75c8c72a52b2b7eef46a75bd162e9fca112a51/.venv/lib/python3.7/site-packages/Orange3.egg-link (link to .)\nAdding Orange3 3.33.0.dev0+bf75c8c to easy-install.pth file\nInstalling orange-canvas script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_bf75c8c72a52b2b7eef46a75bd162e9fca112a51/.venv/bin\n\nInstalled /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_bf75c8c72a52b2b7eef46a75bd162e9fca112a51\nProcessing dependencies for Orange3==3.33.0.dev0+bf75c8c\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_bf75c8c72a52b2b7eef46a75bd162e9fca112a51/.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_bf75c8c72a52b2b7eef46a75bd162e9fca112a51/.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_bf75c8c72a52b2b7eef46a75bd162e9fca112a51/.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_bf75c8c72a52b2b7eef46a75bd162e9fca112a51/.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_bf75c8c72a52b2b7eef46a75bd162e9fca112a51/.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_bf75c8c72a52b2b7eef46a75bd162e9fca112a51/.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_bf75c8c72a52b2b7eef46a75bd162e9fca112a51/.venv/lib/python3.7/site-packages\nSearching for qtconsole==5.4.4\nBest match: qtconsole 5.4.4\nAdding qtconsole 5.4.4 to easy-install.pth file\nInstalling jupyter-qtconsole script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_bf75c8c72a52b2b7eef46a75bd162e9fca112a51/.venv/bin\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_bf75c8c72a52b2b7eef46a75bd162e9fca112a51/.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_bf75c8c72a52b2b7eef46a75bd162e9fca112a51/.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_bf75c8c72a52b2b7eef46a75bd162e9fca112a51/.venv/lib/python3.7/site-packages\nSearching for pyqtgraph==0.12.3\nBest match: pyqtgraph 0.12.3\nAdding pyqtgraph 0.12.3 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_bf75c8c72a52b2b7eef46a75bd162e9fca112a51/.venv/lib/python3.7/site-packages\nSearching for pygments==2.17.2\nBest match: pygments 2.17.2\nAdding pygments 2.17.2 to easy-install.pth file\nInstalling pygmentize script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_bf75c8c72a52b2b7eef46a75bd162e9fca112a51/.venv/bin\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_bf75c8c72a52b2b7eef46a75bd162e9fca112a51/.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_bf75c8c72a52b2b7eef46a75bd162e9fca112a51/.venv/bin\nInstalling pip3 script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_bf75c8c72a52b2b7eef46a75bd162e9fca112a51/.venv/bin\nInstalling pip3.10 script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_bf75c8c72a52b2b7eef46a75bd162e9fca112a51/.venv/bin\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_bf75c8c72a52b2b7eef46a75bd162e9fca112a51/.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_bf75c8c72a52b2b7eef46a75bd162e9fca112a51/.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_bf75c8c72a52b2b7eef46a75bd162e9fca112a51/.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_bf75c8c72a52b2b7eef46a75bd162e9fca112a51/.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_bf75c8c72a52b2b7eef46a75bd162e9fca112a51/.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_bf75c8c72a52b2b7eef46a75bd162e9fca112a51/.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_bf75c8c72a52b2b7eef46a75bd162e9fca112a51/.venv/bin\nInstalling f2py3 script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_bf75c8c72a52b2b7eef46a75bd162e9fca112a51/.venv/bin\nInstalling f2py3.7 script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_bf75c8c72a52b2b7eef46a75bd162e9fca112a51/.venv/bin\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_bf75c8c72a52b2b7eef46a75bd162e9fca112a51/.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_bf75c8c72a52b2b7eef46a75bd162e9fca112a51/.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_bf75c8c72a52b2b7eef46a75bd162e9fca112a51/.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_bf75c8c72a52b2b7eef46a75bd162e9fca112a51/.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_bf75c8c72a52b2b7eef46a75bd162e9fca112a51/.venv/bin\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_bf75c8c72a52b2b7eef46a75bd162e9fca112a51/.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_bf75c8c72a52b2b7eef46a75bd162e9fca112a51/.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_bf75c8c72a52b2b7eef46a75bd162e9fca112a51/.venv/bin\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_bf75c8c72a52b2b7eef46a75bd162e9fca112a51/.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_bf75c8c72a52b2b7eef46a75bd162e9fca112a51/.venv/bin\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_bf75c8c72a52b2b7eef46a75bd162e9fca112a51/.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_bf75c8c72a52b2b7eef46a75bd162e9fca112a51/.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_bf75c8c72a52b2b7eef46a75bd162e9fca112a51/.venv/lib/python3.7/site-packages\nSearching for PyQtWebEngine==5.15.6\nBest match: PyQtWebEngine 5.15.6\nAdding PyQtWebEngine 5.15.6 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_bf75c8c72a52b2b7eef46a75bd162e9fca112a51/.venv/lib/python3.7/site-packages\nSearching for PyQt5==5.15.10\nBest match: PyQt5 5.15.10\nAdding PyQt5 5.15.10 to easy-install.pth file\nInstalling pylupdate5 script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_bf75c8c72a52b2b7eef46a75bd162e9fca112a51/.venv/bin\nInstalling pyrcc5 script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_bf75c8c72a52b2b7eef46a75bd162e9fca112a51/.venv/bin\nInstalling pyuic5 script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_bf75c8c72a52b2b7eef46a75bd162e9fca112a51/.venv/bin\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_bf75c8c72a52b2b7eef46a75bd162e9fca112a51/.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_bf75c8c72a52b2b7eef46a75bd162e9fca112a51/.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_bf75c8c72a52b2b7eef46a75bd162e9fca112a51/.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_bf75c8c72a52b2b7eef46a75bd162e9fca112a51/.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_bf75c8c72a52b2b7eef46a75bd162e9fca112a51/.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_bf75c8c72a52b2b7eef46a75bd162e9fca112a51/.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_bf75c8c72a52b2b7eef46a75bd162e9fca112a51/.venv/bin\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_bf75c8c72a52b2b7eef46a75bd162e9fca112a51/.venv/lib/python3.7/site-packages\nSearching for pyzmq==26.2.0\nBest match: pyzmq 26.2.0\nAdding pyzmq 26.2.0 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_bf75c8c72a52b2b7eef46a75bd162e9fca112a51/.venv/lib/python3.7/site-packages\nSearching for traitlets==5.9.0\nBest match: traitlets 5.9.0\nAdding traitlets 5.9.0 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_bf75c8c72a52b2b7eef46a75bd162e9fca112a51/.venv/lib/python3.7/site-packages\nSearching for jupyter-core==4.12.0\nBest match: jupyter-core 4.12.0\nAdding jupyter-core 4.12.0 to easy-install.pth file\nInstalling jupyter script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_bf75c8c72a52b2b7eef46a75bd162e9fca112a51/.venv/bin\nInstalling jupyter-migrate script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_bf75c8c72a52b2b7eef46a75bd162e9fca112a51/.venv/bin\nInstalling jupyter-troubleshoot script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_bf75c8c72a52b2b7eef46a75bd162e9fca112a51/.venv/bin\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_bf75c8c72a52b2b7eef46a75bd162e9fca112a51/.venv/lib/python3.7/site-packages\nSearching for ipython-genutils==0.2.0\nBest match: ipython-genutils 0.2.0\nAdding ipython-genutils 0.2.0 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_bf75c8c72a52b2b7eef46a75bd162e9fca112a51/.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_bf75c8c72a52b2b7eef46a75bd162e9fca112a51/.venv/lib/python3.7/site-packages\nSearching for QtPy==2.4.2\nBest match: QtPy 2.4.2\nAdding QtPy 2.4.2 to easy-install.pth file\nInstalling qtpy script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_bf75c8c72a52b2b7eef46a75bd162e9fca112a51/.venv/bin\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_bf75c8c72a52b2b7eef46a75bd162e9fca112a51/.venv/lib/python3.7/site-packages\nSearching for ipykernel==6.16.2\nBest match: ipykernel 6.16.2\nAdding ipykernel 6.16.2 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_bf75c8c72a52b2b7eef46a75bd162e9fca112a51/.venv/lib/python3.7/site-packages\nSearching for jupyter-client==7.4.9\nBest match: jupyter-client 7.4.9\nAdding jupyter-client 7.4.9 to easy-install.pth file\nInstalling jupyter-kernel script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_bf75c8c72a52b2b7eef46a75bd162e9fca112a51/.venv/bin\nInstalling jupyter-kernelspec script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_bf75c8c72a52b2b7eef46a75bd162e9fca112a51/.venv/bin\nInstalling jupyter-run script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_bf75c8c72a52b2b7eef46a75bd162e9fca112a51/.venv/bin\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_bf75c8c72a52b2b7eef46a75bd162e9fca112a51/.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_bf75c8c72a52b2b7eef46a75bd162e9fca112a51/.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_bf75c8c72a52b2b7eef46a75bd162e9fca112a51/.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_bf75c8c72a52b2b7eef46a75bd162e9fca112a51/.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_bf75c8c72a52b2b7eef46a75bd162e9fca112a51/.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_bf75c8c72a52b2b7eef46a75bd162e9fca112a51/.venv/bin\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_bf75c8c72a52b2b7eef46a75bd162e9fca112a51/.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_bf75c8c72a52b2b7eef46a75bd162e9fca112a51/.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_bf75c8c72a52b2b7eef46a75bd162e9fca112a51/.venv/bin\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_bf75c8c72a52b2b7eef46a75bd162e9fca112a51/.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_bf75c8c72a52b2b7eef46a75bd162e9fca112a51/.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_bf75c8c72a52b2b7eef46a75bd162e9fca112a51/.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_bf75c8c72a52b2b7eef46a75bd162e9fca112a51/.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_bf75c8c72a52b2b7eef46a75bd162e9fca112a51/.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_bf75c8c72a52b2b7eef46a75bd162e9fca112a51/.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_bf75c8c72a52b2b7eef46a75bd162e9fca112a51/.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_bf75c8c72a52b2b7eef46a75bd162e9fca112a51/.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_bf75c8c72a52b2b7eef46a75bd162e9fca112a51/.venv/bin\nInstalling pyftmerge script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_bf75c8c72a52b2b7eef46a75bd162e9fca112a51/.venv/bin\nInstalling pyftsubset script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_bf75c8c72a52b2b7eef46a75bd162e9fca112a51/.venv/bin\nInstalling ttx script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_bf75c8c72a52b2b7eef46a75bd162e9fca112a51/.venv/bin\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_bf75c8c72a52b2b7eef46a75bd162e9fca112a51/.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_bf75c8c72a52b2b7eef46a75bd162e9fca112a51/.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_bf75c8c72a52b2b7eef46a75bd162e9fca112a51/.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_bf75c8c72a52b2b7eef46a75bd162e9fca112a51/.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_bf75c8c72a52b2b7eef46a75bd162e9fca112a51/.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_bf75c8c72a52b2b7eef46a75bd162e9fca112a51/.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_bf75c8c72a52b2b7eef46a75bd162e9fca112a51/.venv/lib/python3.7/site-packages\nSearching for PyQt5-sip==12.13.0\nBest match: PyQt5-sip 12.13.0\nAdding PyQt5-sip 12.13.0 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_bf75c8c72a52b2b7eef46a75bd162e9fca112a51/.venv/lib/python3.7/site-packages\nSearching for PyQtWebEngine-Qt5==5.15.16\nBest match: PyQtWebEngine-Qt5 5.15.16\nAdding PyQtWebEngine-Qt5 5.15.16 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_bf75c8c72a52b2b7eef46a75bd162e9fca112a51/.venv/lib/python3.7/site-packages\nSearching for PyQt5-Qt5==5.15.16\nBest match: PyQt5-Qt5 5.15.16\nAdding PyQt5-Qt5 5.15.16 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_bf75c8c72a52b2b7eef46a75bd162e9fca112a51/.venv/lib/python3.7/site-packages\nSearching for tornado==6.2\nBest match: tornado 6.2\nAdding tornado 6.2 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_bf75c8c72a52b2b7eef46a75bd162e9fca112a51/.venv/lib/python3.7/site-packages\nSearching for ipython==7.34.0\nBest match: ipython 7.34.0\nAdding ipython 7.34.0 to easy-install.pth file\nInstalling iptest script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_bf75c8c72a52b2b7eef46a75bd162e9fca112a51/.venv/bin\nInstalling iptest3 script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_bf75c8c72a52b2b7eef46a75bd162e9fca112a51/.venv/bin\nInstalling ipython script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_bf75c8c72a52b2b7eef46a75bd162e9fca112a51/.venv/bin\nInstalling ipython3 script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_bf75c8c72a52b2b7eef46a75bd162e9fca112a51/.venv/bin\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_bf75c8c72a52b2b7eef46a75bd162e9fca112a51/.venv/lib/python3.7/site-packages\nSearching for nest-asyncio==1.6.0\nBest match: nest-asyncio 1.6.0\nAdding nest-asyncio 1.6.0 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_bf75c8c72a52b2b7eef46a75bd162e9fca112a51/.venv/lib/python3.7/site-packages\nSearching for debugpy==1.7.0\nBest match: debugpy 1.7.0\nAdding debugpy 1.7.0 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_bf75c8c72a52b2b7eef46a75bd162e9fca112a51/.venv/lib/python3.7/site-packages\nSearching for psutil==6.1.1\nBest match: psutil 6.1.1\nAdding psutil 6.1.1 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_bf75c8c72a52b2b7eef46a75bd162e9fca112a51/.venv/lib/python3.7/site-packages\nSearching for matplotlib-inline==0.1.6\nBest match: matplotlib-inline 0.1.6\nAdding matplotlib-inline 0.1.6 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_bf75c8c72a52b2b7eef46a75bd162e9fca112a51/.venv/lib/python3.7/site-packages\nSearching for entrypoints==0.4\nBest match: entrypoints 0.4\nAdding entrypoints 0.4 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_bf75c8c72a52b2b7eef46a75bd162e9fca112a51/.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_bf75c8c72a52b2b7eef46a75bd162e9fca112a51/.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_bf75c8c72a52b2b7eef46a75bd162e9fca112a51/.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_bf75c8c72a52b2b7eef46a75bd162e9fca112a51/.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_bf75c8c72a52b2b7eef46a75bd162e9fca112a51/.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_bf75c8c72a52b2b7eef46a75bd162e9fca112a51/.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_bf75c8c72a52b2b7eef46a75bd162e9fca112a51/.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_bf75c8c72a52b2b7eef46a75bd162e9fca112a51/.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_bf75c8c72a52b2b7eef46a75bd162e9fca112a51/.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_bf75c8c72a52b2b7eef46a75bd162e9fca112a51/.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_bf75c8c72a52b2b7eef46a75bd162e9fca112a51/.venv/lib/python3.7/site-packages\nSearching for prompt-toolkit==3.0.48\nBest match: prompt-toolkit 3.0.48\nAdding prompt-toolkit 3.0.48 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_bf75c8c72a52b2b7eef46a75bd162e9fca112a51/.venv/lib/python3.7/site-packages\nSearching for backcall==0.2.0\nBest match: backcall 0.2.0\nAdding backcall 0.2.0 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_bf75c8c72a52b2b7eef46a75bd162e9fca112a51/.venv/lib/python3.7/site-packages\nSearching for jedi==0.19.2\nBest match: jedi 0.19.2\nAdding jedi 0.19.2 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_bf75c8c72a52b2b7eef46a75bd162e9fca112a51/.venv/lib/python3.7/site-packages\nSearching for pickleshare==0.7.5\nBest match: pickleshare 0.7.5\nAdding pickleshare 0.7.5 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_bf75c8c72a52b2b7eef46a75bd162e9fca112a51/.venv/lib/python3.7/site-packages\nSearching for decorator==5.1.1\nBest match: decorator 5.1.1\nAdding decorator 5.1.1 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_bf75c8c72a52b2b7eef46a75bd162e9fca112a51/.venv/lib/python3.7/site-packages\nSearching for pexpect==4.9.0\nBest match: pexpect 4.9.0\nAdding pexpect 4.9.0 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_bf75c8c72a52b2b7eef46a75bd162e9fca112a51/.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_bf75c8c72a52b2b7eef46a75bd162e9fca112a51/.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_bf75c8c72a52b2b7eef46a75bd162e9fca112a51/.venv/lib/python3.7/site-packages\nSearching for wcwidth==0.2.13\nBest match: wcwidth 0.2.13\nAdding wcwidth 0.2.13 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_bf75c8c72a52b2b7eef46a75bd162e9fca112a51/.venv/lib/python3.7/site-packages\nSearching for parso==0.8.4\nBest match: parso 0.8.4\nAdding parso 0.8.4 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_bf75c8c72a52b2b7eef46a75bd162e9fca112a51/.venv/lib/python3.7/site-packages\nSearching for ptyprocess==0.7.0\nBest match: ptyprocess 0.7.0\nAdding ptyprocess 0.7.0 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_bf75c8c72a52b2b7eef46a75bd162e9fca112a51/.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_bf75c8c72a52b2b7eef46a75bd162e9fca112a51/.venv/lib/python3.7/site-packages\nFinished processing dependencies for Orange3==3.33.0.dev0+bf75c8c\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 133ms\nPrepared 18 packages in 3ms\nInstalled 18 packages in 161ms\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 267ms\nInstalled 47 packages in 629ms\n + anyio==3.7.1\n + baycomp==1.0.3\n + bottleneck==1.4.0\n + certifi==2024.12.14\n + cffi==1.15.1\n + chardet==5.2.0\n + charset-normalizer==3.4.1\n + cryptography==44.0.0\n + cycler==0.11.0\n + et-xmlfile==1.1.0\n + fonttools==4.38.0\n + h11==0.14.0\n + httpcore==0.17.3\n + httpx==0.24.1\n + idna==3.10\n + importlib-resources==5.12.0\n + jaraco-classes==3.2.3\n + jeepney==0.8.0\n + joblib==1.3.2\n + keyring==24.1.1\n + keyrings-alt==4.2.0\n + kiwisolver==1.4.5\n + matplotlib==3.5.3\n + more-itertools==9.1.0\n + networkx==2.6.3\n + openpyxl==3.1.3\n + opentsne==1.0.1\n + pandas==1.3.5\n + pillow==9.5.0\n + pip==24.0\n + pycparser==2.21\n + pyparsing==3.1.4\n + python-dateutil==2.9.0.post0\n + python-louvain==0.16\n + pytz==2024.2\n + pyyaml==6.0.1\n + requests==2.31.0\n + scikit-learn==1.0.2\n + scipy==1.7.3\n + secretstorage==3.3.3\n + serverfiles==0.3.1\n + six==1.17.0\n + sniffio==1.3.1\n + threadpoolctl==3.1.0\n + urllib3==2.0.7\n + xlrd==2.0.1\n + xlsxwriter==3.2.0\nResolved 59 packages in 285ms\nInstalled 38 packages in 410ms\n + anyqt==0.2.0\n + attrs==24.2.0\n + backcall==0.2.0\n + cattrs==23.1.2\n + commonmark==0.9.1\n + debugpy==1.7.0\n + decorator==5.1.1\n + dictdiffer==0.9.0\n + docutils==0.20.1\n + entrypoints==0.4\n + ipykernel==6.16.2\n + ipython==7.34.0\n + ipython-genutils==0.2.0\n + jedi==0.19.2\n + jupyter-client==7.4.9\n + jupyter-core==4.12.0\n + matplotlib-inline==0.1.6\n + nest-asyncio==1.6.0\n + orange-canvas-core==0.1.35\n + orange-widget-base==4.22.0\n + parso==0.8.4\n + pexpect==4.9.0\n + pickleshare==0.7.5\n + platformdirs==4.0.0\n + prompt-toolkit==3.0.48\n + psutil==6.1.1\n + ptyprocess==0.7.0\n + pygments==2.17.2\n + pyqtgraph==0.12.3\n + pyzmq==26.2.0\n + qasync==0.25.0\n + qtconsole==5.4.4\n + qtpy==2.4.2\n + requests-cache==1.1.1\n + tornado==6.2\n + traitlets==5.9.0\n + url-normalize==1.4.3\n + wcwidth==0.2.13\nResolved 2 packages in 3ms\nInstalled 2 packages in 6ms\n + psycopg2==2.9.9\n + pymssql==2.3.2\nResolved 19 packages in 28ms\nInstalled 5 packages in 1.14s\n + catboost==1.2.7\n + graphviz==0.20.1\n + plotly==5.18.0\n + tenacity==8.2.3\n + xgboost==1.6.2\nAudited 2 packages in 1ms\nIn file included from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_bf75c8c72a52b2b7eef46a75bd162e9fca112a51/.venv/lib/python3.7/site-packages/numpy/core/include/numpy/ndarraytypes.h:1830,\n                 from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_bf75c8c72a52b2b7eef46a75bd162e9fca112a51/.venv/lib/python3.7/site-packages/numpy/core/include/numpy/ndarrayobject.h:12,\n                 from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_bf75c8c72a52b2b7eef46a75bd162e9fca112a51/.venv/lib/python3.7/site-packages/numpy/core/include/numpy/arrayobject.h:4,\n                 from Orange/classification/_tree_scorers.c:775:\n/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_bf75c8c72a52b2b7eef46a75bd162e9fca112a51/.venv/lib/python3.7/site-packages/numpy/core/include/numpy/npy_1_7_deprecated_api.h:17:2: warning: #warning \"Using deprecated NumPy API, disable it with \" \"#define NPY_NO_DEPRECATED_API NPY_1_7_API_VERSION\" [-Wcpp]\n   17 | #warning \"Using deprecated NumPy API, disable it with \" \\\n      |  ^~~~~~~\nOrange/classification/_tree_scorers.c: In function ‘__pyx_pf_6Orange_14classification_13_tree_scorers_2find_threshold_entropy’:\nOrange/classification/_tree_scorers.c:3427:27: warning: comparison of integer expressions of different signedness: ‘unsigned int’ and ‘int’ [-Wsign-compare]\n 3427 |   __pyx_t_7 = ((__pyx_v_N <= __pyx_v_min_leaf) != 0);\n      |                           ^~\nOrange/classification/_tree_scorers.c: In function ‘__pyx_pf_6Orange_14classification_13_tree_scorers_6find_threshold_MSE’:\nOrange/classification/_tree_scorers.c:4883:27: warning: comparison of integer expressions of different signedness: ‘unsigned int’ and ‘int’ [-Wsign-compare]\n 4883 |   __pyx_t_1 = ((__pyx_v_N <= __pyx_v_min_leaf) != 0);\n      |                           ^~\nOrange/classification/_tree_scorers.c: In function ‘__pyx_pf_6Orange_14classification_13_tree_scorers_8find_binarization_MSE’:\nOrange/classification/_tree_scorers.c:5629:43: warning: comparison of integer expressions of different signedness: ‘unsigned int’ and ‘int’ [-Wsign-compare]\n 5629 |           for (__pyx_t_19 = 0; __pyx_t_19 < __pyx_t_18; __pyx_t_19+=1) {\n      |                                           ^\nOrange/classification/_tree_scorers.c:5750:39: warning: comparison of integer expressions of different signedness: ‘unsigned int’ and ‘int’ [-Wsign-compare]\n 5750 |           __pyx_t_20 = ((__pyx_v_left >= __pyx_v_min_leaf) != 0);\n      |                                       ^~\nOrange/classification/_tree_scorers.c:5756:53: warning: comparison of integer expressions of different signedness: ‘unsigned int’ and ‘int’ [-Wsign-compare]\n 5756 |           __pyx_t_20 = (((__pyx_v_N - __pyx_v_left) >= __pyx_v_min_leaf) != 0);\n      |                                                     ^~\nOrange/classification/_tree_scorers.c: In function ‘__pyx_pf_6Orange_14classification_13_tree_scorers_14compute_predictions_csr’:\nOrange/classification/_tree_scorers.c:7217:41: warning: comparison of integer expressions of different signedness: ‘unsigned int’ and ‘int’ [-Wsign-compare]\n 7217 |         for (__pyx_t_12 = 0; __pyx_t_12 < __pyx_t_11; __pyx_t_12+=1) {\n      |                                         ^\nOrange/classification/_tree_scorers.c: In function ‘__pyx_pf_6Orange_14classification_13_tree_scorers_16compute_predictions_csc’:\nOrange/classification/_tree_scorers.c:7874:41: warning: comparison of integer expressions of different signedness: ‘unsigned int’ and ‘int’ [-Wsign-compare]\n 7874 |         for (__pyx_t_12 = 0; __pyx_t_12 < __pyx_t_11; __pyx_t_12+=1) {\n      |                                         ^\nOrange/classification/_tree_scorers.c:7934:150: warning: comparison of integer expressions of different signedness: ‘__pyx_t_5numpy_int32_t’ {aka ‘int’} and ‘unsigned int’ [-Wsign-compare]\n 7934 |               __pyx_t_16 = (((*((__pyx_t_5numpy_int32_t const  *) ( /* dim=0 */ (__pyx_v_indices.data + __pyx_t_15 * __pyx_v_indices.strides[0]) ))) != __pyx_v_i) != 0);\n      |                                                                                                                                                      ^~\nIn file included from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_bf75c8c72a52b2b7eef46a75bd162e9fca112a51/.venv/lib/python3.7/site-packages/numpy/core/include/numpy/ndarraytypes.h:1830,\n                 from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_bf75c8c72a52b2b7eef46a75bd162e9fca112a51/.venv/lib/python3.7/site-packages/numpy/core/include/numpy/ndarrayobject.h:12,\n                 from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_bf75c8c72a52b2b7eef46a75bd162e9fca112a51/.venv/lib/python3.7/site-packages/numpy/core/include/numpy/arrayobject.h:4,\n                 from Orange/data/_contingency.c:775:\n/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_bf75c8c72a52b2b7eef46a75bd162e9fca112a51/.venv/lib/python3.7/site-packages/numpy/core/include/numpy/npy_1_7_deprecated_api.h:17:2: warning: #warning \"Using deprecated NumPy API, disable it with \" \"#define NPY_NO_DEPRECATED_API NPY_1_7_API_VERSION\" [-Wcpp]\n   17 | #warning \"Using deprecated NumPy API, disable it with \" \\\n      |  ^~~~~~~\nOrange/data/_io.c: In function ‘__pyx_pf_6Orange_4data_3_io_sparse_read_float.constprop’:\nOrange/data/_io.c:4120:43: warning: ‘__pyx_v_decs’ may be used uninitialized in this function [-Wmaybe-uninitialized]\n 4120 |           __pyx_v_value = ((__pyx_v_value * __pyx_v_decs) + (__pyx_v_c & 0xf));\n      |                            ~~~~~~~~~~~~~~~^~~~~~~~~~~~~~~\nOrange/data/_io.c:4318:24: warning: ‘__pyx_v_value’ may be used uninitialized in this function [-Wmaybe-uninitialized]\n 4318 |           __pyx_t_10 = PyFloat_FromDouble(__pyx_v_value); if (unlikely(!__pyx_t_10)) __PYX_ERR(0, 223, __pyx_L5_error)\n      |                        ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\nOrange/data/_io.c:683:40: warning: ‘__pyx_v_attr_index’ may be used uninitialized in this function [-Wmaybe-uninitialized]\n  683 |   #define PyInt_FromLong               PyLong_FromLong\n      |                                        ^~~~~~~~~~~~~~~\nOrange/data/_io.c:1590:7: note: ‘__pyx_v_attr_index’ was declared here\n 1590 |   int __pyx_v_attr_index;\n      |       ^~~~~~~~~~~~~~~~~~\nOrange/data/_io.c:3183:25: warning: ‘__pyx_v_atomp’ may be used uninitialized in this function [-Wmaybe-uninitialized]\n 3183 |           __pyx_v_atomp = (__pyx_v_atomp + 1);\n      |           ~~~~~~~~~~~~~~^~~~~~~~~~~~~~~~~~~~~\nIn file included from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_bf75c8c72a52b2b7eef46a75bd162e9fca112a51/.venv/lib/python3.7/site-packages/numpy/core/include/numpy/ndarraytypes.h:1830,\n                 from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_bf75c8c72a52b2b7eef46a75bd162e9fca112a51/.venv/lib/python3.7/site-packages/numpy/core/include/numpy/ndarrayobject.h:12,\n                 from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_bf75c8c72a52b2b7eef46a75bd162e9fca112a51/.venv/lib/python3.7/site-packages/numpy/core/include/numpy/arrayobject.h:4,\n                 from Orange/data/_valuecount.c:775:\n/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_bf75c8c72a52b2b7eef46a75bd162e9fca112a51/.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_bf75c8c72a52b2b7eef46a75bd162e9fca112a51/.venv/lib/python3.7/site-packages/numpy/core/include/numpy/ndarraytypes.h:1830,\n                 from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_bf75c8c72a52b2b7eef46a75bd162e9fca112a51/.venv/lib/python3.7/site-packages/numpy/core/include/numpy/ndarrayobject.h:12,\n                 from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_bf75c8c72a52b2b7eef46a75bd162e9fca112a51/.venv/lib/python3.7/site-packages/numpy/core/include/numpy/arrayobject.h:4,\n                 from Orange/distance/_distance.c:775:\n/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_bf75c8c72a52b2b7eef46a75bd162e9fca112a51/.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_bf75c8c72a52b2b7eef46a75bd162e9fca112a51/.venv/lib/python3.7/site-packages/numpy/core/include/numpy/ndarraytypes.h:1830,\n                 from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_bf75c8c72a52b2b7eef46a75bd162e9fca112a51/.venv/lib/python3.7/site-packages/numpy/core/include/numpy/ndarrayobject.h:12,\n                 from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_bf75c8c72a52b2b7eef46a75bd162e9fca112a51/.venv/lib/python3.7/site-packages/numpy/core/include/numpy/arrayobject.h:4,\n                 from Orange/preprocess/_discretize.c:774:\n/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_bf75c8c72a52b2b7eef46a75bd162e9fca112a51/.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_bf75c8c72a52b2b7eef46a75bd162e9fca112a51/.venv/lib/python3.7/site-packages/numpy/core/include/numpy/ndarraytypes.h:1830,\n                 from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_bf75c8c72a52b2b7eef46a75bd162e9fca112a51/.venv/lib/python3.7/site-packages/numpy/core/include/numpy/ndarrayobject.h:12,\n                 from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_bf75c8c72a52b2b7eef46a75bd162e9fca112a51/.venv/lib/python3.7/site-packages/numpy/core/include/numpy/arrayobject.h:4,\n                 from Orange/preprocess/_relieff.cpp:790:\n/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_bf75c8c72a52b2b7eef46a75bd162e9fca112a51/.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_bf75c8c72a52b2b7eef46a75bd162e9fca112a51/.venv/lib/python3.7/site-packages/numpy/core/include/numpy/ndarraytypes.h:1830,\n                 from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_bf75c8c72a52b2b7eef46a75bd162e9fca112a51/.venv/lib/python3.7/site-packages/numpy/core/include/numpy/ndarrayobject.h:12,\n                 from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_bf75c8c72a52b2b7eef46a75bd162e9fca112a51/.venv/lib/python3.7/site-packages/numpy/core/include/numpy/arrayobject.h:4,\n                 from Orange/projection/_som.c:775:\n/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_bf75c8c72a52b2b7eef46a75bd162e9fca112a51/.venv/lib/python3.7/site-packages/numpy/core/include/numpy/npy_1_7_deprecated_api.h:17:2: warning: #warning \"Using deprecated NumPy API, disable it with \" \"#define NPY_NO_DEPRECATED_API NPY_1_7_API_VERSION\" [-Wcpp]\n   17 | #warning \"Using deprecated NumPy API, disable it with \" \\\n      |  ^~~~~~~\nOrange/projection/_som.c: In function ‘__pyx_pw_6Orange_10projection_4_som_5update_hex’:\nOrange/projection/_som.c:3863:7: warning: ‘__pyx_v_win_y’ may be used uninitialized in this function [-Wmaybe-uninitialized]\n 3863 |   int __pyx_v_win_y;\n      |       ^~~~~~~~~~~~~\nOrange/projection/_som.c:3862:7: warning: ‘__pyx_v_win_x’ may be used uninitialized in this function [-Wmaybe-uninitialized]\n 3862 |   int __pyx_v_win_x;\n      |       ^~~~~~~~~~~~~\nOrange/projection/_som.c: In function ‘__pyx_pf_6Orange_10projection_4_som_8update_sparse.constprop’:\nOrange/projection/_som.c:4934:7: warning: ‘__pyx_v_win_y’ may be used uninitialized in this function [-Wmaybe-uninitialized]\n 4934 |   int __pyx_v_win_y;\n      |       ^~~~~~~~~~~~~\nOrange/projection/_som.c:4933:7: warning: ‘__pyx_v_win_x’ may be used uninitialized in this function [-Wmaybe-uninitialized]\n 4933 |   int __pyx_v_win_x;\n      |       ^~~~~~~~~~~~~\nOrange/projection/_som.c: In function ‘__pyx_pw_6Orange_10projection_4_som_7get_winners_sparse’:\nOrange/projection/_som.c:4732:223: warning: ‘__pyx_v_win_y’ may be used uninitialized in this function [-Wmaybe-uninitialized]\n 4732 |           *__Pyx_BufPtrStrided2d(__pyx_t_5numpy_int16_t *, __pyx_pybuffernd_winners.rcbuffer->pybuffer.buf, __pyx_t_26, __pyx_pybuffernd_winners.diminfo[0].strides, __pyx_t_27, __pyx_pybuffernd_winners.diminfo[1].strides) = __pyx_v_win_y;\n      |           ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^~~~~~~~~~~~~~~\nOrange/projection/_som.c:4307:7: note: ‘__pyx_v_win_y’ was declared here\n 4307 |   int __pyx_v_win_y;\n      |       ^~~~~~~~~~~~~\nOrange/projection/_som.c:4721:223: warning: ‘__pyx_v_win_x’ may be used uninitialized in this function [-Wmaybe-uninitialized]\n 4721 |           *__Pyx_BufPtrStrided2d(__pyx_t_5numpy_int16_t *, __pyx_pybuffernd_winners.rcbuffer->pybuffer.buf, __pyx_t_27, __pyx_pybuffernd_winners.diminfo[0].strides, __pyx_t_26, __pyx_pybuffernd_winners.diminfo[1].strides) = __pyx_v_win_x;\n      |           ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^~~~~~~~~~~~~~~\nOrange/projection/_som.c:4306:7: note: ‘__pyx_v_win_x’ was declared here\n 4306 |   int __pyx_v_win_x;\n      |       ^~~~~~~~~~~~~\nOrange/projection/_som.c: In function ‘__pyx_pf_6Orange_10projection_4_som_10update_sparse_hex.constprop’:\nOrange/projection/_som.c:5682:7: warning: ‘__pyx_v_win_y’ may be used uninitialized in this function [-Wmaybe-uninitialized]\n 5682 |   int __pyx_v_win_y;\n      |       ^~~~~~~~~~~~~\nOrange/projection/_som.c:5681:7: warning: ‘__pyx_v_win_x’ may be used uninitialized in this function [-Wmaybe-uninitialized]\n 5681 |   int __pyx_v_win_x;\n      |       ^~~~~~~~~~~~~\nOrange/projection/_som.c: In function ‘__pyx_pw_6Orange_10projection_4_som_1get_winners’:\nOrange/projection/_som.c:3184:223: warning: ‘__pyx_v_win_y’ may be used uninitialized in this function [-Wmaybe-uninitialized]\n 3184 |           *__Pyx_BufPtrStrided2d(__pyx_t_5numpy_int16_t *, __pyx_pybuffernd_winners.rcbuffer->pybuffer.buf, __pyx_t_22, __pyx_pybuffernd_winners.diminfo[0].strides, __pyx_t_23, __pyx_pybuffernd_winners.diminfo[1].strides) = __pyx_v_win_y;\n      |           ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^~~~~~~~~~~~~~~\nOrange/projection/_som.c:2887:7: note: ‘__pyx_v_win_y’ was declared here\n 2887 |   int __pyx_v_win_y;\n      |       ^~~~~~~~~~~~~\nOrange/projection/_som.c:3173:223: warning: ‘__pyx_v_win_x’ may be used uninitialized in this function [-Wmaybe-uninitialized]\n 3173 |           *__Pyx_BufPtrStrided2d(__pyx_t_5numpy_int16_t *, __pyx_pybuffernd_winners.rcbuffer->pybuffer.buf, __pyx_t_23, __pyx_pybuffernd_winners.diminfo[0].strides, __pyx_t_22, __pyx_pybuffernd_winners.diminfo[1].strides) = __pyx_v_win_x;\n      |           ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^~~~~~~~~~~~~~~\nOrange/projection/_som.c:2886:7: note: ‘__pyx_v_win_x’ was declared here\n 2886 |   int __pyx_v_win_x;\n      |       ^~~~~~~~~~~~~\nOrange/projection/_som.c: In function ‘__pyx_pw_6Orange_10projection_4_som_3update’:\nOrange/projection/_som.c:3357:7: warning: ‘__pyx_v_win_y’ may be used uninitialized in this function [-Wmaybe-uninitialized]\n 3357 |   int __pyx_v_win_y;\n      |       ^~~~~~~~~~~~~\nOrange/classification/_simple_tree.c: In function ‘gain_ratio_c’:\nOrange/classification/_simple_tree.c:63:9: warning: implicit declaration of function ‘qsort_r’; did you mean ‘qsort’? [-Wimplicit-function-declaration]\n   63 |         qsort_r(base, nel, size, compar, thunk)\n      |         ^~~~~~~\nOrange/classification/_simple_tree.c:147:9: note: in expansion of macro ‘QSORT_R_FUNC’\n  147 |         QSORT_R_FUNC(examples, size, sizeof(struct Example), (void*) &compar_attr, compar_examples);\n      |         ^~~~~~~~~~~~\n/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_bf75c8c72a52b2b7eef46a75bd162e9fca112a51/.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_bf75c8c72a52b2b7eef46a75bd162e9fca112a51/.venv/lib/python3.7/site-packages/setuptools/command/install.py:37: SetuptoolsDeprecationWarning: setup.py install is deprecated. Use build and pip and other standards-based tools.\n  setuptools.SetuptoolsDeprecationWarning,\nwarning: no files found matching 'README*' under directory 'Orange'\nwarning: no files found matching 'LICENSE' under directory 'Orange'\nwarning: no files found matching '*.basket' under directory 'Orange/datasets'\nwarning: no files found matching '*.svg' under directory 'Orange/canvas'\nwarning: no files found matching '*.js' under directory 'Orange/widgets'\nwarning: no files found matching '*.css' under directory 'Orange/widgets'\nwarning: no files found matching '*.html' under directory 'Orange/widgets'\nwarning: no files found matching '*.svg' under directory 'distribute'\nwarning: no files found matching 'COPYING'\n",
    "new_commit_res_code": 0,
    "new_commit_res_stdout": "============================= test session starts ==============================\nplatform linux -- Python 3.7.9, pytest-7.4.4, pluggy-1.2.0\nrootdir: /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_bf75c8c72a52b2b7eef46a75bd162e9fca112a51\nplugins: anyio-3.7.1\ncollected 33 items\n\nr2e_tests/test_1.py sss..............................                    [100%]\n\n==================================== PASSES ====================================\n________________ TestOWHeatMap.test_centering_threshold_change _________________\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_________________ 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\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_col_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\n_______________ TestOWHeatMap.test_col_color_annotations_with_na _______________\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_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\n_____________________ TestOWHeatMap.test_data_with_hidden ______________________\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______________________ 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\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\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\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\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_row_color_annotations_with_na _______________\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_column_key ____________________\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\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_column_key_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\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_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\n=========================== short test summary info ============================\nPASSED r2e_tests/test_1.py::TestOWHeatMap::test_centering_threshold_change\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_col_color_annotations\nPASSED r2e_tests/test_1.py::TestOWHeatMap::test_col_color_annotations_with_na\nPASSED r2e_tests/test_1.py::TestOWHeatMap::test_data_column_nans\nPASSED r2e_tests/test_1.py::TestOWHeatMap::test_data_with_hidden\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_row_color_annotations_with_na\nPASSED r2e_tests/test_1.py::TestOWHeatMap::test_saved_selection\nPASSED r2e_tests/test_1.py::TestOWHeatMap::test_saved_selection_when_not_possible\nPASSED r2e_tests/test_1.py::TestOWHeatMap::test_set_split_column_key\nPASSED r2e_tests/test_1.py::TestOWHeatMap::test_set_split_column_key_missing\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\n======================== 30 passed, 3 skipped in 10.56s ========================\nWidgets left: 0    Max widgets: 6220 \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_bf75c8c72a52b2b7eef46a75bd162e9fca112a51\nplugins: anyio-3.7.1\ncollected 33 items\n\nr2e_tests/test_1.py sss..........F...................                    [100%]\n\n=================================== FAILURES ===================================\n____________________ TestOWHeatMap.test_information_message ____________________\n\nself = <r2e_tests.test_1.TestOWHeatMap testMethod=test_information_message>\n\n    def test_information_message(self):\n        self.widget.set_row_clustering(Clustering.OrderedClustering)\n        self.widget.MaxClustering = 20\n        self.widget.MaxOrderedClustering = 15\n        data = self.brown_selected[:, :10]\n        self.send_signal(self.widget.Inputs.data, data[:15])\n        self.assertFalse(self.widget.Information.active)\n        self.send_signal(self.widget.Inputs.data, data[:16])\n        self.assertTrue(self.widget.Information.active)\n        self.assertEqual(self.widget.row_clustering, Clustering.Clustering)\n        self.send_signal(self.widget.Inputs.data, data[:20])\n        self.assertFalse(self.widget.Information.active)\n        self.send_signal(self.widget.Inputs.data, data[:21])\n        self.assertTrue(self.widget.Information.active)\n        data = Table(\"heart_disease.tab\")[:10]\n        self.send_signal(self.widget.Inputs.data, data)\n>       self.assertTrue(self.widget.Information.discrete_ignored.is_shown())\nE       AssertionError: False is not true\n\nr2e_tests/test_1.py:77: 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\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==================================== PASSES ====================================\n________________ TestOWHeatMap.test_centering_threshold_change _________________\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_________________ 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\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_col_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\n_______________ TestOWHeatMap.test_col_color_annotations_with_na _______________\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_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\n_____________________ TestOWHeatMap.test_data_with_hidden ______________________\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______________________ 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\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_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\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\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_row_color_annotations_with_na _______________\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_column_key ____________________\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\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_column_key_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\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_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\n=========================== short test summary info ============================\nPASSED r2e_tests/test_1.py::TestOWHeatMap::test_centering_threshold_change\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_col_color_annotations\nPASSED r2e_tests/test_1.py::TestOWHeatMap::test_col_color_annotations_with_na\nPASSED r2e_tests/test_1.py::TestOWHeatMap::test_data_column_nans\nPASSED r2e_tests/test_1.py::TestOWHeatMap::test_data_with_hidden\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_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_row_color_annotations_with_na\nPASSED r2e_tests/test_1.py::TestOWHeatMap::test_saved_selection\nPASSED r2e_tests/test_1.py::TestOWHeatMap::test_saved_selection_when_not_possible\nPASSED r2e_tests/test_1.py::TestOWHeatMap::test_set_split_column_key\nPASSED r2e_tests/test_1.py::TestOWHeatMap::test_set_split_column_key_missing\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_information_message - Asserti...\n=================== 1 failed, 29 passed, 3 skipped in 7.95s ====================\nWidgets left: 0    Max widgets: 6220 \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: Warning Not Displayed When Discrete Data is Provided in HeatMap Widget

**Description**
When discrete data is input into the HeatMap widget, it is expected to display an information message indicating that the discrete data is being ignored. However, the widget does not show this warning, which can lead to confusion for users who are unaware that their discrete data is not being processed.

**Example Buggy Code**
```python
data = Table("heart_disease.tab")[:10]
widget.send_signal(widget.Inputs.data, data)
assert widget.Information.discrete_ignored.is_shown()
```
In this example, after sending discrete data to the widget, the information message `discrete_ignored` should be displayed. However, the assertion fails because the message is not shown.

**Expected Behavior**
The HeatMap widget should display an information message notifying the user that discrete data has been ignored when such data is provided.

**Actual Behavior**
No information message is displayed when discrete data is input into the HeatMap widget, leaving users unaware that their discrete data is not being utilized.

[/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 collections import defaultdict
from itertools import islice
from typing import (
    Iterable, Mapping, Any, TypeVar, NamedTuple, Sequence, Optional,
    Union, Tuple, List, Callable
)

import numpy as np
import scipy.sparse as sp

from AnyQt.QtWidgets import (
    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, \
    ContinuousVariable
from Orange.data.sql.table import SqlTable
import Orange.distance

from Orange.clustering import hierarchical, kmeans
from Orange.widgets.utils import colorpalettes, apply_all, enum_get, itemmodels
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.graphicsscene import GraphicsScene
from Orange.widgets.utils.colorpalettes import 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.colorgradientselection import ColorGradientSelection
from Orange.widgets.utils.widgetpreview import WidgetPreview


__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 split_domain(domain: Domain, split_label: str):
    """Split the domain based on values of `split_label` value.
    """
    groups = defaultdict(list)
    for var in domain.attributes:
        val = var.attributes.get(split_label)
        groups[val].append(var)
    if None in groups:
        na = groups.pop(None)
        return [*groups.items(), ("N/A", na)]
    else:
        return list(groups.items())


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


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)
    color_center = settings.Setting(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)
    column_annotation_color_key: Optional[Tuple[str, str]] = settings.ContextSetting(None)

    # Discrete variable used to split that data/heatmaps (vertically)
    split_by_var = settings.ContextSetting(None)
    # Split heatmap columns by 'key' (horizontal)
    split_columns_key: Optional[Tuple[str, str]] = 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"

    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")

    UserAdviceMessages = [
        widget.Message(
            "For data with a meaningful mid-point, "
            "choose one of diverging palettes.",
            "diverging_palette")]

    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(self._save_state_for_serialization)
        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 stripped of discrete features and hidden
        #: attributes, and often merged using k-means
        self.data = None
        self.effective_data = None
        #: Source of column annotations (derived from self.data)
        self.col_annot_data: Optional[Table] = 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 = {}

        # GUI definition
        colorbox = gui.vBox(self.controlArea, "Color")

        self.color_map_widget = cmw = ColorGradientSelection(
            thresholds=(self.threshold_low, self.threshold_high),
            center=self.color_center
        )
        model = itemmodels.ContinuousPalettesModel(parent=self)
        cmw.setModel(model)
        idx = cmw.findData(self.palette_name, model.KeyRole)
        if idx != -1:
            cmw.setCurrentIndex(idx)

        cmw.activated.connect(self.update_color_schema)

        def _set_thresholds(low, high):
            self.threshold_low, self.threshold_high = low, high
            self.update_color_schema()
        cmw.thresholdsChanged.connect(_set_thresholds)

        def _set_centering(center):
            self.color_center = center
            self.update_color_schema()
        cmw.centerChanged.connect(_set_centering)

        colorbox.layout().addWidget(self.color_map_widget)

        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")
        form = QFormLayout(
            formAlignment=Qt.AlignLeft, labelAlignment=Qt.AlignLeft,
            fieldGrowthPolicy=QFormLayout.AllNonFixedFieldsGrow,
        )
        box.layout().addLayout(form)

        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
        )
        self.col_split_model = DomainModel(
            placeholder="(None)",
            order=DomainModel.MIXED,
            valid_types=(Orange.data.DiscreteVariable,),
            parent=self,
        )
        self.col_split_cb = cb = ComboBoxSearch(
            sizeAdjustPolicy=ComboBox.AdjustToMinimumContentsLengthWithIcon,
            minimumContentsLength=14,
            toolTip="Split the heatmap horizontally by column annotation"
        )
        self.col_split_cb.setModel(self.col_split_model)
        self.connect_control(
            "split_columns_var", lambda value, cb=cb: cbselect(cb, value)
        )
        self.split_columns_var = None
        self.col_split_cb.activated.connect(self.__on_split_cols_activated)
        form.addRow("Rows:", self.row_split_cb)
        form.addRow("Columns:", self.col_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)
        gui.separator(box)
        annotbox = QGroupBox("Row Annotations")
        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.AdjustToMinimumContentsLengthWithIcon
        )
        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.AdjustToMinimumContentsLengthWithIcon,
            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)
        annotbox = QGroupBox("Column annotations")
        form = QFormLayout(
            annotbox,
            formAlignment=Qt.AlignLeft,
            labelAlignment=Qt.AlignLeft,
            fieldGrowthPolicy=QFormLayout.AllNonFixedFieldsGrow
        )
        self.col_side_color_model = DomainModel(
            placeholder="(None)",
            valid_types=(DiscreteVariable, ContinuousVariable),
            parent=self
        )
        self.col_side_color_cb = cb = ComboBoxSearch(
            sizeAdjustPolicy=QComboBox.AdjustToMinimumContentsLengthWithIcon,
            minimumContentsLength=12
        )
        self.col_side_color_cb.setModel(self.col_side_color_model)
        self.connect_control(
            "column_annotation_color_var", self.column_annotation_color_var_changed,
        )
        self.column_annotation_color_var = None
        self.col_side_color_cb.activated.connect(
            self.__set_column_annotation_color_var_index)

        cb = gui.comboBox(
            None, self, "column_label_pos",
            callback=self.update_column_annotations)
        cb.setModel(create_list_model(ColumnLabelsPosData, parent=self))
        cb.setCurrentIndex(self.column_label_pos)
        form.addRow("Position", cb)
        form.addRow("Color", self.col_side_color_cb)
        box.layout().addWidget(annotbox)

        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.buttonsArea, self, "auto_commit")

        # Scene with heatmap
        class HeatmapScene(GraphicsScene):
            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])

    def _save_state_for_serialization(self):
        def desc(var: Optional[Variable]) -> Optional[Tuple[str, str]]:
            if var is not None:
                return type(var).__name__, var.name
            else:
                return None

        self.col_clustering_method = self.col_clustering.name
        self.row_clustering_method = self.row_clustering.name

        self.column_annotation_color_key = desc(self.column_annotation_color_var)
        self.split_columns_key = desc(self.split_columns_var)

    @property
    def center_palette(self):
        palette = self.color_map_widget.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_map_widget.currentData().lookup_table()

    def color_map(self) -> GradientColorMap:
        return GradientColorMap(
            self.color_palette(), (self.threshold_low, self.threshold_high),
            self.color_map_widget.center() 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.col_side_color_model.set_domain(None)
        self.annotation_color_var = None
        self.column_annotation_color_var = None
        self.row_split_model.set_domain(None)
        self.col_split_model.set_domain(None)
        self.split_by_var = None
        self.split_columns_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()

        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 or hidden attributes which must be
        # filtered
        if data is not None and \
                any(var.is_discrete or var.attributes.get('hidden', False)
                    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 and
                        not var.attributes.get('hidden', False)],
                       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)
            self.col_annot_data = data.transpose(data[:0].transform(Domain(data.domain.attributes)))
            self.col_split_model.set_domain(self.col_annot_data.domain)
            self.col_side_color_model.set_domain(self.col_annot_data.domain)
            if data.domain.has_discrete_class:
                self.split_by_var = data.domain.class_var
            else:
                self.split_by_var = None
            self.split_columns_var = None
            self.column_annotation_color_var = None
            self.openContext(self.input_data)
            if self.split_by_var not in self.row_split_model:
                self.split_by_var = None

            def match(desc: Tuple[str, str], source: Iterable[Variable]):
                for v in source:
                    if desc == (type(v).__name__, v.name):
                        return v
                return None

            def is_variable(obj):
                return isinstance(obj, Variable)

            if self.split_columns_key is not None:
                self.split_columns_var = match(
                    self.split_columns_key,
                    filter(is_variable, self.col_split_model)
                )

            if self.column_annotation_color_key is not None:
                self.column_annotation_color_var = match(
                    self.column_annotation_color_key,
                    filter(is_variable, self.col_side_color_model)
                )

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

        self.commit.now()

    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 is not self.split_by_var:
            self.split_by_var = var
            self.update_heatmaps()

    def __on_split_cols_activated(self):
        self.set_column_split_var(self.col_split_cb.currentData(Qt.EditRole))

    def set_column_split_var(self, var: Optional[Variable]):
        if var is not self.split_columns_var:
            self.split_columns_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.split_columns_var)
                self.construct_heatmaps_scene(parts, self.effective_data)
                self.selected_rows = []
        else:
            self.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.deferred()

    def _make_parts(self, data, group_var=None, column_split_key=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)]
            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)]

        if column_split_key is not None:
            col_groups = split_domain(data.domain, column_split_key)
            assert len(col_groups) > 0
            col_indices = [np.array([data.domain.index(var) for var in group])
                           for _, group in col_groups]
            col_groups = [ColumnPart(title=str(name), domain=d, indices=ind,
                                     cluster=None, cluster_ordered=None)
                          for (name, d), ind in zip(col_groups, col_indices)]
        else:
            col_groups = [
                ColumnPart(
                    title=None, indices=range(0, len(data.domain.attributes)),
                    domain=data.domain.attributes, 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:
                    cluster = hierarchical.dist_matrix_clustering(
                        matrix, linkage=hierarchical.WARD
                    )
                if ordered and cluster_ord is None:
                    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: 'Parts', ordered=False):
        assert all(var.is_continuous for var in data.domain.attributes)
        col_groups = []
        for col in parts.columns:
            if col.cluster is not None:
                cluster = col.cluster
            else:
                cluster = None
            if col.cluster_ordered is not None:
                cluster_ord = col.cluster_ordered
            else:
                cluster_ord = None
            if col.can_cluster:
                need_dist = cluster is None or (ordered and cluster_ord is None)
                matrix = None
                if need_dist:
                    subset = data.transform(Domain(col.domain))
                    subset = Orange.distance._preprocess(subset)
                    matrix = np.asarray(Orange.distance.PearsonR(subset, axis=0))
                    # nan values break clustering below
                    matrix = np.nan_to_num(matrix)

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

            col_groups.append(col._replace(cluster=cluster, cluster_ordered=cluster_ord))
        return parts._replace(columns=col_groups)

    def construct_heatmaps(self, data, group_var=None, column_split_key=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

        parts = self._make_parts(
            effective_data, group_var,
            column_split_key.name if column_split_key is not None else None)

        self.__update_clustering_enable_state(parts)
        # 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 column_split_key in self.__columns_cache:
            parts = parts._replace(
                columns=self.__columns_cache[column_split_key].columns)

        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)

        side = self.column_side_colors()
        if side is not None:
            widget.setColumnSideColorAnnotations(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, parts: Optional['Parts']):
        def c_cost(sizes: Iterable[int]) -> int:
            """Estimated cost for clustering of `sizes`"""
            return sum(n ** 2 for n in sizes)

        def co_cost(sizes: Iterable[int]) -> int:
            """Estimated cost for cluster ordering of `sizes`"""
            # ~O(N ** 3) but O(N ** 4) worst case.
            return sum(n ** 4 for n in sizes)

        if parts is not None:
            Ns = [len(p.indices) for p in parts.rows]
            Ms = [len(p.indices) for p in parts.columns]
        else:
            Ns = Ms = [0]

        rc_enabled = c_cost(Ns) <= c_cost([self.MaxClustering])
        rco_enabled = co_cost(Ns) <= co_cost([self.MaxOrderedClustering])
        cc_enabled = c_cost(Ms) <= c_cost([self.MaxClustering])
        cco_enabled = co_cost(Ms) <= co_cost([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 " \
                            "estimated runtime cost"
        if not rc_enabled and row_clust == Clustering.Clustering:
            row_clust = Clustering.None_
            row_clust_msg = "Row clustering was was disabled due to the " \
                            "estimated runtime cost"

        if not cco_enabled and col_clust == Clustering.OrderedClustering:
            col_clust = Clustering.Clustering
            col_clust_msg = "Column cluster ordering was disabled due to " \
                            "estimated runtime cost"
        if not cc_enabled and col_clust == Clustering.Clustering:
            col_clust = Clustering.None_
            col_clust_msg = "Column clustering was disabled due to the " \
                            "estimated runtime cost"

        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_color_schema(self):
        self.palette_name = self.color_map_widget.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.deferred()

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

    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 is not 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)
        merges = self._merge_row_indices()
        if merges is not None:
            column_data = aggregate(var, column_data, merges)
        data, colormap = colorize(var, column_data)
        if var.is_continuous:
            span = (np.nanmin(column_data), np.nanmax(column_data))
            if np.any(np.isnan(span)):
                span = 0., 1.
            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 is not 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 __set_column_annotation_color_var_index(self, index: int):
        key = self.col_side_color_cb.itemData(index, Qt.EditRole)
        self.set_column_annotation_color_var(key)

    def column_annotation_color_var_changed(self, value):
        cbselect(self.col_side_color_cb, value, Qt.EditRole)

    def set_column_annotation_color_var(self, var):
        if self.column_annotation_color_var is not var:
            self.column_annotation_color_var = var
            colors = self.column_side_colors()
            if colors is not None:
                self.scene.widget.setColumnSideColorAnnotations(
                    colors[0], colors[1], colors[2].name,
                )
            else:
                self.scene.widget.setColumnSideColorAnnotations(None)

    def column_side_colors(self):
        var = self.column_annotation_color_var
        if var is None:
            return None
        table = self.col_annot_data
        return color_annotation_data(table, var)

    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.deferred()

    @gui.deferred
    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]

        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

    @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 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 color_annotation_data(
        table: Table, var: Union[int, str, Variable]
) -> Tuple[np.ndarray, ColorMap, Variable]:
    var = table.domain[var]
    column_data = column_data_from_table(table, var)
    data, colormap = colorize(var, column_data)
    return data, colormap, var


def colorize(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 = data.astype(int)
        data[mask] = -1
        if mask.any():
            values = (*var.values, "N/A")
        else:
            values = var.values
            colors = colors[: -1]
        return data, CategoricalColorMap(colors, values)
    elif var.is_continuous:
        span = np.nanmin(data), np.nanmax(data)
        if np.any(np.isnan(span)):
            span = 0, 1.
        return data, GradientColorMap(colors[:-1], span=span)
    else:
        raise TypeError


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: Warning Not Displayed When Discrete Data is Provided in HeatMap Widget

**Description**
When discrete data is input into the HeatMap widget, it is expected to display an information message indicating that the discrete data is being ignored. However, the widget does not show this warning, which can lead to confusion for users who are unaware that their discrete data is not being processed.

**Example Buggy Code**
```python
data = Table("heart_disease.tab")[:10]
widget.send_signal(widget.Inputs.data, data)
assert widget.Information.discrete_ignored.is_shown()
```
In this example, after sending discrete data to the widget, the information message `discrete_ignored` should be displayed. However, the assertion fails because the message is not shown.

**Expected Behavior**
The HeatMap widget should display an information message notifying the user that discrete data has been ignored when such data is provided.

**Actual Behavior**
No information message is displayed when discrete data is input into the HeatMap widget, leaving users unaware that their discrete data is not being utilized.

[/ISSUE]
{
    "TestOWHeatMap.test_centering_threshold_change": "PASSED",
    "TestOWHeatMap.test_cls_with_single_instance": "PASSED",
    "TestOWHeatMap.test_cluster_column_on_all_zero_column": "PASSED",
    "TestOWHeatMap.test_col_color_annotations": "PASSED",
    "TestOWHeatMap.test_col_color_annotations_with_na": "PASSED",
    "TestOWHeatMap.test_data_column_nans": "PASSED",
    "TestOWHeatMap.test_data_with_hidden": "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_row_color_annotations_with_na": "PASSED",
    "TestOWHeatMap.test_saved_selection": "PASSED",
    "TestOWHeatMap.test_saved_selection_when_not_possible": "PASSED",
    "TestOWHeatMap.test_set_split_column_key": "PASSED",
    "TestOWHeatMap.test_set_split_column_key_missing": "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"
}
diff --git a/Orange/widgets/visualize/owheatmap.py b/Orange/widgets/visualize/owheatmap.py
index 81b04bd6a..fac4bd15a 100644
--- a/Orange/widgets/visualize/owheatmap.py
+++ b/Orange/widgets/visualize/owheatmap.py
@@ -692,7 +692,11 @@ class OWHeatMap(widget.OWWidget):
     def update_heatmaps(self):
         if self.data is not None:
             self.clear_scene()
-            self.clear_messages()
+            self.Error.clear()
+            self.Warning.clear()
+            self.Information.row_clust.clear()
+            self.Information.col_clust.clear()
+            self.Information.sampled.clear()
             if self.col_clustering != Clustering.None_ and \
                     len(self.data.domain.attributes) < 2:
                 self.Error.not_enough_features()

{"Orange/widgets/visualize/owheatmap.py": "import enum\nfrom collections import defaultdict\nfrom itertools import islice\nfrom typing import (\n    Iterable, Mapping, Any, TypeVar, 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    QGraphicsView, QFormLayout, QComboBox, QGroupBox, QMenu, QAction,\n    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, \\\n    ContinuousVariable\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, enum_get, itemmodels\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.graphicsscene import GraphicsScene\nfrom Orange.widgets.utils.colorpalettes import 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.colorgradientselection import ColorGradientSelection\nfrom Orange.widgets.utils.widgetpreview import WidgetPreview\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 split_domain(domain: Domain, split_label: str):\n    \"\"\"Split the domain based on values of `split_label` value.\n    \"\"\"\n    groups = defaultdict(list)\n    for var in domain.attributes:\n        val = var.attributes.get(split_label)\n        groups[val].append(var)\n    if None in groups:\n        na = groups.pop(None)\n        return [*groups.items(), (\"N/A\", na)]\n    else:\n        return list(groups.items())\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\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    color_center = settings.Setting(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    column_annotation_color_key: Optional[Tuple[str, str]] = settings.ContextSetting(None)\n\n    # Discrete variable used to split that data/heatmaps (vertically)\n    split_by_var = settings.ContextSetting(None)\n    # Split heatmap columns by 'key' (horizontal)\n    split_columns_key: Optional[Tuple[str, str]] = 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    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    UserAdviceMessages = [\n        widget.Message(\n            \"For data with a meaningful mid-point, \"\n            \"choose one of diverging palettes.\",\n            \"diverging_palette\")]\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(self._save_state_for_serialization)\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 stripped of discrete features and hidden\n        #: attributes, and often merged using k-means\n        self.data = None\n        self.effective_data = None\n        #: Source of column annotations (derived from self.data)\n        self.col_annot_data: Optional[Table] = 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        # GUI definition\n        colorbox = gui.vBox(self.controlArea, \"Color\")\n\n        self.color_map_widget = cmw = ColorGradientSelection(\n            thresholds=(self.threshold_low, self.threshold_high),\n            center=self.color_center\n        )\n        model = itemmodels.ContinuousPalettesModel(parent=self)\n        cmw.setModel(model)\n        idx = cmw.findData(self.palette_name, model.KeyRole)\n        if idx != -1:\n            cmw.setCurrentIndex(idx)\n\n        cmw.activated.connect(self.update_color_schema)\n\n        def _set_thresholds(low, high):\n            self.threshold_low, self.threshold_high = low, high\n            self.update_color_schema()\n        cmw.thresholdsChanged.connect(_set_thresholds)\n\n        def _set_centering(center):\n            self.color_center = center\n            self.update_color_schema()\n        cmw.centerChanged.connect(_set_centering)\n\n        colorbox.layout().addWidget(self.color_map_widget)\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        form = QFormLayout(\n            formAlignment=Qt.AlignLeft, labelAlignment=Qt.AlignLeft,\n            fieldGrowthPolicy=QFormLayout.AllNonFixedFieldsGrow,\n        )\n        box.layout().addLayout(form)\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        self.col_split_model = DomainModel(\n            placeholder=\"(None)\",\n            order=DomainModel.MIXED,\n            valid_types=(Orange.data.DiscreteVariable,),\n            parent=self,\n        )\n        self.col_split_cb = cb = ComboBoxSearch(\n            sizeAdjustPolicy=ComboBox.AdjustToMinimumContentsLengthWithIcon,\n            minimumContentsLength=14,\n            toolTip=\"Split the heatmap horizontally by column annotation\"\n        )\n        self.col_split_cb.setModel(self.col_split_model)\n        self.connect_control(\n            \"split_columns_var\", lambda value, cb=cb: cbselect(cb, value)\n        )\n        self.split_columns_var = None\n        self.col_split_cb.activated.connect(self.__on_split_cols_activated)\n        form.addRow(\"Rows:\", self.row_split_cb)\n        form.addRow(\"Columns:\", self.col_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        gui.separator(box)\n        annotbox = QGroupBox(\"Row Annotations\")\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.AdjustToMinimumContentsLengthWithIcon\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.AdjustToMinimumContentsLengthWithIcon,\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        annotbox = QGroupBox(\"Column annotations\")\n        form = QFormLayout(\n            annotbox,\n            formAlignment=Qt.AlignLeft,\n            labelAlignment=Qt.AlignLeft,\n            fieldGrowthPolicy=QFormLayout.AllNonFixedFieldsGrow\n        )\n        self.col_side_color_model = DomainModel(\n            placeholder=\"(None)\",\n            valid_types=(DiscreteVariable, ContinuousVariable),\n            parent=self\n        )\n        self.col_side_color_cb = cb = ComboBoxSearch(\n            sizeAdjustPolicy=QComboBox.AdjustToMinimumContentsLengthWithIcon,\n            minimumContentsLength=12\n        )\n        self.col_side_color_cb.setModel(self.col_side_color_model)\n        self.connect_control(\n            \"column_annotation_color_var\", self.column_annotation_color_var_changed,\n        )\n        self.column_annotation_color_var = None\n        self.col_side_color_cb.activated.connect(\n            self.__set_column_annotation_color_var_index)\n\n        cb = gui.comboBox(\n            None, 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        form.addRow(\"Position\", cb)\n        form.addRow(\"Color\", self.col_side_color_cb)\n        box.layout().addWidget(annotbox)\n\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\n        gui.auto_send(self.buttonsArea, self, \"auto_commit\")\n\n        # Scene with heatmap\n        class HeatmapScene(GraphicsScene):\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    def _save_state_for_serialization(self):\n        def desc(var: Optional[Variable]) -> Optional[Tuple[str, str]]:\n            if var is not None:\n                return type(var).__name__, var.name\n            else:\n                return None\n\n        self.col_clustering_method = self.col_clustering.name\n        self.row_clustering_method = self.row_clustering.name\n\n        self.column_annotation_color_key = desc(self.column_annotation_color_var)\n        self.split_columns_key = desc(self.split_columns_var)\n\n    @property\n    def center_palette(self):\n        palette = self.color_map_widget.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_map_widget.currentData().lookup_table()\n\n    def color_map(self) -> GradientColorMap:\n        return GradientColorMap(\n            self.color_palette(), (self.threshold_low, self.threshold_high),\n            self.color_map_widget.center() 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.col_side_color_model.set_domain(None)\n        self.annotation_color_var = None\n        self.column_annotation_color_var = None\n        self.row_split_model.set_domain(None)\n        self.col_split_model.set_domain(None)\n        self.split_by_var = None\n        self.split_columns_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\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 or hidden attributes which must be\n        # filtered\n        if data is not None and \\\n                any(var.is_discrete or var.attributes.get('hidden', False)\n                    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 and\n                        not var.attributes.get('hidden', False)],\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            self.col_annot_data = data.transpose(data[:0].transform(Domain(data.domain.attributes)))\n            self.col_split_model.set_domain(self.col_annot_data.domain)\n            self.col_side_color_model.set_domain(self.col_annot_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.split_columns_var = None\n            self.column_annotation_color_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            def match(desc: Tuple[str, str], source: Iterable[Variable]):\n                for v in source:\n                    if desc == (type(v).__name__, v.name):\n                        return v\n                return None\n\n            def is_variable(obj):\n                return isinstance(obj, Variable)\n\n            if self.split_columns_key is not None:\n                self.split_columns_var = match(\n                    self.split_columns_key,\n                    filter(is_variable, self.col_split_model)\n                )\n\n            if self.column_annotation_color_key is not None:\n                self.column_annotation_color_var = match(\n                    self.column_annotation_color_key,\n                    filter(is_variable, self.col_side_color_model)\n                )\n\n        self.update_heatmaps()\n        if data is not None and self.__pending_selection is not None:\n            if 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.commit.now()\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 is not self.split_by_var:\n            self.split_by_var = var\n            self.update_heatmaps()\n\n    def __on_split_cols_activated(self):\n        self.set_column_split_var(self.col_split_cb.currentData(Qt.EditRole))\n\n    def set_column_split_var(self, var: Optional[Variable]):\n        if var is not self.split_columns_var:\n            self.split_columns_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, self.split_columns_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.deferred()\n\n    def _make_parts(self, data, group_var=None, column_split_key=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        if column_split_key is not None:\n            col_groups = split_domain(data.domain, column_split_key)\n            assert len(col_groups) > 0\n            col_indices = [np.array([data.domain.index(var) for var in group])\n                           for _, group in col_groups]\n            col_groups = [ColumnPart(title=str(name), domain=d, indices=ind,\n                                     cluster=None, cluster_ordered=None)\n                          for (name, d), ind in zip(col_groups, col_indices)]\n        else:\n            col_groups = [\n                ColumnPart(\n                    title=None, indices=range(0, len(data.domain.attributes)),\n                    domain=data.domain.attributes, 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                    cluster = hierarchical.dist_matrix_clustering(\n                        matrix, linkage=hierarchical.WARD\n                    )\n                if ordered and cluster_ord is None:\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: 'Parts', ordered=False):\n        assert all(var.is_continuous for var in data.domain.attributes)\n        col_groups = []\n        for col in parts.columns:\n            if col.cluster is not None:\n                cluster = col.cluster\n            else:\n                cluster = None\n            if col.cluster_ordered is not None:\n                cluster_ord = col.cluster_ordered\n            else:\n                cluster_ord = None\n            if col.can_cluster:\n                need_dist = cluster is None or (ordered and cluster_ord is None)\n                matrix = None\n                if need_dist:\n                    subset = data.transform(Domain(col.domain))\n                    subset = Orange.distance._preprocess(subset)\n                    matrix = np.asarray(Orange.distance.PearsonR(subset, 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                    cluster = hierarchical.dist_matrix_clustering(\n                        matrix, linkage=hierarchical.WARD\n                    )\n                if ordered and cluster_ord is None:\n                    cluster_ord = hierarchical.optimal_leaf_ordering(cluster, matrix)\n\n            col_groups.append(col._replace(cluster=cluster, cluster_ordered=cluster_ord))\n        return parts._replace(columns=col_groups)\n\n    def construct_heatmaps(self, data, group_var=None, column_split_key=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        parts = self._make_parts(\n            effective_data, group_var,\n            column_split_key.name if column_split_key is not None else None)\n\n        self.__update_clustering_enable_state(parts)\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 column_split_key in self.__columns_cache:\n            parts = parts._replace(\n                columns=self.__columns_cache[column_split_key].columns)\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\n        side = self.row_side_colors()\n        if side is not None:\n            widget.setRowSideColorAnnotations(side[0], side[1], name=side[2].name)\n\n        side = self.column_side_colors()\n        if side is not None:\n            widget.setColumnSideColorAnnotations(side[0], side[1], name=side[2].name)\n\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, parts: Optional['Parts']):\n        def c_cost(sizes: Iterable[int]) -> int:\n            \"\"\"Estimated cost for clustering of `sizes`\"\"\"\n            return sum(n ** 2 for n in sizes)\n\n        def co_cost(sizes: Iterable[int]) -> int:\n            \"\"\"Estimated cost for cluster ordering of `sizes`\"\"\"\n            # ~O(N ** 3) but O(N ** 4) worst case.\n            return sum(n ** 4 for n in sizes)\n\n        if parts is not None:\n            Ns = [len(p.indices) for p in parts.rows]\n            Ms = [len(p.indices) for p in parts.columns]\n        else:\n            Ns = Ms = [0]\n\n        rc_enabled = c_cost(Ns) <= c_cost([self.MaxClustering])\n        rco_enabled = co_cost(Ns) <= co_cost([self.MaxOrderedClustering])\n        cc_enabled = c_cost(Ms) <= c_cost([self.MaxClustering])\n        cco_enabled = co_cost(Ms) <= co_cost([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                            \"estimated runtime cost\"\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                            \"estimated runtime cost\"\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                            \"estimated runtime cost\"\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                            \"estimated runtime cost\"\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_color_schema(self):\n        self.palette_name = self.color_map_widget.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.deferred()\n\n    def __update_row_clustering(self):\n        self.update_heatmaps()\n        self.commit.deferred()\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 is not 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        merges = self._merge_row_indices()\n        if merges is not None:\n            column_data = aggregate(var, column_data, merges)\n        data, colormap = colorize(var, column_data)\n        if var.is_continuous:\n            span = (np.nanmin(column_data), np.nanmax(column_data))\n            if np.any(np.isnan(span)):\n                span = 0., 1.\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 is not 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 __set_column_annotation_color_var_index(self, index: int):\n        key = self.col_side_color_cb.itemData(index, Qt.EditRole)\n        self.set_column_annotation_color_var(key)\n\n    def column_annotation_color_var_changed(self, value):\n        cbselect(self.col_side_color_cb, value, Qt.EditRole)\n\n    def set_column_annotation_color_var(self, var):\n        if self.column_annotation_color_var is not var:\n            self.column_annotation_color_var = var\n            colors = self.column_side_colors()\n            if colors is not None:\n                self.scene.widget.setColumnSideColorAnnotations(\n                    colors[0], colors[1], colors[2].name,\n                )\n            else:\n                self.scene.widget.setColumnSideColorAnnotations(None)\n\n    def column_side_colors(self):\n        var = self.column_annotation_color_var\n        if var is None:\n            return None\n        table = self.col_annot_data\n        return color_annotation_data(table, var)\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.deferred()\n\n    @gui.deferred\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        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    @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 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 color_annotation_data(\n        table: Table, var: Union[int, str, Variable]\n) -> Tuple[np.ndarray, ColorMap, Variable]:\n    var = table.domain[var]\n    column_data = column_data_from_table(table, var)\n    data, colormap = colorize(var, column_data)\n    return data, colormap, var\n\n\ndef colorize(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 = data.astype(int)\n        data[mask] = -1\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        span = np.nanmin(data), np.nanmax(data)\n        if np.any(np.isnan(span)):\n            span = 0, 1.\n        return data, GradientColorMap(colors[:-1], span=span)\n    else:\n        raise TypeError\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: Warning Not Displayed When Discrete Data is Provided in HeatMap Widget

**Description**
When discrete data is input into the HeatMap widget, it is expected to display an information message indicating that the discrete data is being ignored. However, the widget does not show this warning, which can lead to confusion for users who are unaware that their discrete data is not being processed.

**Example Buggy Code**
```python
data = Table("heart_disease.tab")[:10]
widget.send_signal(widget.Inputs.data, data)
assert widget.Information.discrete_ignored.is_shown()
```
In this example, after sending discrete data to the widget, the information message `discrete_ignored` should be displayed. However, the assertion fails because the message is not shown.

**Expected Behavior**
The HeatMap widget should display an information message notifying the user that discrete data has been ignored when such data is provided.

**Actual Behavior**
No information message is displayed when discrete data is input into the HeatMap widget, leaving users unaware that their discrete data is not being utilized.

[/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 collections import defaultdict
from itertools import islice
from typing import (
    Iterable, Mapping, Any, TypeVar, NamedTuple, Sequence, Optional,
    Union, Tuple, List, Callable
)

import numpy as np
import scipy.sparse as sp

from AnyQt.QtWidgets import (
    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, \
    ContinuousVariable
from Orange.data.sql.table import SqlTable
import Orange.distance

from Orange.clustering import hierarchical, kmeans
from Orange.widgets.utils import colorpalettes, apply_all, enum_get, itemmodels
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.graphicsscene import GraphicsScene
from Orange.widgets.utils.colorpalettes import 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.colorgradientselection import ColorGradientSelection
from Orange.widgets.utils.widgetpreview import WidgetPreview


__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 split_domain(domain: Domain, split_label: str):
    """Split the domain based on values of `split_label` value.
    """
    groups = defaultdict(list)
    for var in domain.attributes:
        val = var.attributes.get(split_label)
        groups[val].append(var)
    if None in groups:
        na = groups.pop(None)
        return [*groups.items(), ("N/A", na)]
    else:
        return list(groups.items())


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


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)
    color_center = settings.Setting(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)
    column_annotation_color_key: Optional[Tuple[str, str]] = settings.ContextSetting(None)

    # Discrete variable used to split that data/heatmaps (vertically)
    split_by_var = settings.ContextSetting(None)
    # Split heatmap columns by 'key' (horizontal)
    split_columns_key: Optional[Tuple[str, str]] = 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"

    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")

    UserAdviceMessages = [
        widget.Message(
            "For data with a meaningful mid-point, "
            "choose one of diverging palettes.",
            "diverging_palette")]

    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(self._save_state_for_serialization)
        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 stripped of discrete features and hidden
        #: attributes, and often merged using k-means
        self.data = None
        self.effective_data = None
        #: Source of column annotations (derived from self.data)
        self.col_annot_data: Optional[Table] = 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 = {}

        # GUI definition
        colorbox = gui.vBox(self.controlArea, "Color")

        self.color_map_widget = cmw = ColorGradientSelection(
            thresholds=(self.threshold_low, self.threshold_high),
            center=self.color_center
        )
        model = itemmodels.ContinuousPalettesModel(parent=self)
        cmw.setModel(model)
        idx = cmw.findData(self.palette_name, model.KeyRole)
        if idx != -1:
            cmw.setCurrentIndex(idx)

        cmw.activated.connect(self.update_color_schema)

        def _set_thresholds(low, high):
            self.threshold_low, self.threshold_high = low, high
            self.update_color_schema()
        cmw.thresholdsChanged.connect(_set_thresholds)

        def _set_centering(center):
            self.color_center = center
            self.update_color_schema()
        cmw.centerChanged.connect(_set_centering)

        colorbox.layout().addWidget(self.color_map_widget)

        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")
        form = QFormLayout(
            formAlignment=Qt.AlignLeft, labelAlignment=Qt.AlignLeft,
            fieldGrowthPolicy=QFormLayout.AllNonFixedFieldsGrow,
        )
        box.layout().addLayout(form)

        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
        )
        self.col_split_model = DomainModel(
            placeholder="(None)",
            order=DomainModel.MIXED,
            valid_types=(Orange.data.DiscreteVariable,),
            parent=self,
        )
        self.col_split_cb = cb = ComboBoxSearch(
            sizeAdjustPolicy=ComboBox.AdjustToMinimumContentsLengthWithIcon,
            minimumContentsLength=14,
            toolTip="Split the heatmap horizontally by column annotation"
        )
        self.col_split_cb.setModel(self.col_split_model)
        self.connect_control(
            "split_columns_var", lambda value, cb=cb: cbselect(cb, value)
        )
        self.split_columns_var = None
        self.col_split_cb.activated.connect(self.__on_split_cols_activated)
        form.addRow("Rows:", self.row_split_cb)
        form.addRow("Columns:", self.col_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)
        gui.separator(box)
        annotbox = QGroupBox("Row Annotations")
        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.AdjustToMinimumContentsLengthWithIcon
        )
        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.AdjustToMinimumContentsLengthWithIcon,
            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)
        annotbox = QGroupBox("Column annotations")
        form = QFormLayout(
            annotbox,
            formAlignment=Qt.AlignLeft,
            labelAlignment=Qt.AlignLeft,
            fieldGrowthPolicy=QFormLayout.AllNonFixedFieldsGrow
        )
        self.col_side_color_model = DomainModel(
            placeholder="(None)",
            valid_types=(DiscreteVariable, ContinuousVariable),
            parent=self
        )
        self.col_side_color_cb = cb = ComboBoxSearch(
            sizeAdjustPolicy=QComboBox.AdjustToMinimumContentsLengthWithIcon,
            minimumContentsLength=12
        )
        self.col_side_color_cb.setModel(self.col_side_color_model)
        self.connect_control(
            "column_annotation_color_var", self.column_annotation_color_var_changed,
        )
        self.column_annotation_color_var = None
        self.col_side_color_cb.activated.connect(
            self.__set_column_annotation_color_var_index)

        cb = gui.comboBox(
            None, self, "column_label_pos",
            callback=self.update_column_annotations)
        cb.setModel(create_list_model(ColumnLabelsPosData, parent=self))
        cb.setCurrentIndex(self.column_label_pos)
        form.addRow("Position", cb)
        form.addRow("Color", self.col_side_color_cb)
        box.layout().addWidget(annotbox)

        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.buttonsArea, self, "auto_commit")

        # Scene with heatmap
        class HeatmapScene(GraphicsScene):
            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])

    def _save_state_for_serialization(self):
        def desc(var: Optional[Variable]) -> Optional[Tuple[str, str]]:
            if var is not None:
                return type(var).__name__, var.name
            else:
                return None

        self.col_clustering_method = self.col_clustering.name
        self.row_clustering_method = self.row_clustering.name

        self.column_annotation_color_key = desc(self.column_annotation_color_var)
        self.split_columns_key = desc(self.split_columns_var)

    @property
    def center_palette(self):
        palette = self.color_map_widget.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_map_widget.currentData().lookup_table()

    def color_map(self) -> GradientColorMap:
        return GradientColorMap(
            self.color_palette(), (self.threshold_low, self.threshold_high),
            self.color_map_widget.center() 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.col_side_color_model.set_domain(None)
        self.annotation_color_var = None
        self.column_annotation_color_var = None
        self.row_split_model.set_domain(None)
        self.col_split_model.set_domain(None)
        self.split_by_var = None
        self.split_columns_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()

        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 or hidden attributes which must be
        # filtered
        if data is not None and \
                any(var.is_discrete or var.attributes.get('hidden', False)
                    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 and
                        not var.attributes.get('hidden', False)],
                       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)
            self.col_annot_data = data.transpose(data[:0].transform(Domain(data.domain.attributes)))
            self.col_split_model.set_domain(self.col_annot_data.domain)
            self.col_side_color_model.set_domain(self.col_annot_data.domain)
            if data.domain.has_discrete_class:
                self.split_by_var = data.domain.class_var
            else:
                self.split_by_var = None
            self.split_columns_var = None
            self.column_annotation_color_var = None
            self.openContext(self.input_data)
            if self.split_by_var not in self.row_split_model:
                self.split_by_var = None

            def match(desc: Tuple[str, str], source: Iterable[Variable]):
                for v in source:
                    if desc == (type(v).__name__, v.name):
                        return v
                return None

            def is_variable(obj):
                return isinstance(obj, Variable)

            if self.split_columns_key is not None:
                self.split_columns_var = match(
                    self.split_columns_key,
                    filter(is_variable, self.col_split_model)
                )

            if self.column_annotation_color_key is not None:
                self.column_annotation_color_var = match(
                    self.column_annotation_color_key,
                    filter(is_variable, self.col_side_color_model)
                )

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

        self.commit.now()

    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 is not self.split_by_var:
            self.split_by_var = var
            self.update_heatmaps()

    def __on_split_cols_activated(self):
        self.set_column_split_var(self.col_split_cb.currentData(Qt.EditRole))

    def set_column_split_var(self, var: Optional[Variable]):
        if var is not self.split_columns_var:
            self.split_columns_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.split_columns_var)
                self.construct_heatmaps_scene(parts, self.effective_data)
                self.selected_rows = []
        else:
            self.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.deferred()

    def _make_parts(self, data, group_var=None, column_split_key=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)]
            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)]

        if column_split_key is not None:
            col_groups = split_domain(data.domain, column_split_key)
            assert len(col_groups) > 0
            col_indices = [np.array([data.domain.index(var) for var in group])
                           for _, group in col_groups]
            col_groups = [ColumnPart(title=str(name), domain=d, indices=ind,
                                     cluster=None, cluster_ordered=None)
                          for (name, d), ind in zip(col_groups, col_indices)]
        else:
            col_groups = [
                ColumnPart(
                    title=None, indices=range(0, len(data.domain.attributes)),
                    domain=data.domain.attributes, 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:
                    cluster = hierarchical.dist_matrix_clustering(
                        matrix, linkage=hierarchical.WARD
                    )
                if ordered and cluster_ord is None:
                    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: 'Parts', ordered=False):
        assert all(var.is_continuous for var in data.domain.attributes)
        col_groups = []
        for col in parts.columns:
            if col.cluster is not None:
                cluster = col.cluster
            else:
                cluster = None
            if col.cluster_ordered is not None:
                cluster_ord = col.cluster_ordered
            else:
                cluster_ord = None
            if col.can_cluster:
                need_dist = cluster is None or (ordered and cluster_ord is None)
                matrix = None
                if need_dist:
                    subset = data.transform(Domain(col.domain))
                    subset = Orange.distance._preprocess(subset)
                    matrix = np.asarray(Orange.distance.PearsonR(subset, axis=0))
                    # nan values break clustering below
                    matrix = np.nan_to_num(matrix)

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

            col_groups.append(col._replace(cluster=cluster, cluster_ordered=cluster_ord))
        return parts._replace(columns=col_groups)

    def construct_heatmaps(self, data, group_var=None, column_split_key=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

        parts = self._make_parts(
            effective_data, group_var,
            column_split_key.name if column_split_key is not None else None)

        self.__update_clustering_enable_state(parts)
        # 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 column_split_key in self.__columns_cache:
            parts = parts._replace(
                columns=self.__columns_cache[column_split_key].columns)

        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)

        side = self.column_side_colors()
        if side is not None:
            widget.setColumnSideColorAnnotations(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, parts: Optional['Parts']):
        def c_cost(sizes: Iterable[int]) -> int:
            """Estimated cost for clustering of `sizes`"""
            return sum(n ** 2 for n in sizes)

        def co_cost(sizes: Iterable[int]) -> int:
            """Estimated cost for cluster ordering of `sizes`"""
            # ~O(N ** 3) but O(N ** 4) worst case.
            return sum(n ** 4 for n in sizes)

        if parts is not None:
            Ns = [len(p.indices) for p in parts.rows]
            Ms = [len(p.indices) for p in parts.columns]
        else:
            Ns = Ms = [0]

        rc_enabled = c_cost(Ns) <= c_cost([self.MaxClustering])
        rco_enabled = co_cost(Ns) <= co_cost([self.MaxOrderedClustering])
        cc_enabled = c_cost(Ms) <= c_cost([self.MaxClustering])
        cco_enabled = co_cost(Ms) <= co_cost([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 " \
                            "estimated runtime cost"
        if not rc_enabled and row_clust == Clustering.Clustering:
            row_clust = Clustering.None_
            row_clust_msg = "Row clustering was was disabled due to the " \
                            "estimated runtime cost"

        if not cco_enabled and col_clust == Clustering.OrderedClustering:
            col_clust = Clustering.Clustering
            col_clust_msg = "Column cluster ordering was disabled due to " \
                            "estimated runtime cost"
        if not cc_enabled and col_clust == Clustering.Clustering:
            col_clust = Clustering.None_
            col_clust_msg = "Column clustering was disabled due to the " \
                            "estimated runtime cost"

        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_color_schema(self):
        self.palette_name = self.color_map_widget.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.deferred()

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

    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 is not 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)
        merges = self._merge_row_indices()
        if merges is not None:
            column_data = aggregate(var, column_data, merges)
        data, colormap = colorize(var, column_data)
        if var.is_continuous:
            span = (np.nanmin(column_data), np.nanmax(column_data))
            if np.any(np.isnan(span)):
                span = 0., 1.
            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 is not 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 __set_column_annotation_color_var_index(self, index: int):
        key = self.col_side_color_cb.itemData(index, Qt.EditRole)
        self.set_column_annotation_color_var(key)

    def column_annotation_color_var_changed(self, value):
        cbselect(self.col_side_color_cb, value, Qt.EditRole)

    def set_column_annotation_color_var(self, var):
        if self.column_annotation_color_var is not var:
            self.column_annotation_color_var = var
            colors = self.column_side_colors()
            if colors is not None:
                self.scene.widget.setColumnSideColorAnnotations(
                    colors[0], colors[1], colors[2].name,
                )
            else:
                self.scene.widget.setColumnSideColorAnnotations(None)

    def column_side_colors(self):
        var = self.column_annotation_color_var
        if var is None:
            return None
        table = self.col_annot_data
        return color_annotation_data(table, var)

    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.deferred()

    @gui.deferred
    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]

        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

    @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 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 color_annotation_data(
        table: Table, var: Union[int, str, Variable]
) -> Tuple[np.ndarray, ColorMap, Variable]:
    var = table.domain[var]
    column_data = column_data_from_table(table, var)
    data, colormap = colorize(var, column_data)
    return data, colormap, var


def colorize(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 = data.astype(int)
        data[mask] = -1
        if mask.any():
            values = (*var.values, "N/A")
        else:
            values = var.values
            colors = colors[: -1]
        return data, CategoricalColorMap(colors, values)
    elif var.is_continuous:
        span = np.nanmin(data), np.nanmax(data)
        if np.any(np.isnan(span)):
            span = 0, 1.
        return data, GradientColorMap(colors[:-1], span=span)
    else:
        raise TypeError


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