{
    "file_diffs": [
        {
            "old_file_content": "from collections import defaultdict, namedtuple\nfrom xml.sax.saxutils import escape\n\nimport numpy as np\nimport scipy.sparse as sp\n\nfrom AnyQt.QtCore import Qt, QRectF, pyqtSignal as Signal, QObject, QThread, \\\n    pyqtSlot as Slot\nfrom AnyQt.QtGui import QTransform, QPen, QBrush, QColor, QPainter, \\\n    QPainterPath, QKeyEvent\nfrom AnyQt.QtWidgets import \\\n    QGraphicsView, QGraphicsScene, \\\n    QGraphicsItem, QGraphicsRectItem, QGraphicsItemGroup, QSizePolicy, \\\n    QGraphicsPathItem\n\nfrom Orange.data import Table, Domain\nfrom Orange.preprocess import decimal_binnings, time_binnings\nfrom Orange.projection.som import SOM\n\nfrom Orange.widgets import gui\nfrom Orange.widgets.widget import OWWidget, Msg, Input, Output\nfrom Orange.widgets.settings import \\\n    DomainContextHandler, ContextSetting, Setting\nfrom Orange.widgets.utils.itemmodels import DomainModel\nfrom Orange.widgets.utils.widgetpreview import WidgetPreview\nfrom Orange.widgets.utils.annotated_data import \\\n    create_annotated_table, create_groups_table, ANNOTATED_DATA_SIGNAL_NAME\nfrom Orange.widgets.utils.colorpalettes import \\\n    BinnedContinuousPalette, LimitedDiscretePalette\nfrom Orange.widgets.visualize.utils import CanvasRectangle, CanvasText\nfrom Orange.widgets.visualize.utils.plotutils import wrap_legend_items\n\n\nsqrt3_2 = np.sqrt(3) / 2\n\n\nclass SomView(QGraphicsView):\n    SelectionSet, SelectionNewGroup, SelectionAddToGroup, SelectionRemove \\\n        = range(4)\n    selection_changed = Signal(np.ndarray, int)\n    selection_moved = Signal(QKeyEvent)\n    selection_mark_changed = Signal(np.ndarray)\n\n    def __init__(self, scene):\n        super().__init__(scene)\n        self.__button_down_pos = None\n        self.size_x = self.size_y = 1\n        self.hexagonal = True\n\n    def set_dimensions(self, size_x, size_y, hexagonal):\n        self.size_x = size_x\n        self.size_y = size_y\n        self.hexagonal = hexagonal\n\n    def _get_marked_cells(self, event):\n        x0, y0 = self.__button_down_pos.x(), self.__button_down_pos.y()\n        pos = self.mapToScene(event.pos())\n        x1, y1 = pos.x(), pos.y()\n        x0, x1 = sorted((x0, x1))\n        y0, y1 = sorted((y0, y1))\n        selection = np.zeros((self.size_x, self.size_y), dtype=bool)\n        if self.hexagonal:\n            y0 = max(0, int(y0 / sqrt3_2 + 0.5))\n            y1 = min(self.size_y, int(np.ceil(y1 / sqrt3_2 + 0.5)))\n            for y in range(y0, y1):\n                if x1 < 0:\n                    continue\n                x0_ = max(0, int(x0 + 0.5 - (y % 2) / 2))\n                x1_ = min(self.size_x - y % 2,\n                          int(np.ceil(x1 + 0.5 - (y % 2) / 2)))\n                selection[x0_:x1_, y] = True\n        elif not(x1 < -0.5 or x0 > self.size_x - 0.5\n                 or y1 < -0.5 or y0 > self.size_y - 0.5):\n\n            def roundclip(z, zmax):\n                return int(np.clip(np.round(z), 0, zmax - 1))\\\n\n            x0 = roundclip(x0, self.size_x)\n            y0 = roundclip(y0, self.size_y)\n            x1 = roundclip(x1, self.size_x)\n            y1 = roundclip(y1, self.size_y)\n            selection[x0:x1 + 1, y0:y1 + 1] = True\n\n        return selection\n\n    def mousePressEvent(self, event):\n        if event.button() != Qt.LeftButton:\n            return\n\n        self.__button_down_pos = self.mapToScene(event.pos())\n        event.accept()\n\n    def mouseMoveEvent(self, event):\n        if event.buttons() != Qt.LeftButton:\n            return\n\n        self.selection_mark_changed.emit(self._get_marked_cells(event))\n        event.accept()\n\n    def mouseReleaseEvent(self, event):\n        if event.button() != Qt.LeftButton:\n            return\n\n        if event.modifiers() & Qt.ShiftModifier:\n            if event.modifiers() & Qt.ControlModifier:\n                action = self.SelectionAddToGroup\n            else:\n                action = self.SelectionNewGroup\n        elif event.modifiers() & Qt.AltModifier:\n            action = self.SelectionRemove\n        else:\n            action = self.SelectionSet\n        selection = self._get_marked_cells(event)\n        self.selection_changed.emit(selection, action)\n        event.accept()\n\n    def keyPressEvent(self, event: QKeyEvent):\n        if event.key() in (Qt.Key_Up, Qt.Key_Down, Qt.Key_Left, Qt.Key_Right):\n            self.selection_moved.emit(event)\n        else:\n            super().keyPressEvent(event)\n\n\nclass PieChart(QGraphicsItem):\n    def __init__(self, dist, r, colors):\n        super().__init__()\n        self.dist = dist\n        self.r = r\n        self.colors = colors\n\n    def boundingRect(self):\n        return QRectF(- self.r, - self.r, 2 * self.r, 2 * self.r)\n\n    def paint(self, painter, _option, _index):\n        painter.save()\n        start_angle = 0\n        pen = QPen(QBrush(Qt.black), 2)\n        pen.setCosmetic(True)\n        painter.setPen(pen)\n        painter.drawEllipse(self.boundingRect())\n        for angle, color in zip(self.dist * 16 * 360, self.colors):\n            if angle == 0:\n                continue\n            painter.setBrush(QBrush(color))\n            painter.setPen(Qt.NoPen)\n            painter.drawPie(self.boundingRect(), int(start_angle), int(angle))\n            start_angle += angle\n        painter.restore()\n\n\nclass ColoredCircle(QGraphicsItem):\n    def __init__(self, r, color, proportion):\n        super().__init__()\n        self.r = r\n        self.color = color\n        self.proportion = proportion\n\n    def boundingRect(self):\n        return QRectF(-self.r, -self.r, 2 * self.r, 2 * self.r)\n\n    def paint(self, painter, _option, _index):\n        painter.save()\n        pen = QPen(QBrush(self.color), 2)\n        pen.setCosmetic(True)\n        painter.setPen(pen)\n        painter.setBrush(QBrush(self.color.lighter(int(200 - 80 * self.proportion))))\n        painter.drawEllipse(self.boundingRect())\n        painter.restore()\n\n\nN_ITERATIONS = 200\n\n\nclass OWSOM(OWWidget):\n    name = \"Self-Organizing Map\"\n    description = \"Computation of self-organizing map.\"\n    icon = \"icons/SOM.svg\"\n    keywords = [\"SOM\"]\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    settingsHandler = DomainContextHandler()\n    auto_dimension = Setting(True)\n    size_x = Setting(10)\n    size_y = Setting(10)\n    hexagonal = Setting(1)\n    initialization = Setting(0)\n\n    attr_color = ContextSetting(None)\n    size_by_instances = Setting(True)\n    pie_charts = Setting(False)\n    selection = Setting(None, schema_only=True)\n\n    graph_name = \"view\"\n\n    _grid_pen = QPen(QBrush(QColor(224, 224, 224)), 2)\n    _grid_pen.setCosmetic(True)\n\n    OptControls = namedtuple(\n        \"OptControls\",\n        (\"shape\", \"auto_dim\", \"spin_x\", \"spin_y\", \"initialization\", \"start\")\n    )\n\n    class Warning(OWWidget.Warning):\n        ignoring_disc_variables = Msg(\"SOM ignores categorical variables.\")\n        missing_colors = \\\n            Msg(\"Some data instances have undefined value of '{}'.\")\n        missing_values = \\\n            Msg(\"{} data instance{} with undefined value(s) {} not shown.\")\n        single_attribute = Msg(\"Data contains a single numeric column.\")\n\n    class Error(OWWidget.Error):\n        no_numeric_variables = Msg(\"Data contains no numeric columns.\")\n        no_defined_rows = Msg(\"All rows contain at least one undefined value.\")\n\n    def __init__(self):\n        super().__init__()\n        self.__pending_selection = self.selection\n        self._optimizer = None\n        self._optimizer_thread = None\n        self.stop_optimization = False\n\n        self.data = self.cont_x = None\n        self.cells = self.member_data = None\n        self.selection = None\n        self.colors = self.thresholds = self.bin_labels = None\n\n        box = gui.vBox(self.controlArea, box=\"SOM\")\n        shape = gui.comboBox(\n            box, self, \"\", items=(\"Hexagonal grid\", \"Square grid\"))\n        shape.setCurrentIndex(1 - self.hexagonal)\n\n        box2 = gui.indentedBox(box, 10)\n        auto_dim = gui.checkBox(\n            box2, self, \"auto_dimension\", \"Set dimensions automatically\",\n            callback=self.on_auto_dimension_changed)\n        self.manual_box = box3 = gui.hBox(box2)\n        spinargs = dict(\n            value=\"\", widget=box3, master=self, minv=5, maxv=100, step=5,\n            alignment=Qt.AlignRight)\n        spin_x = gui.spin(**spinargs)\n        spin_x.setValue(self.size_x)\n        gui.widgetLabel(box3, \"×\")\n        spin_y = gui.spin(**spinargs)\n        spin_y.setValue(self.size_y)\n        gui.rubber(box3)\n        self.manual_box.setEnabled(not self.auto_dimension)\n\n        initialization = gui.comboBox(\n            box, self, \"initialization\",\n            items=(\"Initialize with PCA\", \"Random initialization\",\n                   \"Replicable random\"))\n\n        start = gui.button(\n            box, self, \"Restart\", callback=self.restart_som_pressed,\n            sizePolicy=(QSizePolicy.MinimumExpanding, QSizePolicy.Fixed))\n\n        self.opt_controls = self.OptControls(\n            shape, auto_dim, spin_x, spin_y, initialization, start)\n\n        box = gui.vBox(self.controlArea, \"Color\")\n        gui.comboBox(\n            box, self, \"attr_color\", searchable=True,\n            callback=self.on_attr_color_change,\n            model=DomainModel(placeholder=\"(Same color)\",\n                              valid_types=DomainModel.PRIMITIVE))\n        gui.checkBox(\n            box, self, \"pie_charts\", label=\"Show pie charts\",\n            callback=self.on_pie_chart_change)\n        gui.checkBox(\n            box, self, \"size_by_instances\", label=\"Size by number of instances\",\n            callback=self.on_attr_size_change)\n\n        gui.rubber(self.controlArea)\n\n        self.scene = QGraphicsScene(self)\n\n        self.view = SomView(self.scene)\n        self.view.setMinimumWidth(400)\n        self.view.setMinimumHeight(400)\n        self.view.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)\n        self.view.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)\n        self.view.setRenderHint(QPainter.Antialiasing)\n        self.view.selection_changed.connect(self.on_selection_change)\n        self.view.selection_moved.connect(self.on_selection_move)\n        self.view.selection_mark_changed.connect(self.on_selection_mark_change)\n        self.mainArea.layout().addWidget(self.view)\n\n        self.elements = None\n        self.grid = None\n        self.grid_cells = None\n        self.legend = None\n\n    @Inputs.data\n    def set_data(self, data):\n        def prepare_data():\n            if len(cont_attrs) < len(attrs):\n                self.Warning.ignoring_disc_variables()\n            if len(cont_attrs) == 1:\n                self.Warning.single_attribute()\n            x = Table.from_table(Domain(cont_attrs), data).X\n            if sp.issparse(x):\n                self.data = data\n                self.cont_x = x.tocsr()\n            else:\n                mask = np.all(np.isfinite(x), axis=1)\n                if not np.any(mask):\n                    self.Error.no_defined_rows()\n                else:\n                    if np.all(mask):\n                        self.data = data\n                        self.cont_x = x.copy()\n                    else:\n                        self.data = data[mask]\n                        self.cont_x = x[mask]\n                    self.cont_x -= np.min(self.cont_x, axis=0)[None, :]\n                    sums = np.sum(self.cont_x, axis=0)[None, :]\n                    sums[sums == 0] = 1\n                    self.cont_x /= sums\n\n        def set_warnings():\n            missing = len(data) - len(self.data)\n            if missing == 1:\n                self.Warning.missing_values(1, \"\", \"is\")\n            elif missing > 1:\n                self.Warning.missing_values(missing, \"s\", \"are\")\n\n        self.stop_optimization_and_wait()\n\n        self.closeContext()\n        self.clear()\n        self.Error.clear()\n        self.Warning.clear()\n\n        if data is not None:\n            attrs = data.domain.attributes\n            cont_attrs = [var for var in attrs if var.is_continuous]\n            if not cont_attrs:\n                self.Error.no_numeric_variables()\n            else:\n                prepare_data()\n\n        if self.data is not None:\n            self.controls.attr_color.model().set_domain(data.domain)\n            self.attr_color = data.domain.class_var\n            set_warnings()\n\n        self.openContext(self.data)\n        self.set_color_bins()\n        self.create_legend()\n        self.recompute_dimensions()\n        self.start_som()\n\n    def clear(self):\n        self.data = self.cont_x = None\n        self.cells = self.member_data = None\n        self.attr_color = None\n        self.colors = self.thresholds = self.bin_labels = None\n        if self.elements is not None:\n            self.scene.removeItem(self.elements)\n            self.elements = None\n        self.clear_selection()\n        self.controls.attr_color.model().set_domain(None)\n        self.Warning.clear()\n        self.Error.clear()\n\n    def recompute_dimensions(self):\n        if not self.auto_dimension or self.cont_x is None:\n            return\n        dim = max(5, int(np.ceil(np.sqrt(5 * np.sqrt(self.cont_x.shape[0])))))\n        self.opt_controls.spin_x.setValue(dim)\n        self.opt_controls.spin_y.setValue(dim)\n\n    def on_auto_dimension_changed(self):\n        self.manual_box.setEnabled(not self.auto_dimension)\n        if self.auto_dimension:\n            self.recompute_dimensions()\n        else:\n            spin_x = self.opt_controls.spin_x\n            spin_y = self.opt_controls.spin_y\n            dimx = int(5 * np.round(spin_x.value() / 5))\n            dimy = int(5 * np.round(spin_y.value() / 5))\n            spin_x.setValue(dimx)\n            spin_y.setValue(dimy)\n\n    def on_attr_color_change(self):\n        self.controls.pie_charts.setEnabled(self.attr_color is not None)\n        self.set_color_bins()\n        self.create_legend()\n        self.rescale()\n        self._redraw()\n\n    def on_attr_size_change(self):\n        self._redraw()\n\n    def on_pie_chart_change(self):\n        self._redraw()\n\n    def clear_selection(self):\n        self.selection = None\n        self.redraw_selection()\n\n    def on_selection_change(self, selection, action=SomView.SelectionSet):\n        if self.data is None:  # clicks on empty canvas\n            return\n        if self.selection is None:\n            self.selection = np.zeros(self.grid_cells.T.shape, dtype=np.int16)\n        if action == SomView.SelectionSet:\n            self.selection[:] = 0\n            self.selection[selection] = 1\n        elif action == SomView.SelectionAddToGroup:\n            self.selection[selection] = max(1, np.max(self.selection))\n        elif action == SomView.SelectionNewGroup:\n            self.selection[selection] = 1 + np.max(self.selection)\n        elif action & SomView.SelectionRemove:\n            self.selection[selection] = 0\n        self.redraw_selection()\n        self.update_output()\n\n    def on_selection_move(self, event: QKeyEvent):\n        if self.selection is None or not np.any(self.selection):\n            if event.key() in (Qt.Key_Right, Qt.Key_Down):\n                x = y = 0\n            else:\n                x = self.size_x - 1\n                y = self.size_y - 1\n        else:\n            x, y = np.nonzero(self.selection)\n            if len(x) > 1:\n                return\n            if event.key() == Qt.Key_Up and y > 0:\n                y -= 1\n            if event.key() == Qt.Key_Down and y < self.size_y - 1:\n                y += 1\n            if event.key() == Qt.Key_Left and x:\n                x -= 1\n            if event.key() == Qt.Key_Right and x < self.size_x - 1:\n                x += 1\n            x -= self.hexagonal and x == self.size_x - 1 and y % 2\n\n        if self.selection is not None and self.selection[x, y]:\n            return\n        selection = np.zeros(self.grid_cells.shape, dtype=bool)\n        selection[x, y] = True\n        self.on_selection_change(selection)\n\n    def on_selection_mark_change(self, marks):\n        self.redraw_selection(marks=marks)\n\n    def redraw_selection(self, marks=None):\n        if self.grid_cells is None:\n            return\n\n        sel_pen = QPen(QBrush(QColor(128, 128, 128)), 2)\n        sel_pen.setCosmetic(True)\n        mark_pen = QPen(QBrush(QColor(128, 128, 128)), 4)\n        mark_pen.setCosmetic(True)\n        pens = [self._grid_pen, sel_pen]\n\n        mark_brush = QBrush(QColor(224, 255, 255))\n        sels = self.selection is not None and np.max(self.selection)\n        palette = LimitedDiscretePalette(number_of_colors=sels + 1)\n        brushes = [QBrush(Qt.NoBrush)] + \\\n                  [QBrush(palette[i].lighter(165)) for i in range(sels)]\n\n        for y in range(self.size_y):\n            for x in range(self.size_x - (y % 2) * self.hexagonal):\n                cell = self.grid_cells[y, x]\n                marked = marks is not None and marks[x, y]\n                sel_group = self.selection is not None and self.selection[x, y]\n                if marked:\n                    cell.setBrush(mark_brush)\n                    cell.setPen(mark_pen)\n                else:\n                    cell.setBrush(brushes[sel_group])\n                    cell.setPen(pens[bool(sel_group)])\n                cell.setZValue(marked or sel_group)\n\n    def restart_som_pressed(self):\n        if self._optimizer_thread is not None:\n            self.stop_optimization = True\n            self._optimizer.stop_optimization = True\n        else:\n            self.start_som()\n\n    def start_som(self):\n        self.read_controls()\n        self.update_layout()\n        self.clear_selection()\n        if self.cont_x is not None:\n            self.enable_controls(False)\n            self._recompute_som()\n        else:\n            self.update_output()\n\n    def read_controls(self):\n        c = self.opt_controls\n        self.hexagonal = c.shape.currentIndex() == 0\n        self.size_x = c.spin_x.value()\n        self.size_y = c.spin_y.value()\n\n    def enable_controls(self, enable):\n        c = self.opt_controls\n        c.shape.setEnabled(enable)\n        c.auto_dim.setEnabled(enable)\n        c.start.setText(\"Start\" if enable else \"Stop\")\n\n    def update_layout(self):\n        self.set_legend_pos()\n        if self.elements:  # Prevent having redrawn grid but with old elements\n            self.scene.removeItem(self.elements)\n            self.elements = None\n        self.redraw_grid()\n        self.rescale()\n\n    def _redraw(self):\n        self.Warning.missing_colors.clear()\n        if self.elements:\n            self.scene.removeItem(self.elements)\n            self.elements = None\n        self.view.set_dimensions(self.size_x, self.size_y, self.hexagonal)\n\n        if self.cells is None:\n            return\n        sizes = self.cells[:, :, 1] - self.cells[:, :, 0]\n        sizes = sizes.astype(float)\n        if not self.size_by_instances:\n            sizes[sizes != 0] = 0.8\n        else:\n            sizes *= 0.8 / np.max(sizes)\n\n        self.elements = QGraphicsItemGroup()\n        self.scene.addItem(self.elements)\n        if self.attr_color is None:\n            self._draw_same_color(sizes)\n        elif self.pie_charts:\n            self._draw_pie_charts(sizes)\n        else:\n            self._draw_colored_circles(sizes)\n\n    @property\n    def _grid_factors(self):\n        return (0.5, sqrt3_2) if self.hexagonal else (0, 1)\n\n    def _draw_same_color(self, sizes):\n        fx, fy = self._grid_factors\n        color = QColor(64, 64, 64)\n        for y in range(self.size_y):\n            for x in range(self.size_x - self.hexagonal * (y % 2)):\n                r = sizes[x, y]\n                n = len(self.get_member_indices(x, y))\n                if not r:\n                    continue\n                ellipse = ColoredCircle(r / 2, color, 0)\n                ellipse.setPos(x + (y % 2) * fx, y * fy)\n                ellipse.setToolTip(f\"{n} instances\")\n                self.elements.addToGroup(ellipse)\n\n    def _get_color_column(self):\n        color_column = \\\n            self.data.get_column_view(self.attr_color)[0].astype(float,\n                                                                 copy=False)\n        if self.attr_color.is_discrete:\n            with np.errstate(invalid=\"ignore\"):\n                int_col = color_column.astype(int)\n            int_col[np.isnan(color_column)] = len(self.colors)\n        else:\n            int_col = np.zeros(len(color_column), dtype=int)\n            # The following line is unnecessary because rows with missing\n            # numeric data are excluded. Uncomment it if you change SOM to\n            # tolerate missing values.\n            # int_col[np.isnan(color_column)] = len(self.colors)\n            for i, thresh in enumerate(self.thresholds, start=1):\n                int_col[color_column >= thresh] = i\n        return int_col\n\n    def _tooltip(self, colors, distribution):\n        if self.attr_color.is_discrete:\n            values = self.attr_color.values\n        else:\n            values = self._bin_names()\n        tot = np.sum(distribution)\n        nbhp = \"\\N{NON-BREAKING HYPHEN}\"\n        return '<table style=\"white-space: nowrap\">' + \"\".join(f\"\"\"\n            <tr>\n                <td>\n                    <font color={color.name()}>■</font>\n                    <b>{escape(val).replace(\"-\", nbhp)}</b>:\n                </td>\n                <td>\n                    {n} ({n / tot * 100:.1f}&nbsp;%)\n                </td>\n            </tr>\n            \"\"\" for color, val, n in zip(colors, values, distribution) if n) \\\n            + \"</table>\"\n\n    def _draw_pie_charts(self, sizes):\n        fx, fy = self._grid_factors\n        color_column = self._get_color_column()\n        colors = self.colors.qcolors_w_nan\n        for y in range(self.size_y):\n            for x in range(self.size_x - self.hexagonal * (y % 2)):\n                r = sizes[x, y]\n                if not r:\n                    self.grid_cells[y, x].setToolTip(\"\")\n                    continue\n                members = self.get_member_indices(x, y)\n                color_dist = np.bincount(color_column[members],\n                                         minlength=len(colors))\n                rel_color_dist = color_dist.astype(float) / len(members)\n                pie = PieChart(rel_color_dist, r / 2, colors)\n                pie.setToolTip(self._tooltip(colors, color_dist))\n                self.elements.addToGroup(pie)\n                pie.setPos(x + (y % 2) * fx, y * fy)\n\n    def _draw_colored_circles(self, sizes):\n        fx, fy = self._grid_factors\n        color_column = self._get_color_column()\n        qcolors = self.colors.qcolors_w_nan\n        for y in range(self.size_y):\n            for x in range(self.size_x - self.hexagonal * (y % 2)):\n                r = sizes[x, y]\n                if not r:\n                    continue\n                members = self.get_member_indices(x, y)\n                color_dist = color_column[members]\n                color_dist = color_dist[color_dist < len(self.colors)]\n                if len(color_dist) != len(members):\n                    self.Warning.missing_colors(self.attr_color.name)\n                bc = np.bincount(color_dist, minlength=len(self.colors))\n                color = qcolors[np.argmax(bc)]\n                ellipse = ColoredCircle(r / 2, color, np.max(bc) / len(members))\n                ellipse.setPos(x + (y % 2) * fx, y * fy)\n                ellipse.setToolTip(self._tooltip(qcolors, bc))\n                self.elements.addToGroup(ellipse)\n\n    def redraw_grid(self):\n        if self.grid is not None:\n            self.scene.removeItem(self.grid)\n        self.grid = QGraphicsItemGroup()\n        self.grid.setZValue(-200)\n        self.grid_cells = np.full((self.size_y, self.size_x), None)\n        for y in range(self.size_y):\n            for x in range(self.size_x - (y % 2) * self.hexagonal):\n                if self.hexagonal:\n                    cell = QGraphicsPathItem(_hexagon_path)\n                    cell.setPos(x + (y % 2) / 2, y * sqrt3_2)\n                else:\n                    cell = QGraphicsRectItem(x - 0.5, y - 0.5, 1, 1)\n                self.grid_cells[y, x] = cell\n                cell.setPen(self._grid_pen)\n                self.grid.addToGroup(cell)\n        self.scene.addItem(self.grid)\n\n    def get_member_indices(self, x, y):\n        i, j = self.cells[x, y]\n        return self.member_data[i:j]\n\n    def _recompute_som(self):\n        if self.cont_x is None:\n            return\n\n        som = SOM(\n            self.size_x, self.size_y,\n            hexagonal=self.hexagonal,\n            pca_init=self.initialization == 0,\n            random_seed=0 if self.initialization == 2 else None\n        )\n\n        class Optimizer(QObject):\n            update = Signal(float, np.ndarray, np.ndarray)\n            done = Signal(SOM)\n            stopped = Signal()\n            stop_optimization = False\n\n            def __init__(self, data, som):\n                super().__init__()\n                self.som = som\n                self.data = data\n\n            def callback(self, progress):\n                self.update.emit(\n                    progress,\n                    self.som.weights.copy(), self.som.ssum_weights.copy())\n                return not self.stop_optimization\n\n            def run(self):\n                try:\n                    self.som.fit(self.data, N_ITERATIONS,\n                                 callback=self.callback)\n                    # Report an exception, but still remove the thread\n                finally:\n                    self.done.emit(self.som)\n                    self.stopped.emit()\n\n        def thread_finished():\n            self._optimizer = None\n            self._optimizer_thread = None\n\n        self.progressBarInit()\n\n        self._optimizer = Optimizer(self.cont_x, som)\n        self._optimizer_thread = QThread()\n        self._optimizer_thread.setStackSize(5 * 2 ** 20)\n        self._optimizer.update.connect(self.__update)\n        self._optimizer.done.connect(self.__done)\n        self._optimizer.stopped.connect(self._optimizer_thread.quit)\n        self._optimizer.moveToThread(self._optimizer_thread)\n        self._optimizer_thread.started.connect(self._optimizer.run)\n        self._optimizer_thread.finished.connect(thread_finished)\n        self.stop_optimization = False\n        self._optimizer_thread.start()\n\n    @Slot(float, object, object)\n    def __update(self, _progress, weights, ssum_weights):\n        self.progressBarSet(_progress)\n        self._assign_instances(weights, ssum_weights)\n        self._redraw()\n\n    @Slot(object)\n    def __done(self, som):\n        self.enable_controls(True)\n        self.progressBarFinished()\n        self._assign_instances(som.weights, som.ssum_weights)\n        self._redraw()\n        # This is the first time we know what was selected (assuming that\n        # initialization is not set to random)\n        if self.__pending_selection is not None:\n            self.on_selection_change(self.__pending_selection)\n            self.__pending_selection = None\n        self.update_output()\n\n    def stop_optimization_and_wait(self):\n        if self._optimizer_thread is not None:\n            self.stop_optimization = True\n            self._optimizer.stop_optimization = True\n            self._optimizer_thread.quit()\n            self._optimizer_thread.wait()\n            self._optimizer_thread = None\n\n    def onDeleteWidget(self):\n        self.stop_optimization_and_wait()\n        self.clear()\n        super().onDeleteWidget()\n\n    def _assign_instances(self, weights, ssum_weights):\n        if self.cont_x is None:\n            return  # the widget is shutting down while signals still processed\n        assignments = SOM.winner_from_weights(\n            self.cont_x, weights, ssum_weights, self.hexagonal)\n        members = defaultdict(list)\n        for i, (x, y) in enumerate(assignments):\n            members[(x, y)].append(i)\n        members.pop(None, None)\n        self.cells = np.empty((self.size_x, self.size_y, 2), dtype=int)\n        self.member_data = np.empty(self.cont_x.shape[0], dtype=int)\n        index = 0\n        for x in range(self.size_x):\n            for y in range(self.size_y):\n                nmembers = len(members[(x, y)])\n                self.member_data[index:index + nmembers] = members[(x, y)]\n                self.cells[x, y] = [index, index + nmembers]\n                index += nmembers\n\n    def resizeEvent(self, event):\n        super().resizeEvent(event)\n        self.create_legend()  # re-wrap lines if necessary\n        self.rescale()\n\n    def rescale(self):\n        if self.legend:\n            leg_height = self.legend.boundingRect().height()\n            leg_extra = 1.5\n        else:\n            leg_height = 0\n            leg_extra = 1\n\n        vw, vh = self.view.width(), self.view.height() - leg_height\n        scale = min(vw / (self.size_x + 1),\n                    vh / ((self.size_y + leg_extra) * self._grid_factors[1]))\n        self.view.setTransform(QTransform.fromScale(scale, scale))\n        if self.hexagonal:\n            self.view.setSceneRect(\n                0, -1, self.size_x - 1,\n                (self.size_y + leg_extra) * sqrt3_2 + leg_height / scale)\n        else:\n            self.view.setSceneRect(\n                -0.25, -0.25, self.size_x - 0.5,\n                self.size_y - 0.5 + leg_height / scale)\n\n    def update_output(self):\n        if self.data is None:\n            self.Outputs.selected_data.send(None)\n            self.Outputs.annotated_data.send(None)\n            return\n\n        indices = np.zeros(len(self.data), dtype=int)\n        if self.selection is not None and np.any(self.selection):\n            for y in range(self.size_y):\n                for x in range(self.size_x):\n                    rows = self.get_member_indices(x, y)\n                    indices[rows] = self.selection[x, y]\n\n        if np.any(indices):\n            sel_data = create_groups_table(self.data, indices, False, \"Group\")\n            self.Outputs.selected_data.send(sel_data)\n        else:\n            self.Outputs.selected_data.send(None)\n\n        if np.max(indices) > 1:\n            annotated = create_groups_table(self.data, indices)\n        else:\n            annotated = create_annotated_table(\n                self.data, np.flatnonzero(indices))\n        self.Outputs.annotated_data.send(annotated)\n\n    def set_color_bins(self):\n        if self.attr_color is None:\n            self.thresholds = self.bin_labels = self.colors = None\n        elif self.attr_color.is_discrete:\n            self.thresholds = self.bin_labels = None\n            self.colors = self.attr_color.palette\n        else:\n            col = self.data.get_column_view(self.attr_color)[0].astype(float)\n            if self.attr_color.is_time:\n                binning = time_binnings(col, min_bins=4)[-1]\n            else:\n                binning = decimal_binnings(col, min_bins=4)[-1]\n            self.thresholds = binning.thresholds[1:-1]\n            self.bin_labels = (binning.labels[1:-1], binning.short_labels[1:-1])\n            palette = BinnedContinuousPalette.from_palette(\n                self.attr_color.palette, binning.thresholds)\n            self.colors = palette\n\n    def create_legend(self):\n        if self.legend is not None:\n            self.scene.removeItem(self.legend)\n            self.legend = None\n        if self.attr_color is None:\n            return\n\n        if self.attr_color.is_discrete:\n            names = self.attr_color.values\n        else:\n            names = self._bin_names()\n\n        items = []\n        size = 8\n        for name, color in zip(names, self.colors.qcolors):\n            item = QGraphicsItemGroup()\n            item.addToGroup(\n                CanvasRectangle(None, -size / 2, -size / 2, size, size,\n                                Qt.gray, color))\n            item.addToGroup(CanvasText(None, name, size, 0, Qt.AlignVCenter))\n            items.append(item)\n\n        self.legend = wrap_legend_items(\n            items, hspacing=20, vspacing=16 + size,\n            max_width=self.view.width() - 25)\n        self.legend.setFlags(self.legend.ItemIgnoresTransformations)\n        self.legend.setTransform(\n            QTransform.fromTranslate(-self.legend.boundingRect().width() / 2,\n                                     0))\n        self.scene.addItem(self.legend)\n        self.set_legend_pos()\n\n    def _bin_names(self):\n        labels, short_labels = self.bin_labels\n        return \\\n            [f\"< {labels[0]}\"] \\\n            + [f\"{x} - {y}\" for x, y in zip(labels, short_labels[1:])] \\\n            + [f\"≥ {labels[-1]}\"]\n\n    def set_legend_pos(self):\n        if self.legend is None:\n            return\n        self.legend.setPos(\n            self.size_x / 2,\n            (self.size_y + 0.2 + 0.3 * self.hexagonal) * self._grid_factors[1])\n\n    def send_report(self):\n        self.report_plot()\n        if self.attr_color:\n            self.report_caption(\n                f\"Self-organizing map colored by '{self.attr_color.name}'\")\n\n\ndef _draw_hexagon():\n    path = QPainterPath()\n    s = 0.5 / (np.sqrt(3) / 2)\n    path.moveTo(-0.5, -s / 2)\n    path.lineTo(-0.5, s / 2)\n    path.lineTo(0, s)\n    path.lineTo(0.5, s / 2)\n    path.lineTo(0.5, -s / 2)\n    path.lineTo(0, -s)\n    path.lineTo(-0.5, -s / 2)\n    return path\n\n\n_hexagon_path = _draw_hexagon()\n\n\nif __name__ == \"__main__\":  # pragma: no cover\n    WidgetPreview(OWSOM).run(Table(\"iris\"))\n",
            "new_file_content": "from collections import defaultdict, namedtuple\nfrom xml.sax.saxutils import escape\n\nimport numpy as np\nimport scipy.sparse as sp\n\nfrom AnyQt.QtCore import Qt, QRectF, pyqtSignal as Signal, QObject, QThread, \\\n    pyqtSlot as Slot\nfrom AnyQt.QtGui import QTransform, QPen, QBrush, QColor, QPainter, \\\n    QPainterPath, QKeyEvent\nfrom AnyQt.QtWidgets import \\\n    QGraphicsView, QGraphicsScene, \\\n    QGraphicsItem, QGraphicsRectItem, QGraphicsItemGroup, QSizePolicy, \\\n    QGraphicsPathItem\n\nfrom Orange.data import Table, Domain\nfrom Orange.data.util import array_equal\nfrom Orange.preprocess import decimal_binnings, time_binnings\nfrom Orange.projection.som import SOM\n\nfrom Orange.widgets import gui\nfrom Orange.widgets.widget import OWWidget, Msg, Input, Output\nfrom Orange.widgets.settings import \\\n    DomainContextHandler, ContextSetting, Setting\nfrom Orange.widgets.utils.itemmodels import DomainModel\nfrom Orange.widgets.utils.widgetpreview import WidgetPreview\nfrom Orange.widgets.utils.annotated_data import \\\n    create_annotated_table, create_groups_table, ANNOTATED_DATA_SIGNAL_NAME\nfrom Orange.widgets.utils.colorpalettes import \\\n    BinnedContinuousPalette, LimitedDiscretePalette\nfrom Orange.widgets.visualize.utils import CanvasRectangle, CanvasText\nfrom Orange.widgets.visualize.utils.plotutils import wrap_legend_items\n\n\nsqrt3_2 = np.sqrt(3) / 2\n\n\nclass SomView(QGraphicsView):\n    SelectionSet, SelectionNewGroup, SelectionAddToGroup, SelectionRemove \\\n        = range(4)\n    selection_changed = Signal(np.ndarray, int)\n    selection_moved = Signal(QKeyEvent)\n    selection_mark_changed = Signal(np.ndarray)\n\n    def __init__(self, scene):\n        super().__init__(scene)\n        self.__button_down_pos = None\n        self.size_x = self.size_y = 1\n        self.hexagonal = True\n\n    def set_dimensions(self, size_x, size_y, hexagonal):\n        self.size_x = size_x\n        self.size_y = size_y\n        self.hexagonal = hexagonal\n\n    def _get_marked_cells(self, event):\n        x0, y0 = self.__button_down_pos.x(), self.__button_down_pos.y()\n        pos = self.mapToScene(event.pos())\n        x1, y1 = pos.x(), pos.y()\n        x0, x1 = sorted((x0, x1))\n        y0, y1 = sorted((y0, y1))\n        selection = np.zeros((self.size_x, self.size_y), dtype=bool)\n        if self.hexagonal:\n            y0 = max(0, int(y0 / sqrt3_2 + 0.5))\n            y1 = min(self.size_y, int(np.ceil(y1 / sqrt3_2 + 0.5)))\n            for y in range(y0, y1):\n                if x1 < 0:\n                    continue\n                x0_ = max(0, int(x0 + 0.5 - (y % 2) / 2))\n                x1_ = min(self.size_x - y % 2,\n                          int(np.ceil(x1 + 0.5 - (y % 2) / 2)))\n                selection[x0_:x1_, y] = True\n        elif not(x1 < -0.5 or x0 > self.size_x - 0.5\n                 or y1 < -0.5 or y0 > self.size_y - 0.5):\n\n            def roundclip(z, zmax):\n                return int(np.clip(np.round(z), 0, zmax - 1))\\\n\n            x0 = roundclip(x0, self.size_x)\n            y0 = roundclip(y0, self.size_y)\n            x1 = roundclip(x1, self.size_x)\n            y1 = roundclip(y1, self.size_y)\n            selection[x0:x1 + 1, y0:y1 + 1] = True\n\n        return selection\n\n    def mousePressEvent(self, event):\n        if event.button() != Qt.LeftButton:\n            return\n\n        self.__button_down_pos = self.mapToScene(event.pos())\n        event.accept()\n\n    def mouseMoveEvent(self, event):\n        if event.buttons() != Qt.LeftButton:\n            return\n\n        self.selection_mark_changed.emit(self._get_marked_cells(event))\n        event.accept()\n\n    def mouseReleaseEvent(self, event):\n        if event.button() != Qt.LeftButton:\n            return\n\n        if event.modifiers() & Qt.ShiftModifier:\n            if event.modifiers() & Qt.ControlModifier:\n                action = self.SelectionAddToGroup\n            else:\n                action = self.SelectionNewGroup\n        elif event.modifiers() & Qt.AltModifier:\n            action = self.SelectionRemove\n        else:\n            action = self.SelectionSet\n        selection = self._get_marked_cells(event)\n        self.selection_changed.emit(selection, action)\n        event.accept()\n\n    def keyPressEvent(self, event: QKeyEvent):\n        if event.key() in (Qt.Key_Up, Qt.Key_Down, Qt.Key_Left, Qt.Key_Right):\n            self.selection_moved.emit(event)\n        else:\n            super().keyPressEvent(event)\n\n\nclass PieChart(QGraphicsItem):\n    def __init__(self, dist, r, colors):\n        super().__init__()\n        self.dist = dist\n        self.r = r\n        self.colors = colors\n\n    def boundingRect(self):\n        return QRectF(- self.r, - self.r, 2 * self.r, 2 * self.r)\n\n    def paint(self, painter, _option, _index):\n        painter.save()\n        start_angle = 0\n        pen = QPen(QBrush(Qt.black), 2)\n        pen.setCosmetic(True)\n        painter.setPen(pen)\n        painter.drawEllipse(self.boundingRect())\n        for angle, color in zip(self.dist * 16 * 360, self.colors):\n            if angle == 0:\n                continue\n            painter.setBrush(QBrush(color))\n            painter.setPen(Qt.NoPen)\n            painter.drawPie(self.boundingRect(), int(start_angle), int(angle))\n            start_angle += angle\n        painter.restore()\n\n\nclass ColoredCircle(QGraphicsItem):\n    def __init__(self, r, color, proportion):\n        super().__init__()\n        self.r = r\n        self.color = color\n        self.proportion = proportion\n\n    def boundingRect(self):\n        return QRectF(-self.r, -self.r, 2 * self.r, 2 * self.r)\n\n    def paint(self, painter, _option, _index):\n        painter.save()\n        pen = QPen(QBrush(self.color), 2)\n        pen.setCosmetic(True)\n        painter.setPen(pen)\n        painter.setBrush(QBrush(self.color.lighter(int(200 - 80 * self.proportion))))\n        painter.drawEllipse(self.boundingRect())\n        painter.restore()\n\n\nN_ITERATIONS = 200\n\n\nclass OWSOM(OWWidget):\n    name = \"Self-Organizing Map\"\n    description = \"Computation of self-organizing map.\"\n    icon = \"icons/SOM.svg\"\n    keywords = [\"SOM\"]\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    settingsHandler = DomainContextHandler()\n    auto_dimension = Setting(True)\n    size_x = Setting(10)\n    size_y = Setting(10)\n    hexagonal = Setting(1)\n    initialization = Setting(0)\n\n    attr_color = ContextSetting(None)\n    size_by_instances = Setting(True)\n    pie_charts = Setting(False)\n    selection = Setting(None, schema_only=True)\n\n    graph_name = \"view\"\n\n    _grid_pen = QPen(QBrush(QColor(224, 224, 224)), 2)\n    _grid_pen.setCosmetic(True)\n\n    OptControls = namedtuple(\n        \"OptControls\",\n        (\"shape\", \"auto_dim\", \"spin_x\", \"spin_y\", \"initialization\", \"start\")\n    )\n\n    class Warning(OWWidget.Warning):\n        ignoring_disc_variables = Msg(\"SOM ignores categorical variables.\")\n        missing_colors = \\\n            Msg(\"Some data instances have undefined value of '{}'.\")\n        missing_values = \\\n            Msg(\"{} data instance{} with undefined value(s) {} not shown.\")\n        single_attribute = Msg(\"Data contains a single numeric column.\")\n\n    class Error(OWWidget.Error):\n        no_numeric_variables = Msg(\"Data contains no numeric columns.\")\n        no_defined_rows = Msg(\"All rows contain at least one undefined value.\")\n\n    def __init__(self):\n        super().__init__()\n        self.__pending_selection = self.selection\n        self._optimizer = None\n        self._optimizer_thread = None\n        self.stop_optimization = False\n\n        self.data = self.cont_x = None\n        self.cells = self.member_data = None\n        self.selection = None\n        self.colors = self.thresholds = self.bin_labels = None\n\n        box = gui.vBox(self.controlArea, box=\"SOM\")\n        shape = gui.comboBox(\n            box, self, \"\", items=(\"Hexagonal grid\", \"Square grid\"))\n        shape.setCurrentIndex(1 - self.hexagonal)\n\n        box2 = gui.indentedBox(box, 10)\n        auto_dim = gui.checkBox(\n            box2, self, \"auto_dimension\", \"Set dimensions automatically\",\n            callback=self.on_auto_dimension_changed)\n        self.manual_box = box3 = gui.hBox(box2)\n        spinargs = dict(\n            value=\"\", widget=box3, master=self, minv=5, maxv=100, step=5,\n            alignment=Qt.AlignRight)\n        spin_x = gui.spin(**spinargs)\n        spin_x.setValue(self.size_x)\n        gui.widgetLabel(box3, \"×\")\n        spin_y = gui.spin(**spinargs)\n        spin_y.setValue(self.size_y)\n        gui.rubber(box3)\n        self.manual_box.setEnabled(not self.auto_dimension)\n\n        initialization = gui.comboBox(\n            box, self, \"initialization\",\n            items=(\"Initialize with PCA\", \"Random initialization\",\n                   \"Replicable random\"))\n\n        start = gui.button(\n            box, self, \"Restart\", callback=self.restart_som_pressed,\n            sizePolicy=(QSizePolicy.MinimumExpanding, QSizePolicy.Fixed))\n\n        self.opt_controls = self.OptControls(\n            shape, auto_dim, spin_x, spin_y, initialization, start)\n\n        box = gui.vBox(self.controlArea, \"Color\")\n        gui.comboBox(\n            box, self, \"attr_color\", searchable=True,\n            callback=self.on_attr_color_change,\n            model=DomainModel(placeholder=\"(Same color)\",\n                              valid_types=DomainModel.PRIMITIVE))\n        gui.checkBox(\n            box, self, \"pie_charts\", label=\"Show pie charts\",\n            callback=self.on_pie_chart_change)\n        gui.checkBox(\n            box, self, \"size_by_instances\", label=\"Size by number of instances\",\n            callback=self.on_attr_size_change)\n\n        gui.rubber(self.controlArea)\n\n        self.scene = QGraphicsScene(self)\n\n        self.view = SomView(self.scene)\n        self.view.setMinimumWidth(400)\n        self.view.setMinimumHeight(400)\n        self.view.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)\n        self.view.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)\n        self.view.setRenderHint(QPainter.Antialiasing)\n        self.view.selection_changed.connect(self.on_selection_change)\n        self.view.selection_moved.connect(self.on_selection_move)\n        self.view.selection_mark_changed.connect(self.on_selection_mark_change)\n        self.mainArea.layout().addWidget(self.view)\n\n        self.elements = None\n        self.grid = None\n        self.grid_cells = None\n        self.legend = None\n\n    @Inputs.data\n    def set_data(self, data):\n        def prepare_data():\n            if len(cont_attrs) < len(attrs):\n                self.Warning.ignoring_disc_variables()\n            if len(cont_attrs) == 1:\n                self.Warning.single_attribute()\n            x = Table.from_table(Domain(cont_attrs), data).X\n            if sp.issparse(x):\n                self.data = data\n                self.cont_x = x.tocsr()\n            else:\n                mask = np.all(np.isfinite(x), axis=1)\n                if not np.any(mask):\n                    self.Error.no_defined_rows()\n                else:\n                    if np.all(mask):\n                        self.data = data\n                        self.cont_x = x.copy()\n                    else:\n                        self.data = data[mask]\n                        self.cont_x = x[mask]\n                    self.cont_x -= np.min(self.cont_x, axis=0)[None, :]\n                    sums = np.sum(self.cont_x, axis=0)[None, :]\n                    sums[sums == 0] = 1\n                    self.cont_x /= sums\n\n        def set_warnings():\n            missing = len(data) - len(self.data)\n            if missing == 1:\n                self.Warning.missing_values(1, \"\", \"is\")\n            elif missing > 1:\n                self.Warning.missing_values(missing, \"s\", \"are\")\n\n        cont_x = self.cont_x.copy() if self.cont_x is not None else None\n        self.data = self.cont_x = None\n        self.closeContext()\n        self.clear_messages()\n\n        if data is not None:\n            attrs = data.domain.attributes\n            cont_attrs = [var for var in attrs if var.is_continuous]\n            if not cont_attrs:\n                self.Error.no_numeric_variables()\n            else:\n                prepare_data()\n\n        invalidated = cont_x is None or self.cont_x is None \\\n            or not array_equal(cont_x, self.cont_x)\n        if invalidated:\n            self.stop_optimization_and_wait()\n            self.clear()\n\n        if self.data is not None:\n            self.controls.attr_color.model().set_domain(data.domain)\n            self.attr_color = data.domain.class_var\n            set_warnings()\n\n        self.openContext(self.data)\n        self.set_color_bins()\n        self.create_legend()\n        if invalidated:\n            self.recompute_dimensions()\n            self.start_som()\n        else:\n            self._redraw()\n\n    def clear(self):\n        self.cells = self.member_data = None\n        self.attr_color = None\n        self.colors = self.thresholds = self.bin_labels = None\n        if self.elements is not None:\n            self.scene.removeItem(self.elements)\n            self.elements = None\n        self.clear_selection()\n        self.controls.attr_color.model().set_domain(None)\n\n    def recompute_dimensions(self):\n        if not self.auto_dimension or self.cont_x is None:\n            return\n        dim = max(5, int(np.ceil(np.sqrt(5 * np.sqrt(self.cont_x.shape[0])))))\n        self.opt_controls.spin_x.setValue(dim)\n        self.opt_controls.spin_y.setValue(dim)\n\n    def on_auto_dimension_changed(self):\n        self.manual_box.setEnabled(not self.auto_dimension)\n        if self.auto_dimension:\n            self.recompute_dimensions()\n        else:\n            spin_x = self.opt_controls.spin_x\n            spin_y = self.opt_controls.spin_y\n            dimx = int(5 * np.round(spin_x.value() / 5))\n            dimy = int(5 * np.round(spin_y.value() / 5))\n            spin_x.setValue(dimx)\n            spin_y.setValue(dimy)\n\n    def on_attr_color_change(self):\n        self.controls.pie_charts.setEnabled(self.attr_color is not None)\n        self.set_color_bins()\n        self.create_legend()\n        self.rescale()\n        self._redraw()\n\n    def on_attr_size_change(self):\n        self._redraw()\n\n    def on_pie_chart_change(self):\n        self._redraw()\n\n    def clear_selection(self):\n        self.selection = None\n        self.redraw_selection()\n\n    def on_selection_change(self, selection, action=SomView.SelectionSet):\n        if self.data is None:  # clicks on empty canvas\n            return\n        if self.selection is None:\n            self.selection = np.zeros(self.grid_cells.T.shape, dtype=np.int16)\n        if action == SomView.SelectionSet:\n            self.selection[:] = 0\n            self.selection[selection] = 1\n        elif action == SomView.SelectionAddToGroup:\n            self.selection[selection] = max(1, np.max(self.selection))\n        elif action == SomView.SelectionNewGroup:\n            self.selection[selection] = 1 + np.max(self.selection)\n        elif action & SomView.SelectionRemove:\n            self.selection[selection] = 0\n        self.redraw_selection()\n        self.update_output()\n\n    def on_selection_move(self, event: QKeyEvent):\n        if self.selection is None or not np.any(self.selection):\n            if event.key() in (Qt.Key_Right, Qt.Key_Down):\n                x = y = 0\n            else:\n                x = self.size_x - 1\n                y = self.size_y - 1\n        else:\n            x, y = np.nonzero(self.selection)\n            if len(x) > 1:\n                return\n            if event.key() == Qt.Key_Up and y > 0:\n                y -= 1\n            if event.key() == Qt.Key_Down and y < self.size_y - 1:\n                y += 1\n            if event.key() == Qt.Key_Left and x:\n                x -= 1\n            if event.key() == Qt.Key_Right and x < self.size_x - 1:\n                x += 1\n            x -= self.hexagonal and x == self.size_x - 1 and y % 2\n\n        if self.selection is not None and self.selection[x, y]:\n            return\n        selection = np.zeros(self.grid_cells.shape, dtype=bool)\n        selection[x, y] = True\n        self.on_selection_change(selection)\n\n    def on_selection_mark_change(self, marks):\n        self.redraw_selection(marks=marks)\n\n    def redraw_selection(self, marks=None):\n        if self.grid_cells is None:\n            return\n\n        sel_pen = QPen(QBrush(QColor(128, 128, 128)), 2)\n        sel_pen.setCosmetic(True)\n        mark_pen = QPen(QBrush(QColor(128, 128, 128)), 4)\n        mark_pen.setCosmetic(True)\n        pens = [self._grid_pen, sel_pen]\n\n        mark_brush = QBrush(QColor(224, 255, 255))\n        sels = self.selection is not None and np.max(self.selection)\n        palette = LimitedDiscretePalette(number_of_colors=sels + 1)\n        brushes = [QBrush(Qt.NoBrush)] + \\\n                  [QBrush(palette[i].lighter(165)) for i in range(sels)]\n\n        for y in range(self.size_y):\n            for x in range(self.size_x - (y % 2) * self.hexagonal):\n                cell = self.grid_cells[y, x]\n                marked = marks is not None and marks[x, y]\n                sel_group = self.selection is not None and self.selection[x, y]\n                if marked:\n                    cell.setBrush(mark_brush)\n                    cell.setPen(mark_pen)\n                else:\n                    cell.setBrush(brushes[sel_group])\n                    cell.setPen(pens[bool(sel_group)])\n                cell.setZValue(marked or sel_group)\n\n    def restart_som_pressed(self):\n        if self._optimizer_thread is not None:\n            self.stop_optimization = True\n            self._optimizer.stop_optimization = True\n        else:\n            self.start_som()\n\n    def start_som(self):\n        self.read_controls()\n        self.update_layout()\n        self.clear_selection()\n        if self.cont_x is not None:\n            self.enable_controls(False)\n            self._recompute_som()\n        else:\n            self.update_output()\n\n    def read_controls(self):\n        c = self.opt_controls\n        self.hexagonal = c.shape.currentIndex() == 0\n        self.size_x = c.spin_x.value()\n        self.size_y = c.spin_y.value()\n\n    def enable_controls(self, enable):\n        c = self.opt_controls\n        c.shape.setEnabled(enable)\n        c.auto_dim.setEnabled(enable)\n        c.start.setText(\"Start\" if enable else \"Stop\")\n\n    def update_layout(self):\n        self.set_legend_pos()\n        if self.elements:  # Prevent having redrawn grid but with old elements\n            self.scene.removeItem(self.elements)\n            self.elements = None\n        self.redraw_grid()\n        self.rescale()\n\n    def _redraw(self):\n        self.Warning.missing_colors.clear()\n        if self.elements:\n            self.scene.removeItem(self.elements)\n            self.elements = None\n        self.view.set_dimensions(self.size_x, self.size_y, self.hexagonal)\n\n        if self.cells is None:\n            return\n        sizes = self.cells[:, :, 1] - self.cells[:, :, 0]\n        sizes = sizes.astype(float)\n        if not self.size_by_instances:\n            sizes[sizes != 0] = 0.8\n        else:\n            sizes *= 0.8 / np.max(sizes)\n\n        self.elements = QGraphicsItemGroup()\n        self.scene.addItem(self.elements)\n        if self.attr_color is None:\n            self._draw_same_color(sizes)\n        elif self.pie_charts:\n            self._draw_pie_charts(sizes)\n        else:\n            self._draw_colored_circles(sizes)\n\n    @property\n    def _grid_factors(self):\n        return (0.5, sqrt3_2) if self.hexagonal else (0, 1)\n\n    def _draw_same_color(self, sizes):\n        fx, fy = self._grid_factors\n        color = QColor(64, 64, 64)\n        for y in range(self.size_y):\n            for x in range(self.size_x - self.hexagonal * (y % 2)):\n                r = sizes[x, y]\n                n = len(self.get_member_indices(x, y))\n                if not r:\n                    continue\n                ellipse = ColoredCircle(r / 2, color, 0)\n                ellipse.setPos(x + (y % 2) * fx, y * fy)\n                ellipse.setToolTip(f\"{n} instances\")\n                self.elements.addToGroup(ellipse)\n\n    def _get_color_column(self):\n        color_column = \\\n            self.data.get_column_view(self.attr_color)[0].astype(float,\n                                                                 copy=False)\n        if self.attr_color.is_discrete:\n            with np.errstate(invalid=\"ignore\"):\n                int_col = color_column.astype(int)\n            int_col[np.isnan(color_column)] = len(self.colors)\n        else:\n            int_col = np.zeros(len(color_column), dtype=int)\n            # The following line is unnecessary because rows with missing\n            # numeric data are excluded. Uncomment it if you change SOM to\n            # tolerate missing values.\n            # int_col[np.isnan(color_column)] = len(self.colors)\n            for i, thresh in enumerate(self.thresholds, start=1):\n                int_col[color_column >= thresh] = i\n        return int_col\n\n    def _tooltip(self, colors, distribution):\n        if self.attr_color.is_discrete:\n            values = self.attr_color.values\n        else:\n            values = self._bin_names()\n        tot = np.sum(distribution)\n        nbhp = \"\\N{NON-BREAKING HYPHEN}\"\n        return '<table style=\"white-space: nowrap\">' + \"\".join(f\"\"\"\n            <tr>\n                <td>\n                    <font color={color.name()}>■</font>\n                    <b>{escape(val).replace(\"-\", nbhp)}</b>:\n                </td>\n                <td>\n                    {n} ({n / tot * 100:.1f}&nbsp;%)\n                </td>\n            </tr>\n            \"\"\" for color, val, n in zip(colors, values, distribution) if n) \\\n            + \"</table>\"\n\n    def _draw_pie_charts(self, sizes):\n        fx, fy = self._grid_factors\n        color_column = self._get_color_column()\n        colors = self.colors.qcolors_w_nan\n        for y in range(self.size_y):\n            for x in range(self.size_x - self.hexagonal * (y % 2)):\n                r = sizes[x, y]\n                if not r:\n                    self.grid_cells[y, x].setToolTip(\"\")\n                    continue\n                members = self.get_member_indices(x, y)\n                color_dist = np.bincount(color_column[members],\n                                         minlength=len(colors))\n                rel_color_dist = color_dist.astype(float) / len(members)\n                pie = PieChart(rel_color_dist, r / 2, colors)\n                pie.setToolTip(self._tooltip(colors, color_dist))\n                self.elements.addToGroup(pie)\n                pie.setPos(x + (y % 2) * fx, y * fy)\n\n    def _draw_colored_circles(self, sizes):\n        fx, fy = self._grid_factors\n        color_column = self._get_color_column()\n        qcolors = self.colors.qcolors_w_nan\n        for y in range(self.size_y):\n            for x in range(self.size_x - self.hexagonal * (y % 2)):\n                r = sizes[x, y]\n                if not r:\n                    continue\n                members = self.get_member_indices(x, y)\n                color_dist = color_column[members]\n                color_dist = color_dist[color_dist < len(self.colors)]\n                if len(color_dist) != len(members):\n                    self.Warning.missing_colors(self.attr_color.name)\n                bc = np.bincount(color_dist, minlength=len(self.colors))\n                color = qcolors[np.argmax(bc)]\n                ellipse = ColoredCircle(r / 2, color, np.max(bc) / len(members))\n                ellipse.setPos(x + (y % 2) * fx, y * fy)\n                ellipse.setToolTip(self._tooltip(qcolors, bc))\n                self.elements.addToGroup(ellipse)\n\n    def redraw_grid(self):\n        if self.grid is not None:\n            self.scene.removeItem(self.grid)\n        self.grid = QGraphicsItemGroup()\n        self.grid.setZValue(-200)\n        self.grid_cells = np.full((self.size_y, self.size_x), None)\n        for y in range(self.size_y):\n            for x in range(self.size_x - (y % 2) * self.hexagonal):\n                if self.hexagonal:\n                    cell = QGraphicsPathItem(_hexagon_path)\n                    cell.setPos(x + (y % 2) / 2, y * sqrt3_2)\n                else:\n                    cell = QGraphicsRectItem(x - 0.5, y - 0.5, 1, 1)\n                self.grid_cells[y, x] = cell\n                cell.setPen(self._grid_pen)\n                self.grid.addToGroup(cell)\n        self.scene.addItem(self.grid)\n\n    def get_member_indices(self, x, y):\n        i, j = self.cells[x, y]\n        return self.member_data[i:j]\n\n    def _recompute_som(self):\n        if self.cont_x is None:\n            return\n\n        som = SOM(\n            self.size_x, self.size_y,\n            hexagonal=self.hexagonal,\n            pca_init=self.initialization == 0,\n            random_seed=0 if self.initialization == 2 else None\n        )\n\n        class Optimizer(QObject):\n            update = Signal(float, np.ndarray, np.ndarray)\n            done = Signal(SOM)\n            stopped = Signal()\n            stop_optimization = False\n\n            def __init__(self, data, som):\n                super().__init__()\n                self.som = som\n                self.data = data\n\n            def callback(self, progress):\n                self.update.emit(\n                    progress,\n                    self.som.weights.copy(), self.som.ssum_weights.copy())\n                return not self.stop_optimization\n\n            def run(self):\n                try:\n                    self.som.fit(self.data, N_ITERATIONS,\n                                 callback=self.callback)\n                    # Report an exception, but still remove the thread\n                finally:\n                    self.done.emit(self.som)\n                    self.stopped.emit()\n\n        def thread_finished():\n            self._optimizer = None\n            self._optimizer_thread = None\n\n        self.progressBarInit()\n\n        self._optimizer = Optimizer(self.cont_x, som)\n        self._optimizer_thread = QThread()\n        self._optimizer_thread.setStackSize(5 * 2 ** 20)\n        self._optimizer.update.connect(self.__update)\n        self._optimizer.done.connect(self.__done)\n        self._optimizer.stopped.connect(self._optimizer_thread.quit)\n        self._optimizer.moveToThread(self._optimizer_thread)\n        self._optimizer_thread.started.connect(self._optimizer.run)\n        self._optimizer_thread.finished.connect(thread_finished)\n        self.stop_optimization = False\n        self._optimizer_thread.start()\n\n    @Slot(float, object, object)\n    def __update(self, _progress, weights, ssum_weights):\n        self.progressBarSet(_progress)\n        self._assign_instances(weights, ssum_weights)\n        self._redraw()\n\n    @Slot(object)\n    def __done(self, som):\n        self.enable_controls(True)\n        self.progressBarFinished()\n        self._assign_instances(som.weights, som.ssum_weights)\n        self._redraw()\n        # This is the first time we know what was selected (assuming that\n        # initialization is not set to random)\n        if self.__pending_selection is not None:\n            self.on_selection_change(self.__pending_selection)\n            self.__pending_selection = None\n        self.update_output()\n\n    def stop_optimization_and_wait(self):\n        if self._optimizer_thread is not None:\n            self.stop_optimization = True\n            self._optimizer.stop_optimization = True\n            self._optimizer_thread.quit()\n            self._optimizer_thread.wait()\n            self._optimizer_thread = None\n\n    def onDeleteWidget(self):\n        self.stop_optimization_and_wait()\n        self.clear()\n        super().onDeleteWidget()\n\n    def _assign_instances(self, weights, ssum_weights):\n        if self.cont_x is None:\n            return  # the widget is shutting down while signals still processed\n        assignments = SOM.winner_from_weights(\n            self.cont_x, weights, ssum_weights, self.hexagonal)\n        members = defaultdict(list)\n        for i, (x, y) in enumerate(assignments):\n            members[(x, y)].append(i)\n        members.pop(None, None)\n        self.cells = np.empty((self.size_x, self.size_y, 2), dtype=int)\n        self.member_data = np.empty(self.cont_x.shape[0], dtype=int)\n        index = 0\n        for x in range(self.size_x):\n            for y in range(self.size_y):\n                nmembers = len(members[(x, y)])\n                self.member_data[index:index + nmembers] = members[(x, y)]\n                self.cells[x, y] = [index, index + nmembers]\n                index += nmembers\n\n    def resizeEvent(self, event):\n        super().resizeEvent(event)\n        self.create_legend()  # re-wrap lines if necessary\n        self.rescale()\n\n    def rescale(self):\n        if self.legend:\n            leg_height = self.legend.boundingRect().height()\n            leg_extra = 1.5\n        else:\n            leg_height = 0\n            leg_extra = 1\n\n        vw, vh = self.view.width(), self.view.height() - leg_height\n        scale = min(vw / (self.size_x + 1),\n                    vh / ((self.size_y + leg_extra) * self._grid_factors[1]))\n        self.view.setTransform(QTransform.fromScale(scale, scale))\n        if self.hexagonal:\n            self.view.setSceneRect(\n                0, -1, self.size_x - 1,\n                (self.size_y + leg_extra) * sqrt3_2 + leg_height / scale)\n        else:\n            self.view.setSceneRect(\n                -0.25, -0.25, self.size_x - 0.5,\n                self.size_y - 0.5 + leg_height / scale)\n\n    def update_output(self):\n        if self.data is None:\n            self.Outputs.selected_data.send(None)\n            self.Outputs.annotated_data.send(None)\n            return\n\n        indices = np.zeros(len(self.data), dtype=int)\n        if self.selection is not None and np.any(self.selection):\n            for y in range(self.size_y):\n                for x in range(self.size_x):\n                    rows = self.get_member_indices(x, y)\n                    indices[rows] = self.selection[x, y]\n\n        if np.any(indices):\n            sel_data = create_groups_table(self.data, indices, False, \"Group\")\n            self.Outputs.selected_data.send(sel_data)\n        else:\n            self.Outputs.selected_data.send(None)\n\n        if np.max(indices) > 1:\n            annotated = create_groups_table(self.data, indices)\n        else:\n            annotated = create_annotated_table(\n                self.data, np.flatnonzero(indices))\n        self.Outputs.annotated_data.send(annotated)\n\n    def set_color_bins(self):\n        if self.attr_color is None:\n            self.thresholds = self.bin_labels = self.colors = None\n        elif self.attr_color.is_discrete:\n            self.thresholds = self.bin_labels = None\n            self.colors = self.attr_color.palette\n        else:\n            col = self.data.get_column_view(self.attr_color)[0].astype(float)\n            if self.attr_color.is_time:\n                binning = time_binnings(col, min_bins=4)[-1]\n            else:\n                binning = decimal_binnings(col, min_bins=4)[-1]\n            self.thresholds = binning.thresholds[1:-1]\n            self.bin_labels = (binning.labels[1:-1], binning.short_labels[1:-1])\n            palette = BinnedContinuousPalette.from_palette(\n                self.attr_color.palette, binning.thresholds)\n            self.colors = palette\n\n    def create_legend(self):\n        if self.legend is not None:\n            self.scene.removeItem(self.legend)\n            self.legend = None\n        if self.attr_color is None:\n            return\n\n        if self.attr_color.is_discrete:\n            names = self.attr_color.values\n        else:\n            names = self._bin_names()\n\n        items = []\n        size = 8\n        for name, color in zip(names, self.colors.qcolors):\n            item = QGraphicsItemGroup()\n            item.addToGroup(\n                CanvasRectangle(None, -size / 2, -size / 2, size, size,\n                                Qt.gray, color))\n            item.addToGroup(CanvasText(None, name, size, 0, Qt.AlignVCenter))\n            items.append(item)\n\n        self.legend = wrap_legend_items(\n            items, hspacing=20, vspacing=16 + size,\n            max_width=self.view.width() - 25)\n        self.legend.setFlags(self.legend.ItemIgnoresTransformations)\n        self.legend.setTransform(\n            QTransform.fromTranslate(-self.legend.boundingRect().width() / 2,\n                                     0))\n        self.scene.addItem(self.legend)\n        self.set_legend_pos()\n\n    def _bin_names(self):\n        labels, short_labels = self.bin_labels\n        return \\\n            [f\"< {labels[0]}\"] \\\n            + [f\"{x} - {y}\" for x, y in zip(labels, short_labels[1:])] \\\n            + [f\"≥ {labels[-1]}\"]\n\n    def set_legend_pos(self):\n        if self.legend is None:\n            return\n        self.legend.setPos(\n            self.size_x / 2,\n            (self.size_y + 0.2 + 0.3 * self.hexagonal) * self._grid_factors[1])\n\n    def send_report(self):\n        self.report_plot()\n        if self.attr_color:\n            self.report_caption(\n                f\"Self-organizing map colored by '{self.attr_color.name}'\")\n\n\ndef _draw_hexagon():\n    path = QPainterPath()\n    s = 0.5 / (np.sqrt(3) / 2)\n    path.moveTo(-0.5, -s / 2)\n    path.lineTo(-0.5, s / 2)\n    path.lineTo(0, s)\n    path.lineTo(0.5, s / 2)\n    path.lineTo(0.5, -s / 2)\n    path.lineTo(0, -s)\n    path.lineTo(-0.5, -s / 2)\n    return path\n\n\n_hexagon_path = _draw_hexagon()\n\n\nif __name__ == \"__main__\":  # pragma: no cover\n    WidgetPreview(OWSOM).run(Table(\"iris\"))\n",
            "header": {
                "file": {
                    "path": "Orange/widgets/unsupervised/owsom.py"
                },
                "misc_line": null
            },
            "index_line": {
                "old_commit_hash": "65cdda50f",
                "new_commit_hash": "f29d774cd",
                "mode": "100644"
            },
            "is_binary_file": false,
            "binary_line": null,
            "minus_file": {
                "path": "a/Orange/widgets/unsupervised/owsom.py"
            },
            "plus_file": {
                "path": "b/Orange/widgets/unsupervised/owsom.py"
            },
            "hunks": [
                {
                    "descriptor": {
                        "old_range": {
                            "start": 14,
                            "length": 6
                        },
                        "new_range": {
                            "start": 14,
                            "length": 7
                        },
                        "section": "from AnyQt.QtWidgets import \\"
                    },
                    "line_group": {
                        "all_lines": [
                            {
                                "content": "    QGraphicsPathItem",
                                "type": "context"
                            },
                            {
                                "content": "",
                                "type": "context"
                            },
                            {
                                "content": "from Orange.data import Table, Domain",
                                "type": "context"
                            },
                            {
                                "content": "from Orange.data.util import array_equal",
                                "type": "added"
                            },
                            {
                                "content": "from Orange.preprocess import decimal_binnings, time_binnings",
                                "type": "context"
                            },
                            {
                                "content": "from Orange.projection.som import SOM",
                                "type": "context"
                            },
                            {
                                "content": "",
                                "type": "context"
                            }
                        ]
                    },
                    "modified_entities": [],
                    "added_entities": [
                        {
                            "file_name": "Orange/widgets/unsupervised/owsom.py",
                            "type": "statement",
                            "name": "",
                            "content": "from Orange.data.util import array_equal",
                            "ast_type_str": "ImportFrom",
                            "start_lineno": 17,
                            "end_lineno": 17,
                            "parent": null
                        }
                    ],
                    "deleted_entities": []
                },
                {
                    "descriptor": {
                        "old_range": {
                            "start": 330,
                            "length": 12
                        },
                        "new_range": {
                            "start": 331,
                            "length": 10
                        },
                        "section": "class OWSOM(OWWidget):"
                    },
                    "line_group": {
                        "all_lines": [
                            {
                                "content": "            elif missing > 1:",
                                "type": "context"
                            },
                            {
                                "content": "                self.Warning.missing_values(missing, \"s\", \"are\")",
                                "type": "context"
                            },
                            {
                                "content": "",
                                "type": "context"
                            },
                            {
                                "content": "        self.stop_optimization_and_wait()",
                                "type": "deleted"
                            },
                            {
                                "content": "",
                                "type": "deleted"
                            },
                            {
                                "content": "        cont_x = self.cont_x.copy() if self.cont_x is not None else None",
                                "type": "added"
                            },
                            {
                                "content": "        self.data = self.cont_x = None",
                                "type": "added"
                            },
                            {
                                "content": "        self.closeContext()",
                                "type": "context"
                            },
                            {
                                "content": "        self.clear()",
                                "type": "deleted"
                            },
                            {
                                "content": "        self.Error.clear()",
                                "type": "deleted"
                            },
                            {
                                "content": "        self.Warning.clear()",
                                "type": "deleted"
                            },
                            {
                                "content": "        self.clear_messages()",
                                "type": "added"
                            },
                            {
                                "content": "",
                                "type": "context"
                            },
                            {
                                "content": "        if data is not None:",
                                "type": "context"
                            },
                            {
                                "content": "            attrs = data.domain.attributes",
                                "type": "context"
                            }
                        ]
                    },
                    "modified_entities": [
                        {
                            "file_name": "Orange/widgets/unsupervised/owsom.py",
                            "type": "method",
                            "name": "OWSOM.set_data",
                            "content": "class OWSOM(OWWidget):\n    name = \"Self-Organizing Map\"\n    description = \"Computation of self-organizing map.\"\n    icon = \"icons/SOM.svg\"\n    keywords = [\"SOM\"]\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    settingsHandler = DomainContextHandler()\n    auto_dimension = Setting(True)\n    size_x = Setting(10)\n    size_y = Setting(10)\n    hexagonal = Setting(1)\n    initialization = Setting(0)\n\n    attr_color = ContextSetting(None)\n    size_by_instances = Setting(True)\n    pie_charts = Setting(False)\n    selection = Setting(None, schema_only=True)\n\n    graph_name = \"view\"\n\n    _grid_pen = QPen(QBrush(QColor(224, 224, 224)), 2)\n    _grid_pen.setCosmetic(True)\n\n    OptControls = namedtuple(\n        \"OptControls\",\n        (\"shape\", \"auto_dim\", \"spin_x\", \"spin_y\", \"initialization\", \"start\")\n    )\n\n    class Warning(OWWidget.Warning):\n        ignoring_disc_variables = Msg(\"SOM ignores categorical variables.\")\n        missing_colors = \\\n            Msg(\"Some data instances have undefined value of '{}'.\")\n        missing_values = \\\n            Msg(\"{} data instance{} with undefined value(s) {} not shown.\")\n        single_attribute = Msg(\"Data contains a single numeric column.\")\n\n    class Error(OWWidget.Error):\n        no_numeric_variables = Msg(\"Data contains no numeric columns.\")\n        no_defined_rows = Msg(\"All rows contain at least one undefined value.\")\n\n    def __init__(self):\n        super().__init__()\n        self.__pending_selection = self.selection\n        self._optimizer = None\n        self._optimizer_thread = None\n        self.stop_optimization = False\n\n        self.data = self.cont_x = None\n        self.cells = self.member_data = None\n        self.selection = None\n        self.colors = self.thresholds = self.bin_labels = None\n\n        box = gui.vBox(self.controlArea, box=\"SOM\")\n        shape = gui.comboBox(\n            box, self, \"\", items=(\"Hexagonal grid\", \"Square grid\"))\n        shape.setCurrentIndex(1 - self.hexagonal)\n\n        box2 = gui.indentedBox(box, 10)\n        auto_dim = gui.checkBox(\n            box2, self, \"auto_dimension\", \"Set dimensions automatically\",\n            callback=self.on_auto_dimension_changed)\n        self.manual_box = box3 = gui.hBox(box2)\n        spinargs = dict(\n            value=\"\", widget=box3, master=self, minv=5, maxv=100, step=5,\n            alignment=Qt.AlignRight)\n        spin_x = gui.spin(**spinargs)\n        spin_x.setValue(self.size_x)\n        gui.widgetLabel(box3, \"×\")\n        spin_y = gui.spin(**spinargs)\n        spin_y.setValue(self.size_y)\n        gui.rubber(box3)\n        self.manual_box.setEnabled(not self.auto_dimension)\n\n        initialization = gui.comboBox(\n            box, self, \"initialization\",\n            items=(\"Initialize with PCA\", \"Random initialization\",\n                   \"Replicable random\"))\n\n        start = gui.button(\n            box, self, \"Restart\", callback=self.restart_som_pressed,\n            sizePolicy=(QSizePolicy.MinimumExpanding, QSizePolicy.Fixed))\n\n        self.opt_controls = self.OptControls(\n            shape, auto_dim, spin_x, spin_y, initialization, start)\n\n        box = gui.vBox(self.controlArea, \"Color\")\n        gui.comboBox(\n            box, self, \"attr_color\", searchable=True,\n            callback=self.on_attr_color_change,\n            model=DomainModel(placeholder=\"(Same color)\",\n                              valid_types=DomainModel.PRIMITIVE))\n        gui.checkBox(\n            box, self, \"pie_charts\", label=\"Show pie charts\",\n            callback=self.on_pie_chart_change)\n        gui.checkBox(\n            box, self, \"size_by_instances\", label=\"Size by number of instances\",\n            callback=self.on_attr_size_change)\n\n        gui.rubber(self.controlArea)\n\n        self.scene = QGraphicsScene(self)\n\n        self.view = SomView(self.scene)\n        self.view.setMinimumWidth(400)\n        self.view.setMinimumHeight(400)\n        self.view.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)\n        self.view.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)\n        self.view.setRenderHint(QPainter.Antialiasing)\n        self.view.selection_changed.connect(self.on_selection_change)\n        self.view.selection_moved.connect(self.on_selection_move)\n        self.view.selection_mark_changed.connect(self.on_selection_mark_change)\n        self.mainArea.layout().addWidget(self.view)\n\n        self.elements = None\n        self.grid = None\n        self.grid_cells = None\n        self.legend = None\n\n    @Inputs.data\n    def set_data(self, data):\n        def prepare_data():\n            if len(cont_attrs) < len(attrs):\n                self.Warning.ignoring_disc_variables()\n            if len(cont_attrs) == 1:\n                self.Warning.single_attribute()\n            x = Table.from_table(Domain(cont_attrs), data).X\n            if sp.issparse(x):\n                self.data = data\n                self.cont_x = x.tocsr()\n            else:\n                mask = np.all(np.isfinite(x), axis=1)\n                if not np.any(mask):\n                    self.Error.no_defined_rows()\n                else:\n                    if np.all(mask):\n                        self.data = data\n                        self.cont_x = x.copy()\n                    else:\n                        self.data = data[mask]\n                        self.cont_x = x[mask]\n                    self.cont_x -= np.min(self.cont_x, axis=0)[None, :]\n                    sums = np.sum(self.cont_x, axis=0)[None, :]\n                    sums[sums == 0] = 1\n                    self.cont_x /= sums\n\n        def set_warnings():\n            missing = len(data) - len(self.data)\n            if missing == 1:\n                self.Warning.missing_values(1, \"\", \"is\")\n            elif missing > 1:\n                self.Warning.missing_values(missing, \"s\", \"are\")\n\n        self.stop_optimization_and_wait()\n\n        self.closeContext()\n        self.clear()\n        self.Error.clear()\n        self.Warning.clear()\n\n        if data is not None:\n            attrs = data.domain.attributes\n            cont_attrs = [var for var in attrs if var.is_continuous]\n            if not cont_attrs:\n                self.Error.no_numeric_variables()\n            else:\n                prepare_data()\n\n        if self.data is not None:\n            self.controls.attr_color.model().set_domain(data.domain)\n            self.attr_color = data.domain.class_var\n            set_warnings()\n\n        self.openContext(self.data)\n        self.set_color_bins()\n        self.create_legend()\n        self.recompute_dimensions()\n        self.start_som()\n\n    def clear(self):\n        self.data = self.cont_x = None\n        self.cells = self.member_data = None\n        self.attr_color = None\n        self.colors = self.thresholds = self.bin_labels = None\n        if self.elements is not None:\n            self.scene.removeItem(self.elements)\n            self.elements = None\n        self.clear_selection()\n        self.controls.attr_color.model().set_domain(None)\n        self.Warning.clear()\n        self.Error.clear()\n\n    def recompute_dimensions(self):\n        if not self.auto_dimension or self.cont_x is None:\n            return\n        dim = max(5, int(np.ceil(np.sqrt(5 * np.sqrt(self.cont_x.shape[0])))))\n        self.opt_controls.spin_x.setValue(dim)\n        self.opt_controls.spin_y.setValue(dim)\n\n    def on_auto_dimension_changed(self):\n        self.manual_box.setEnabled(not self.auto_dimension)\n        if self.auto_dimension:\n            self.recompute_dimensions()\n        else:\n            spin_x = self.opt_controls.spin_x\n            spin_y = self.opt_controls.spin_y\n            dimx = int(5 * np.round(spin_x.value() / 5))\n            dimy = int(5 * np.round(spin_y.value() / 5))\n            spin_x.setValue(dimx)\n            spin_y.setValue(dimy)\n\n    def on_attr_color_change(self):\n        self.controls.pie_charts.setEnabled(self.attr_color is not None)\n        self.set_color_bins()\n        self.create_legend()\n        self.rescale()\n        self._redraw()\n\n    def on_attr_size_change(self):\n        self._redraw()\n\n    def on_pie_chart_change(self):\n        self._redraw()\n\n    def clear_selection(self):\n        self.selection = None\n        self.redraw_selection()\n\n    def on_selection_change(self, selection, action=SomView.SelectionSet):\n        if self.data is None:  # clicks on empty canvas\n            return\n        if self.selection is None:\n            self.selection = np.zeros(self.grid_cells.T.shape, dtype=np.int16)\n        if action == SomView.SelectionSet:\n            self.selection[:] = 0\n            self.selection[selection] = 1\n        elif action == SomView.SelectionAddToGroup:\n            self.selection[selection] = max(1, np.max(self.selection))\n        elif action == SomView.SelectionNewGroup:\n            self.selection[selection] = 1 + np.max(self.selection)\n        elif action & SomView.SelectionRemove:\n            self.selection[selection] = 0\n        self.redraw_selection()\n        self.update_output()\n\n    def on_selection_move(self, event: QKeyEvent):\n        if self.selection is None or not np.any(self.selection):\n            if event.key() in (Qt.Key_Right, Qt.Key_Down):\n                x = y = 0\n            else:\n                x = self.size_x - 1\n                y = self.size_y - 1\n        else:\n            x, y = np.nonzero(self.selection)\n            if len(x) > 1:\n                return\n            if event.key() == Qt.Key_Up and y > 0:\n                y -= 1\n            if event.key() == Qt.Key_Down and y < self.size_y - 1:\n                y += 1\n            if event.key() == Qt.Key_Left and x:\n                x -= 1\n            if event.key() == Qt.Key_Right and x < self.size_x - 1:\n                x += 1\n            x -= self.hexagonal and x == self.size_x - 1 and y % 2\n\n        if self.selection is not None and self.selection[x, y]:\n            return\n        selection = np.zeros(self.grid_cells.shape, dtype=bool)\n        selection[x, y] = True\n        self.on_selection_change(selection)\n\n    def on_selection_mark_change(self, marks):\n        self.redraw_selection(marks=marks)\n\n    def redraw_selection(self, marks=None):\n        if self.grid_cells is None:\n            return\n\n        sel_pen = QPen(QBrush(QColor(128, 128, 128)), 2)\n        sel_pen.setCosmetic(True)\n        mark_pen = QPen(QBrush(QColor(128, 128, 128)), 4)\n        mark_pen.setCosmetic(True)\n        pens = [self._grid_pen, sel_pen]\n\n        mark_brush = QBrush(QColor(224, 255, 255))\n        sels = self.selection is not None and np.max(self.selection)\n        palette = LimitedDiscretePalette(number_of_colors=sels + 1)\n        brushes = [QBrush(Qt.NoBrush)] + \\\n                  [QBrush(palette[i].lighter(165)) for i in range(sels)]\n\n        for y in range(self.size_y):\n            for x in range(self.size_x - (y % 2) * self.hexagonal):\n                cell = self.grid_cells[y, x]\n                marked = marks is not None and marks[x, y]\n                sel_group = self.selection is not None and self.selection[x, y]\n                if marked:\n                    cell.setBrush(mark_brush)\n                    cell.setPen(mark_pen)\n                else:\n                    cell.setBrush(brushes[sel_group])\n                    cell.setPen(pens[bool(sel_group)])\n                cell.setZValue(marked or sel_group)\n\n    def restart_som_pressed(self):\n        if self._optimizer_thread is not None:\n            self.stop_optimization = True\n            self._optimizer.stop_optimization = True\n        else:\n            self.start_som()\n\n    def start_som(self):\n        self.read_controls()\n        self.update_layout()\n        self.clear_selection()\n        if self.cont_x is not None:\n            self.enable_controls(False)\n            self._recompute_som()\n        else:\n            self.update_output()\n\n    def read_controls(self):\n        c = self.opt_controls\n        self.hexagonal = c.shape.currentIndex() == 0\n        self.size_x = c.spin_x.value()\n        self.size_y = c.spin_y.value()\n\n    def enable_controls(self, enable):\n        c = self.opt_controls\n        c.shape.setEnabled(enable)\n        c.auto_dim.setEnabled(enable)\n        c.start.setText(\"Start\" if enable else \"Stop\")\n\n    def update_layout(self):\n        self.set_legend_pos()\n        if self.elements:  # Prevent having redrawn grid but with old elements\n            self.scene.removeItem(self.elements)\n            self.elements = None\n        self.redraw_grid()\n        self.rescale()\n\n    def _redraw(self):\n        self.Warning.missing_colors.clear()\n        if self.elements:\n            self.scene.removeItem(self.elements)\n            self.elements = None\n        self.view.set_dimensions(self.size_x, self.size_y, self.hexagonal)\n\n        if self.cells is None:\n            return\n        sizes = self.cells[:, :, 1] - self.cells[:, :, 0]\n        sizes = sizes.astype(float)\n        if not self.size_by_instances:\n            sizes[sizes != 0] = 0.8\n        else:\n            sizes *= 0.8 / np.max(sizes)\n\n        self.elements = QGraphicsItemGroup()\n        self.scene.addItem(self.elements)\n        if self.attr_color is None:\n            self._draw_same_color(sizes)\n        elif self.pie_charts:\n            self._draw_pie_charts(sizes)\n        else:\n            self._draw_colored_circles(sizes)\n\n    @property\n    def _grid_factors(self):\n        return (0.5, sqrt3_2) if self.hexagonal else (0, 1)\n\n    def _draw_same_color(self, sizes):\n        fx, fy = self._grid_factors\n        color = QColor(64, 64, 64)\n        for y in range(self.size_y):\n            for x in range(self.size_x - self.hexagonal * (y % 2)):\n                r = sizes[x, y]\n                n = len(self.get_member_indices(x, y))\n                if not r:\n                    continue\n                ellipse = ColoredCircle(r / 2, color, 0)\n                ellipse.setPos(x + (y % 2) * fx, y * fy)\n                ellipse.setToolTip(f\"{n} instances\")\n                self.elements.addToGroup(ellipse)\n\n    def _get_color_column(self):\n        color_column = \\\n            self.data.get_column_view(self.attr_color)[0].astype(float,\n                                                                 copy=False)\n        if self.attr_color.is_discrete:\n            with np.errstate(invalid=\"ignore\"):\n                int_col = color_column.astype(int)\n            int_col[np.isnan(color_column)] = len(self.colors)\n        else:\n            int_col = np.zeros(len(color_column), dtype=int)\n            # The following line is unnecessary because rows with missing\n            # numeric data are excluded. Uncomment it if you change SOM to\n            # tolerate missing values.\n            # int_col[np.isnan(color_column)] = len(self.colors)\n            for i, thresh in enumerate(self.thresholds, start=1):\n                int_col[color_column >= thresh] = i\n        return int_col\n\n    def _tooltip(self, colors, distribution):\n        if self.attr_color.is_discrete:\n            values = self.attr_color.values\n        else:\n            values = self._bin_names()\n        tot = np.sum(distribution)\n        nbhp = \"\\N{NON-BREAKING HYPHEN}\"\n        return '<table style=\"white-space: nowrap\">' + \"\".join(f\"\"\"\n            <tr>\n                <td>\n                    <font color={color.name()}>■</font>\n                    <b>{escape(val).replace(\"-\", nbhp)}</b>:\n                </td>\n                <td>\n                    {n} ({n / tot * 100:.1f}&nbsp;%)\n                </td>\n            </tr>\n            \"\"\" for color, val, n in zip(colors, values, distribution) if n) \\\n            + \"</table>\"\n\n    def _draw_pie_charts(self, sizes):\n        fx, fy = self._grid_factors\n        color_column = self._get_color_column()\n        colors = self.colors.qcolors_w_nan\n        for y in range(self.size_y):\n            for x in range(self.size_x - self.hexagonal * (y % 2)):\n                r = sizes[x, y]\n                if not r:\n                    self.grid_cells[y, x].setToolTip(\"\")\n                    continue\n                members = self.get_member_indices(x, y)\n                color_dist = np.bincount(color_column[members],\n                                         minlength=len(colors))\n                rel_color_dist = color_dist.astype(float) / len(members)\n                pie = PieChart(rel_color_dist, r / 2, colors)\n                pie.setToolTip(self._tooltip(colors, color_dist))\n                self.elements.addToGroup(pie)\n                pie.setPos(x + (y % 2) * fx, y * fy)\n\n    def _draw_colored_circles(self, sizes):\n        fx, fy = self._grid_factors\n        color_column = self._get_color_column()\n        qcolors = self.colors.qcolors_w_nan\n        for y in range(self.size_y):\n            for x in range(self.size_x - self.hexagonal * (y % 2)):\n                r = sizes[x, y]\n                if not r:\n                    continue\n                members = self.get_member_indices(x, y)\n                color_dist = color_column[members]\n                color_dist = color_dist[color_dist < len(self.colors)]\n                if len(color_dist) != len(members):\n                    self.Warning.missing_colors(self.attr_color.name)\n                bc = np.bincount(color_dist, minlength=len(self.colors))\n                color = qcolors[np.argmax(bc)]\n                ellipse = ColoredCircle(r / 2, color, np.max(bc) / len(members))\n                ellipse.setPos(x + (y % 2) * fx, y * fy)\n                ellipse.setToolTip(self._tooltip(qcolors, bc))\n                self.elements.addToGroup(ellipse)\n\n    def redraw_grid(self):\n        if self.grid is not None:\n            self.scene.removeItem(self.grid)\n        self.grid = QGraphicsItemGroup()\n        self.grid.setZValue(-200)\n        self.grid_cells = np.full((self.size_y, self.size_x), None)\n        for y in range(self.size_y):\n            for x in range(self.size_x - (y % 2) * self.hexagonal):\n                if self.hexagonal:\n                    cell = QGraphicsPathItem(_hexagon_path)\n                    cell.setPos(x + (y % 2) / 2, y * sqrt3_2)\n                else:\n                    cell = QGraphicsRectItem(x - 0.5, y - 0.5, 1, 1)\n                self.grid_cells[y, x] = cell\n                cell.setPen(self._grid_pen)\n                self.grid.addToGroup(cell)\n        self.scene.addItem(self.grid)\n\n    def get_member_indices(self, x, y):\n        i, j = self.cells[x, y]\n        return self.member_data[i:j]\n\n    def _recompute_som(self):\n        if self.cont_x is None:\n            return\n\n        som = SOM(\n            self.size_x, self.size_y,\n            hexagonal=self.hexagonal,\n            pca_init=self.initialization == 0,\n            random_seed=0 if self.initialization == 2 else None\n        )\n\n        class Optimizer(QObject):\n            update = Signal(float, np.ndarray, np.ndarray)\n            done = Signal(SOM)\n            stopped = Signal()\n            stop_optimization = False\n\n            def __init__(self, data, som):\n                super().__init__()\n                self.som = som\n                self.data = data\n\n            def callback(self, progress):\n                self.update.emit(\n                    progress,\n                    self.som.weights.copy(), self.som.ssum_weights.copy())\n                return not self.stop_optimization\n\n            def run(self):\n                try:\n                    self.som.fit(self.data, N_ITERATIONS,\n                                 callback=self.callback)\n                    # Report an exception, but still remove the thread\n                finally:\n                    self.done.emit(self.som)\n                    self.stopped.emit()\n\n        def thread_finished():\n            self._optimizer = None\n            self._optimizer_thread = None\n\n        self.progressBarInit()\n\n        self._optimizer = Optimizer(self.cont_x, som)\n        self._optimizer_thread = QThread()\n        self._optimizer_thread.setStackSize(5 * 2 ** 20)\n        self._optimizer.update.connect(self.__update)\n        self._optimizer.done.connect(self.__done)\n        self._optimizer.stopped.connect(self._optimizer_thread.quit)\n        self._optimizer.moveToThread(self._optimizer_thread)\n        self._optimizer_thread.started.connect(self._optimizer.run)\n        self._optimizer_thread.finished.connect(thread_finished)\n        self.stop_optimization = False\n        self._optimizer_thread.start()\n\n    @Slot(float, object, object)\n    def __update(self, _progress, weights, ssum_weights):\n        self.progressBarSet(_progress)\n        self._assign_instances(weights, ssum_weights)\n        self._redraw()\n\n    @Slot(object)\n    def __done(self, som):\n        self.enable_controls(True)\n        self.progressBarFinished()\n        self._assign_instances(som.weights, som.ssum_weights)\n        self._redraw()\n        # This is the first time we know what was selected (assuming that\n        # initialization is not set to random)\n        if self.__pending_selection is not None:\n            self.on_selection_change(self.__pending_selection)\n            self.__pending_selection = None\n        self.update_output()\n\n    def stop_optimization_and_wait(self):\n        if self._optimizer_thread is not None:\n            self.stop_optimization = True\n            self._optimizer.stop_optimization = True\n            self._optimizer_thread.quit()\n            self._optimizer_thread.wait()\n            self._optimizer_thread = None\n\n    def onDeleteWidget(self):\n        self.stop_optimization_and_wait()\n        self.clear()\n        super().onDeleteWidget()\n\n    def _assign_instances(self, weights, ssum_weights):\n        if self.cont_x is None:\n            return  # the widget is shutting down while signals still processed\n        assignments = SOM.winner_from_weights(\n            self.cont_x, weights, ssum_weights, self.hexagonal)\n        members = defaultdict(list)\n        for i, (x, y) in enumerate(assignments):\n            members[(x, y)].append(i)\n        members.pop(None, None)\n        self.cells = np.empty((self.size_x, self.size_y, 2), dtype=int)\n        self.member_data = np.empty(self.cont_x.shape[0], dtype=int)\n        index = 0\n        for x in range(self.size_x):\n            for y in range(self.size_y):\n                nmembers = len(members[(x, y)])\n                self.member_data[index:index + nmembers] = members[(x, y)]\n                self.cells[x, y] = [index, index + nmembers]\n                index += nmembers\n\n    def resizeEvent(self, event):\n        super().resizeEvent(event)\n        self.create_legend()  # re-wrap lines if necessary\n        self.rescale()\n\n    def rescale(self):\n        if self.legend:\n            leg_height = self.legend.boundingRect().height()\n            leg_extra = 1.5\n        else:\n            leg_height = 0\n            leg_extra = 1\n\n        vw, vh = self.view.width(), self.view.height() - leg_height\n        scale = min(vw / (self.size_x + 1),\n                    vh / ((self.size_y + leg_extra) * self._grid_factors[1]))\n        self.view.setTransform(QTransform.fromScale(scale, scale))\n        if self.hexagonal:\n            self.view.setSceneRect(\n                0, -1, self.size_x - 1,\n                (self.size_y + leg_extra) * sqrt3_2 + leg_height / scale)\n        else:\n            self.view.setSceneRect(\n                -0.25, -0.25, self.size_x - 0.5,\n                self.size_y - 0.5 + leg_height / scale)\n\n    def update_output(self):\n        if self.data is None:\n            self.Outputs.selected_data.send(None)\n            self.Outputs.annotated_data.send(None)\n            return\n\n        indices = np.zeros(len(self.data), dtype=int)\n        if self.selection is not None and np.any(self.selection):\n            for y in range(self.size_y):\n                for x in range(self.size_x):\n                    rows = self.get_member_indices(x, y)\n                    indices[rows] = self.selection[x, y]\n\n        if np.any(indices):\n            sel_data = create_groups_table(self.data, indices, False, \"Group\")\n            self.Outputs.selected_data.send(sel_data)\n        else:\n            self.Outputs.selected_data.send(None)\n\n        if np.max(indices) > 1:\n            annotated = create_groups_table(self.data, indices)\n        else:\n            annotated = create_annotated_table(\n                self.data, np.flatnonzero(indices))\n        self.Outputs.annotated_data.send(annotated)\n\n    def set_color_bins(self):\n        if self.attr_color is None:\n            self.thresholds = self.bin_labels = self.colors = None\n        elif self.attr_color.is_discrete:\n            self.thresholds = self.bin_labels = None\n            self.colors = self.attr_color.palette\n        else:\n            col = self.data.get_column_view(self.attr_color)[0].astype(float)\n            if self.attr_color.is_time:\n                binning = time_binnings(col, min_bins=4)[-1]\n            else:\n                binning = decimal_binnings(col, min_bins=4)[-1]\n            self.thresholds = binning.thresholds[1:-1]\n            self.bin_labels = (binning.labels[1:-1], binning.short_labels[1:-1])\n            palette = BinnedContinuousPalette.from_palette(\n                self.attr_color.palette, binning.thresholds)\n            self.colors = palette\n\n    def create_legend(self):\n        if self.legend is not None:\n            self.scene.removeItem(self.legend)\n            self.legend = None\n        if self.attr_color is None:\n            return\n\n        if self.attr_color.is_discrete:\n            names = self.attr_color.values\n        else:\n            names = self._bin_names()\n\n        items = []\n        size = 8\n        for name, color in zip(names, self.colors.qcolors):\n            item = QGraphicsItemGroup()\n            item.addToGroup(\n                CanvasRectangle(None, -size / 2, -size / 2, size, size,\n                                Qt.gray, color))\n            item.addToGroup(CanvasText(None, name, size, 0, Qt.AlignVCenter))\n            items.append(item)\n\n        self.legend = wrap_legend_items(\n            items, hspacing=20, vspacing=16 + size,\n            max_width=self.view.width() - 25)\n        self.legend.setFlags(self.legend.ItemIgnoresTransformations)\n        self.legend.setTransform(\n            QTransform.fromTranslate(-self.legend.boundingRect().width() / 2,\n                                     0))\n        self.scene.addItem(self.legend)\n        self.set_legend_pos()\n\n    def _bin_names(self):\n        labels, short_labels = self.bin_labels\n        return \\\n            [f\"< {labels[0]}\"] \\\n            + [f\"{x} - {y}\" for x, y in zip(labels, short_labels[1:])] \\\n            + [f\"≥ {labels[-1]}\"]\n\n    def set_legend_pos(self):\n        if self.legend is None:\n            return\n        self.legend.setPos(\n            self.size_x / 2,\n            (self.size_y + 0.2 + 0.3 * self.hexagonal) * self._grid_factors[1])\n\n    def send_report(self):\n        self.report_plot()\n        if self.attr_color:\n            self.report_caption(\n                f\"Self-organizing map colored by '{self.attr_color.name}'\")",
                            "ast_type_str": "ClassDef",
                            "start_lineno": 300,
                            "end_lineno": 357,
                            "parent": {
                                "file_name": "Orange/widgets/unsupervised/owsom.py",
                                "type": "class",
                                "name": "OWSOM",
                                "content": "class OWSOM(OWWidget):\n    name = \"Self-Organizing Map\"\n    description = \"Computation of self-organizing map.\"\n    icon = \"icons/SOM.svg\"\n    keywords = [\"SOM\"]\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    settingsHandler = DomainContextHandler()\n    auto_dimension = Setting(True)\n    size_x = Setting(10)\n    size_y = Setting(10)\n    hexagonal = Setting(1)\n    initialization = Setting(0)\n\n    attr_color = ContextSetting(None)\n    size_by_instances = Setting(True)\n    pie_charts = Setting(False)\n    selection = Setting(None, schema_only=True)\n\n    graph_name = \"view\"\n\n    _grid_pen = QPen(QBrush(QColor(224, 224, 224)), 2)\n    _grid_pen.setCosmetic(True)\n\n    OptControls = namedtuple(\n        \"OptControls\",\n        (\"shape\", \"auto_dim\", \"spin_x\", \"spin_y\", \"initialization\", \"start\")\n    )\n\n    class Warning(OWWidget.Warning):\n        ignoring_disc_variables = Msg(\"SOM ignores categorical variables.\")\n        missing_colors = \\\n            Msg(\"Some data instances have undefined value of '{}'.\")\n        missing_values = \\\n            Msg(\"{} data instance{} with undefined value(s) {} not shown.\")\n        single_attribute = Msg(\"Data contains a single numeric column.\")\n\n    class Error(OWWidget.Error):\n        no_numeric_variables = Msg(\"Data contains no numeric columns.\")\n        no_defined_rows = Msg(\"All rows contain at least one undefined value.\")\n\n    def __init__(self):\n        super().__init__()\n        self.__pending_selection = self.selection\n        self._optimizer = None\n        self._optimizer_thread = None\n        self.stop_optimization = False\n\n        self.data = self.cont_x = None\n        self.cells = self.member_data = None\n        self.selection = None\n        self.colors = self.thresholds = self.bin_labels = None\n\n        box = gui.vBox(self.controlArea, box=\"SOM\")\n        shape = gui.comboBox(\n            box, self, \"\", items=(\"Hexagonal grid\", \"Square grid\"))\n        shape.setCurrentIndex(1 - self.hexagonal)\n\n        box2 = gui.indentedBox(box, 10)\n        auto_dim = gui.checkBox(\n            box2, self, \"auto_dimension\", \"Set dimensions automatically\",\n            callback=self.on_auto_dimension_changed)\n        self.manual_box = box3 = gui.hBox(box2)\n        spinargs = dict(\n            value=\"\", widget=box3, master=self, minv=5, maxv=100, step=5,\n            alignment=Qt.AlignRight)\n        spin_x = gui.spin(**spinargs)\n        spin_x.setValue(self.size_x)\n        gui.widgetLabel(box3, \"×\")\n        spin_y = gui.spin(**spinargs)\n        spin_y.setValue(self.size_y)\n        gui.rubber(box3)\n        self.manual_box.setEnabled(not self.auto_dimension)\n\n        initialization = gui.comboBox(\n            box, self, \"initialization\",\n            items=(\"Initialize with PCA\", \"Random initialization\",\n                   \"Replicable random\"))\n\n        start = gui.button(\n            box, self, \"Restart\", callback=self.restart_som_pressed,\n            sizePolicy=(QSizePolicy.MinimumExpanding, QSizePolicy.Fixed))\n\n        self.opt_controls = self.OptControls(\n            shape, auto_dim, spin_x, spin_y, initialization, start)\n\n        box = gui.vBox(self.controlArea, \"Color\")\n        gui.comboBox(\n            box, self, \"attr_color\", searchable=True,\n            callback=self.on_attr_color_change,\n            model=DomainModel(placeholder=\"(Same color)\",\n                              valid_types=DomainModel.PRIMITIVE))\n        gui.checkBox(\n            box, self, \"pie_charts\", label=\"Show pie charts\",\n            callback=self.on_pie_chart_change)\n        gui.checkBox(\n            box, self, \"size_by_instances\", label=\"Size by number of instances\",\n            callback=self.on_attr_size_change)\n\n        gui.rubber(self.controlArea)\n\n        self.scene = QGraphicsScene(self)\n\n        self.view = SomView(self.scene)\n        self.view.setMinimumWidth(400)\n        self.view.setMinimumHeight(400)\n        self.view.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)\n        self.view.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)\n        self.view.setRenderHint(QPainter.Antialiasing)\n        self.view.selection_changed.connect(self.on_selection_change)\n        self.view.selection_moved.connect(self.on_selection_move)\n        self.view.selection_mark_changed.connect(self.on_selection_mark_change)\n        self.mainArea.layout().addWidget(self.view)\n\n        self.elements = None\n        self.grid = None\n        self.grid_cells = None\n        self.legend = None\n\n    @Inputs.data\n    def set_data(self, data):\n        def prepare_data():\n            if len(cont_attrs) < len(attrs):\n                self.Warning.ignoring_disc_variables()\n            if len(cont_attrs) == 1:\n                self.Warning.single_attribute()\n            x = Table.from_table(Domain(cont_attrs), data).X\n            if sp.issparse(x):\n                self.data = data\n                self.cont_x = x.tocsr()\n            else:\n                mask = np.all(np.isfinite(x), axis=1)\n                if not np.any(mask):\n                    self.Error.no_defined_rows()\n                else:\n                    if np.all(mask):\n                        self.data = data\n                        self.cont_x = x.copy()\n                    else:\n                        self.data = data[mask]\n                        self.cont_x = x[mask]\n                    self.cont_x -= np.min(self.cont_x, axis=0)[None, :]\n                    sums = np.sum(self.cont_x, axis=0)[None, :]\n                    sums[sums == 0] = 1\n                    self.cont_x /= sums\n\n        def set_warnings():\n            missing = len(data) - len(self.data)\n            if missing == 1:\n                self.Warning.missing_values(1, \"\", \"is\")\n            elif missing > 1:\n                self.Warning.missing_values(missing, \"s\", \"are\")\n\n        self.stop_optimization_and_wait()\n\n        self.closeContext()\n        self.clear()\n        self.Error.clear()\n        self.Warning.clear()\n\n        if data is not None:\n            attrs = data.domain.attributes\n            cont_attrs = [var for var in attrs if var.is_continuous]\n            if not cont_attrs:\n                self.Error.no_numeric_variables()\n            else:\n                prepare_data()\n\n        if self.data is not None:\n            self.controls.attr_color.model().set_domain(data.domain)\n            self.attr_color = data.domain.class_var\n            set_warnings()\n\n        self.openContext(self.data)\n        self.set_color_bins()\n        self.create_legend()\n        self.recompute_dimensions()\n        self.start_som()\n\n    def clear(self):\n        self.data = self.cont_x = None\n        self.cells = self.member_data = None\n        self.attr_color = None\n        self.colors = self.thresholds = self.bin_labels = None\n        if self.elements is not None:\n            self.scene.removeItem(self.elements)\n            self.elements = None\n        self.clear_selection()\n        self.controls.attr_color.model().set_domain(None)\n        self.Warning.clear()\n        self.Error.clear()\n\n    def recompute_dimensions(self):\n        if not self.auto_dimension or self.cont_x is None:\n            return\n        dim = max(5, int(np.ceil(np.sqrt(5 * np.sqrt(self.cont_x.shape[0])))))\n        self.opt_controls.spin_x.setValue(dim)\n        self.opt_controls.spin_y.setValue(dim)\n\n    def on_auto_dimension_changed(self):\n        self.manual_box.setEnabled(not self.auto_dimension)\n        if self.auto_dimension:\n            self.recompute_dimensions()\n        else:\n            spin_x = self.opt_controls.spin_x\n            spin_y = self.opt_controls.spin_y\n            dimx = int(5 * np.round(spin_x.value() / 5))\n            dimy = int(5 * np.round(spin_y.value() / 5))\n            spin_x.setValue(dimx)\n            spin_y.setValue(dimy)\n\n    def on_attr_color_change(self):\n        self.controls.pie_charts.setEnabled(self.attr_color is not None)\n        self.set_color_bins()\n        self.create_legend()\n        self.rescale()\n        self._redraw()\n\n    def on_attr_size_change(self):\n        self._redraw()\n\n    def on_pie_chart_change(self):\n        self._redraw()\n\n    def clear_selection(self):\n        self.selection = None\n        self.redraw_selection()\n\n    def on_selection_change(self, selection, action=SomView.SelectionSet):\n        if self.data is None:  # clicks on empty canvas\n            return\n        if self.selection is None:\n            self.selection = np.zeros(self.grid_cells.T.shape, dtype=np.int16)\n        if action == SomView.SelectionSet:\n            self.selection[:] = 0\n            self.selection[selection] = 1\n        elif action == SomView.SelectionAddToGroup:\n            self.selection[selection] = max(1, np.max(self.selection))\n        elif action == SomView.SelectionNewGroup:\n            self.selection[selection] = 1 + np.max(self.selection)\n        elif action & SomView.SelectionRemove:\n            self.selection[selection] = 0\n        self.redraw_selection()\n        self.update_output()\n\n    def on_selection_move(self, event: QKeyEvent):\n        if self.selection is None or not np.any(self.selection):\n            if event.key() in (Qt.Key_Right, Qt.Key_Down):\n                x = y = 0\n            else:\n                x = self.size_x - 1\n                y = self.size_y - 1\n        else:\n            x, y = np.nonzero(self.selection)\n            if len(x) > 1:\n                return\n            if event.key() == Qt.Key_Up and y > 0:\n                y -= 1\n            if event.key() == Qt.Key_Down and y < self.size_y - 1:\n                y += 1\n            if event.key() == Qt.Key_Left and x:\n                x -= 1\n            if event.key() == Qt.Key_Right and x < self.size_x - 1:\n                x += 1\n            x -= self.hexagonal and x == self.size_x - 1 and y % 2\n\n        if self.selection is not None and self.selection[x, y]:\n            return\n        selection = np.zeros(self.grid_cells.shape, dtype=bool)\n        selection[x, y] = True\n        self.on_selection_change(selection)\n\n    def on_selection_mark_change(self, marks):\n        self.redraw_selection(marks=marks)\n\n    def redraw_selection(self, marks=None):\n        if self.grid_cells is None:\n            return\n\n        sel_pen = QPen(QBrush(QColor(128, 128, 128)), 2)\n        sel_pen.setCosmetic(True)\n        mark_pen = QPen(QBrush(QColor(128, 128, 128)), 4)\n        mark_pen.setCosmetic(True)\n        pens = [self._grid_pen, sel_pen]\n\n        mark_brush = QBrush(QColor(224, 255, 255))\n        sels = self.selection is not None and np.max(self.selection)\n        palette = LimitedDiscretePalette(number_of_colors=sels + 1)\n        brushes = [QBrush(Qt.NoBrush)] + \\\n                  [QBrush(palette[i].lighter(165)) for i in range(sels)]\n\n        for y in range(self.size_y):\n            for x in range(self.size_x - (y % 2) * self.hexagonal):\n                cell = self.grid_cells[y, x]\n                marked = marks is not None and marks[x, y]\n                sel_group = self.selection is not None and self.selection[x, y]\n                if marked:\n                    cell.setBrush(mark_brush)\n                    cell.setPen(mark_pen)\n                else:\n                    cell.setBrush(brushes[sel_group])\n                    cell.setPen(pens[bool(sel_group)])\n                cell.setZValue(marked or sel_group)\n\n    def restart_som_pressed(self):\n        if self._optimizer_thread is not None:\n            self.stop_optimization = True\n            self._optimizer.stop_optimization = True\n        else:\n            self.start_som()\n\n    def start_som(self):\n        self.read_controls()\n        self.update_layout()\n        self.clear_selection()\n        if self.cont_x is not None:\n            self.enable_controls(False)\n            self._recompute_som()\n        else:\n            self.update_output()\n\n    def read_controls(self):\n        c = self.opt_controls\n        self.hexagonal = c.shape.currentIndex() == 0\n        self.size_x = c.spin_x.value()\n        self.size_y = c.spin_y.value()\n\n    def enable_controls(self, enable):\n        c = self.opt_controls\n        c.shape.setEnabled(enable)\n        c.auto_dim.setEnabled(enable)\n        c.start.setText(\"Start\" if enable else \"Stop\")\n\n    def update_layout(self):\n        self.set_legend_pos()\n        if self.elements:  # Prevent having redrawn grid but with old elements\n            self.scene.removeItem(self.elements)\n            self.elements = None\n        self.redraw_grid()\n        self.rescale()\n\n    def _redraw(self):\n        self.Warning.missing_colors.clear()\n        if self.elements:\n            self.scene.removeItem(self.elements)\n            self.elements = None\n        self.view.set_dimensions(self.size_x, self.size_y, self.hexagonal)\n\n        if self.cells is None:\n            return\n        sizes = self.cells[:, :, 1] - self.cells[:, :, 0]\n        sizes = sizes.astype(float)\n        if not self.size_by_instances:\n            sizes[sizes != 0] = 0.8\n        else:\n            sizes *= 0.8 / np.max(sizes)\n\n        self.elements = QGraphicsItemGroup()\n        self.scene.addItem(self.elements)\n        if self.attr_color is None:\n            self._draw_same_color(sizes)\n        elif self.pie_charts:\n            self._draw_pie_charts(sizes)\n        else:\n            self._draw_colored_circles(sizes)\n\n    @property\n    def _grid_factors(self):\n        return (0.5, sqrt3_2) if self.hexagonal else (0, 1)\n\n    def _draw_same_color(self, sizes):\n        fx, fy = self._grid_factors\n        color = QColor(64, 64, 64)\n        for y in range(self.size_y):\n            for x in range(self.size_x - self.hexagonal * (y % 2)):\n                r = sizes[x, y]\n                n = len(self.get_member_indices(x, y))\n                if not r:\n                    continue\n                ellipse = ColoredCircle(r / 2, color, 0)\n                ellipse.setPos(x + (y % 2) * fx, y * fy)\n                ellipse.setToolTip(f\"{n} instances\")\n                self.elements.addToGroup(ellipse)\n\n    def _get_color_column(self):\n        color_column = \\\n            self.data.get_column_view(self.attr_color)[0].astype(float,\n                                                                 copy=False)\n        if self.attr_color.is_discrete:\n            with np.errstate(invalid=\"ignore\"):\n                int_col = color_column.astype(int)\n            int_col[np.isnan(color_column)] = len(self.colors)\n        else:\n            int_col = np.zeros(len(color_column), dtype=int)\n            # The following line is unnecessary because rows with missing\n            # numeric data are excluded. Uncomment it if you change SOM to\n            # tolerate missing values.\n            # int_col[np.isnan(color_column)] = len(self.colors)\n            for i, thresh in enumerate(self.thresholds, start=1):\n                int_col[color_column >= thresh] = i\n        return int_col\n\n    def _tooltip(self, colors, distribution):\n        if self.attr_color.is_discrete:\n            values = self.attr_color.values\n        else:\n            values = self._bin_names()\n        tot = np.sum(distribution)\n        nbhp = \"\\N{NON-BREAKING HYPHEN}\"\n        return '<table style=\"white-space: nowrap\">' + \"\".join(f\"\"\"\n            <tr>\n                <td>\n                    <font color={color.name()}>■</font>\n                    <b>{escape(val).replace(\"-\", nbhp)}</b>:\n                </td>\n                <td>\n                    {n} ({n / tot * 100:.1f}&nbsp;%)\n                </td>\n            </tr>\n            \"\"\" for color, val, n in zip(colors, values, distribution) if n) \\\n            + \"</table>\"\n\n    def _draw_pie_charts(self, sizes):\n        fx, fy = self._grid_factors\n        color_column = self._get_color_column()\n        colors = self.colors.qcolors_w_nan\n        for y in range(self.size_y):\n            for x in range(self.size_x - self.hexagonal * (y % 2)):\n                r = sizes[x, y]\n                if not r:\n                    self.grid_cells[y, x].setToolTip(\"\")\n                    continue\n                members = self.get_member_indices(x, y)\n                color_dist = np.bincount(color_column[members],\n                                         minlength=len(colors))\n                rel_color_dist = color_dist.astype(float) / len(members)\n                pie = PieChart(rel_color_dist, r / 2, colors)\n                pie.setToolTip(self._tooltip(colors, color_dist))\n                self.elements.addToGroup(pie)\n                pie.setPos(x + (y % 2) * fx, y * fy)\n\n    def _draw_colored_circles(self, sizes):\n        fx, fy = self._grid_factors\n        color_column = self._get_color_column()\n        qcolors = self.colors.qcolors_w_nan\n        for y in range(self.size_y):\n            for x in range(self.size_x - self.hexagonal * (y % 2)):\n                r = sizes[x, y]\n                if not r:\n                    continue\n                members = self.get_member_indices(x, y)\n                color_dist = color_column[members]\n                color_dist = color_dist[color_dist < len(self.colors)]\n                if len(color_dist) != len(members):\n                    self.Warning.missing_colors(self.attr_color.name)\n                bc = np.bincount(color_dist, minlength=len(self.colors))\n                color = qcolors[np.argmax(bc)]\n                ellipse = ColoredCircle(r / 2, color, np.max(bc) / len(members))\n                ellipse.setPos(x + (y % 2) * fx, y * fy)\n                ellipse.setToolTip(self._tooltip(qcolors, bc))\n                self.elements.addToGroup(ellipse)\n\n    def redraw_grid(self):\n        if self.grid is not None:\n            self.scene.removeItem(self.grid)\n        self.grid = QGraphicsItemGroup()\n        self.grid.setZValue(-200)\n        self.grid_cells = np.full((self.size_y, self.size_x), None)\n        for y in range(self.size_y):\n            for x in range(self.size_x - (y % 2) * self.hexagonal):\n                if self.hexagonal:\n                    cell = QGraphicsPathItem(_hexagon_path)\n                    cell.setPos(x + (y % 2) / 2, y * sqrt3_2)\n                else:\n                    cell = QGraphicsRectItem(x - 0.5, y - 0.5, 1, 1)\n                self.grid_cells[y, x] = cell\n                cell.setPen(self._grid_pen)\n                self.grid.addToGroup(cell)\n        self.scene.addItem(self.grid)\n\n    def get_member_indices(self, x, y):\n        i, j = self.cells[x, y]\n        return self.member_data[i:j]\n\n    def _recompute_som(self):\n        if self.cont_x is None:\n            return\n\n        som = SOM(\n            self.size_x, self.size_y,\n            hexagonal=self.hexagonal,\n            pca_init=self.initialization == 0,\n            random_seed=0 if self.initialization == 2 else None\n        )\n\n        class Optimizer(QObject):\n            update = Signal(float, np.ndarray, np.ndarray)\n            done = Signal(SOM)\n            stopped = Signal()\n            stop_optimization = False\n\n            def __init__(self, data, som):\n                super().__init__()\n                self.som = som\n                self.data = data\n\n            def callback(self, progress):\n                self.update.emit(\n                    progress,\n                    self.som.weights.copy(), self.som.ssum_weights.copy())\n                return not self.stop_optimization\n\n            def run(self):\n                try:\n                    self.som.fit(self.data, N_ITERATIONS,\n                                 callback=self.callback)\n                    # Report an exception, but still remove the thread\n                finally:\n                    self.done.emit(self.som)\n                    self.stopped.emit()\n\n        def thread_finished():\n            self._optimizer = None\n            self._optimizer_thread = None\n\n        self.progressBarInit()\n\n        self._optimizer = Optimizer(self.cont_x, som)\n        self._optimizer_thread = QThread()\n        self._optimizer_thread.setStackSize(5 * 2 ** 20)\n        self._optimizer.update.connect(self.__update)\n        self._optimizer.done.connect(self.__done)\n        self._optimizer.stopped.connect(self._optimizer_thread.quit)\n        self._optimizer.moveToThread(self._optimizer_thread)\n        self._optimizer_thread.started.connect(self._optimizer.run)\n        self._optimizer_thread.finished.connect(thread_finished)\n        self.stop_optimization = False\n        self._optimizer_thread.start()\n\n    @Slot(float, object, object)\n    def __update(self, _progress, weights, ssum_weights):\n        self.progressBarSet(_progress)\n        self._assign_instances(weights, ssum_weights)\n        self._redraw()\n\n    @Slot(object)\n    def __done(self, som):\n        self.enable_controls(True)\n        self.progressBarFinished()\n        self._assign_instances(som.weights, som.ssum_weights)\n        self._redraw()\n        # This is the first time we know what was selected (assuming that\n        # initialization is not set to random)\n        if self.__pending_selection is not None:\n            self.on_selection_change(self.__pending_selection)\n            self.__pending_selection = None\n        self.update_output()\n\n    def stop_optimization_and_wait(self):\n        if self._optimizer_thread is not None:\n            self.stop_optimization = True\n            self._optimizer.stop_optimization = True\n            self._optimizer_thread.quit()\n            self._optimizer_thread.wait()\n            self._optimizer_thread = None\n\n    def onDeleteWidget(self):\n        self.stop_optimization_and_wait()\n        self.clear()\n        super().onDeleteWidget()\n\n    def _assign_instances(self, weights, ssum_weights):\n        if self.cont_x is None:\n            return  # the widget is shutting down while signals still processed\n        assignments = SOM.winner_from_weights(\n            self.cont_x, weights, ssum_weights, self.hexagonal)\n        members = defaultdict(list)\n        for i, (x, y) in enumerate(assignments):\n            members[(x, y)].append(i)\n        members.pop(None, None)\n        self.cells = np.empty((self.size_x, self.size_y, 2), dtype=int)\n        self.member_data = np.empty(self.cont_x.shape[0], dtype=int)\n        index = 0\n        for x in range(self.size_x):\n            for y in range(self.size_y):\n                nmembers = len(members[(x, y)])\n                self.member_data[index:index + nmembers] = members[(x, y)]\n                self.cells[x, y] = [index, index + nmembers]\n                index += nmembers\n\n    def resizeEvent(self, event):\n        super().resizeEvent(event)\n        self.create_legend()  # re-wrap lines if necessary\n        self.rescale()\n\n    def rescale(self):\n        if self.legend:\n            leg_height = self.legend.boundingRect().height()\n            leg_extra = 1.5\n        else:\n            leg_height = 0\n            leg_extra = 1\n\n        vw, vh = self.view.width(), self.view.height() - leg_height\n        scale = min(vw / (self.size_x + 1),\n                    vh / ((self.size_y + leg_extra) * self._grid_factors[1]))\n        self.view.setTransform(QTransform.fromScale(scale, scale))\n        if self.hexagonal:\n            self.view.setSceneRect(\n                0, -1, self.size_x - 1,\n                (self.size_y + leg_extra) * sqrt3_2 + leg_height / scale)\n        else:\n            self.view.setSceneRect(\n                -0.25, -0.25, self.size_x - 0.5,\n                self.size_y - 0.5 + leg_height / scale)\n\n    def update_output(self):\n        if self.data is None:\n            self.Outputs.selected_data.send(None)\n            self.Outputs.annotated_data.send(None)\n            return\n\n        indices = np.zeros(len(self.data), dtype=int)\n        if self.selection is not None and np.any(self.selection):\n            for y in range(self.size_y):\n                for x in range(self.size_x):\n                    rows = self.get_member_indices(x, y)\n                    indices[rows] = self.selection[x, y]\n\n        if np.any(indices):\n            sel_data = create_groups_table(self.data, indices, False, \"Group\")\n            self.Outputs.selected_data.send(sel_data)\n        else:\n            self.Outputs.selected_data.send(None)\n\n        if np.max(indices) > 1:\n            annotated = create_groups_table(self.data, indices)\n        else:\n            annotated = create_annotated_table(\n                self.data, np.flatnonzero(indices))\n        self.Outputs.annotated_data.send(annotated)\n\n    def set_color_bins(self):\n        if self.attr_color is None:\n            self.thresholds = self.bin_labels = self.colors = None\n        elif self.attr_color.is_discrete:\n            self.thresholds = self.bin_labels = None\n            self.colors = self.attr_color.palette\n        else:\n            col = self.data.get_column_view(self.attr_color)[0].astype(float)\n            if self.attr_color.is_time:\n                binning = time_binnings(col, min_bins=4)[-1]\n            else:\n                binning = decimal_binnings(col, min_bins=4)[-1]\n            self.thresholds = binning.thresholds[1:-1]\n            self.bin_labels = (binning.labels[1:-1], binning.short_labels[1:-1])\n            palette = BinnedContinuousPalette.from_palette(\n                self.attr_color.palette, binning.thresholds)\n            self.colors = palette\n\n    def create_legend(self):\n        if self.legend is not None:\n            self.scene.removeItem(self.legend)\n            self.legend = None\n        if self.attr_color is None:\n            return\n\n        if self.attr_color.is_discrete:\n            names = self.attr_color.values\n        else:\n            names = self._bin_names()\n\n        items = []\n        size = 8\n        for name, color in zip(names, self.colors.qcolors):\n            item = QGraphicsItemGroup()\n            item.addToGroup(\n                CanvasRectangle(None, -size / 2, -size / 2, size, size,\n                                Qt.gray, color))\n            item.addToGroup(CanvasText(None, name, size, 0, Qt.AlignVCenter))\n            items.append(item)\n\n        self.legend = wrap_legend_items(\n            items, hspacing=20, vspacing=16 + size,\n            max_width=self.view.width() - 25)\n        self.legend.setFlags(self.legend.ItemIgnoresTransformations)\n        self.legend.setTransform(\n            QTransform.fromTranslate(-self.legend.boundingRect().width() / 2,\n                                     0))\n        self.scene.addItem(self.legend)\n        self.set_legend_pos()\n\n    def _bin_names(self):\n        labels, short_labels = self.bin_labels\n        return \\\n            [f\"< {labels[0]}\"] \\\n            + [f\"{x} - {y}\" for x, y in zip(labels, short_labels[1:])] \\\n            + [f\"≥ {labels[-1]}\"]\n\n    def set_legend_pos(self):\n        if self.legend is None:\n            return\n        self.legend.setPos(\n            self.size_x / 2,\n            (self.size_y + 0.2 + 0.3 * self.hexagonal) * self._grid_factors[1])\n\n    def send_report(self):\n        self.report_plot()\n        if self.attr_color:\n            self.report_caption(\n                f\"Self-organizing map colored by '{self.attr_color.name}'\")",
                                "ast_type_str": "ClassDef",
                                "start_lineno": 174,
                                "end_lineno": 890,
                                "parent": null
                            }
                        },
                        {
                            "file_name": "Orange/widgets/unsupervised/owsom.py",
                            "type": "class",
                            "name": "OWSOM",
                            "content": "class OWSOM(OWWidget):\n    name = \"Self-Organizing Map\"\n    description = \"Computation of self-organizing map.\"\n    icon = \"icons/SOM.svg\"\n    keywords = [\"SOM\"]\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    settingsHandler = DomainContextHandler()\n    auto_dimension = Setting(True)\n    size_x = Setting(10)\n    size_y = Setting(10)\n    hexagonal = Setting(1)\n    initialization = Setting(0)\n\n    attr_color = ContextSetting(None)\n    size_by_instances = Setting(True)\n    pie_charts = Setting(False)\n    selection = Setting(None, schema_only=True)\n\n    graph_name = \"view\"\n\n    _grid_pen = QPen(QBrush(QColor(224, 224, 224)), 2)\n    _grid_pen.setCosmetic(True)\n\n    OptControls = namedtuple(\n        \"OptControls\",\n        (\"shape\", \"auto_dim\", \"spin_x\", \"spin_y\", \"initialization\", \"start\")\n    )\n\n    class Warning(OWWidget.Warning):\n        ignoring_disc_variables = Msg(\"SOM ignores categorical variables.\")\n        missing_colors = \\\n            Msg(\"Some data instances have undefined value of '{}'.\")\n        missing_values = \\\n            Msg(\"{} data instance{} with undefined value(s) {} not shown.\")\n        single_attribute = Msg(\"Data contains a single numeric column.\")\n\n    class Error(OWWidget.Error):\n        no_numeric_variables = Msg(\"Data contains no numeric columns.\")\n        no_defined_rows = Msg(\"All rows contain at least one undefined value.\")\n\n    def __init__(self):\n        super().__init__()\n        self.__pending_selection = self.selection\n        self._optimizer = None\n        self._optimizer_thread = None\n        self.stop_optimization = False\n\n        self.data = self.cont_x = None\n        self.cells = self.member_data = None\n        self.selection = None\n        self.colors = self.thresholds = self.bin_labels = None\n\n        box = gui.vBox(self.controlArea, box=\"SOM\")\n        shape = gui.comboBox(\n            box, self, \"\", items=(\"Hexagonal grid\", \"Square grid\"))\n        shape.setCurrentIndex(1 - self.hexagonal)\n\n        box2 = gui.indentedBox(box, 10)\n        auto_dim = gui.checkBox(\n            box2, self, \"auto_dimension\", \"Set dimensions automatically\",\n            callback=self.on_auto_dimension_changed)\n        self.manual_box = box3 = gui.hBox(box2)\n        spinargs = dict(\n            value=\"\", widget=box3, master=self, minv=5, maxv=100, step=5,\n            alignment=Qt.AlignRight)\n        spin_x = gui.spin(**spinargs)\n        spin_x.setValue(self.size_x)\n        gui.widgetLabel(box3, \"×\")\n        spin_y = gui.spin(**spinargs)\n        spin_y.setValue(self.size_y)\n        gui.rubber(box3)\n        self.manual_box.setEnabled(not self.auto_dimension)\n\n        initialization = gui.comboBox(\n            box, self, \"initialization\",\n            items=(\"Initialize with PCA\", \"Random initialization\",\n                   \"Replicable random\"))\n\n        start = gui.button(\n            box, self, \"Restart\", callback=self.restart_som_pressed,\n            sizePolicy=(QSizePolicy.MinimumExpanding, QSizePolicy.Fixed))\n\n        self.opt_controls = self.OptControls(\n            shape, auto_dim, spin_x, spin_y, initialization, start)\n\n        box = gui.vBox(self.controlArea, \"Color\")\n        gui.comboBox(\n            box, self, \"attr_color\", searchable=True,\n            callback=self.on_attr_color_change,\n            model=DomainModel(placeholder=\"(Same color)\",\n                              valid_types=DomainModel.PRIMITIVE))\n        gui.checkBox(\n            box, self, \"pie_charts\", label=\"Show pie charts\",\n            callback=self.on_pie_chart_change)\n        gui.checkBox(\n            box, self, \"size_by_instances\", label=\"Size by number of instances\",\n            callback=self.on_attr_size_change)\n\n        gui.rubber(self.controlArea)\n\n        self.scene = QGraphicsScene(self)\n\n        self.view = SomView(self.scene)\n        self.view.setMinimumWidth(400)\n        self.view.setMinimumHeight(400)\n        self.view.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)\n        self.view.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)\n        self.view.setRenderHint(QPainter.Antialiasing)\n        self.view.selection_changed.connect(self.on_selection_change)\n        self.view.selection_moved.connect(self.on_selection_move)\n        self.view.selection_mark_changed.connect(self.on_selection_mark_change)\n        self.mainArea.layout().addWidget(self.view)\n\n        self.elements = None\n        self.grid = None\n        self.grid_cells = None\n        self.legend = None\n\n    @Inputs.data\n    def set_data(self, data):\n        def prepare_data():\n            if len(cont_attrs) < len(attrs):\n                self.Warning.ignoring_disc_variables()\n            if len(cont_attrs) == 1:\n                self.Warning.single_attribute()\n            x = Table.from_table(Domain(cont_attrs), data).X\n            if sp.issparse(x):\n                self.data = data\n                self.cont_x = x.tocsr()\n            else:\n                mask = np.all(np.isfinite(x), axis=1)\n                if not np.any(mask):\n                    self.Error.no_defined_rows()\n                else:\n                    if np.all(mask):\n                        self.data = data\n                        self.cont_x = x.copy()\n                    else:\n                        self.data = data[mask]\n                        self.cont_x = x[mask]\n                    self.cont_x -= np.min(self.cont_x, axis=0)[None, :]\n                    sums = np.sum(self.cont_x, axis=0)[None, :]\n                    sums[sums == 0] = 1\n                    self.cont_x /= sums\n\n        def set_warnings():\n            missing = len(data) - len(self.data)\n            if missing == 1:\n                self.Warning.missing_values(1, \"\", \"is\")\n            elif missing > 1:\n                self.Warning.missing_values(missing, \"s\", \"are\")\n\n        self.stop_optimization_and_wait()\n\n        self.closeContext()\n        self.clear()\n        self.Error.clear()\n        self.Warning.clear()\n\n        if data is not None:\n            attrs = data.domain.attributes\n            cont_attrs = [var for var in attrs if var.is_continuous]\n            if not cont_attrs:\n                self.Error.no_numeric_variables()\n            else:\n                prepare_data()\n\n        if self.data is not None:\n            self.controls.attr_color.model().set_domain(data.domain)\n            self.attr_color = data.domain.class_var\n            set_warnings()\n\n        self.openContext(self.data)\n        self.set_color_bins()\n        self.create_legend()\n        self.recompute_dimensions()\n        self.start_som()\n\n    def clear(self):\n        self.data = self.cont_x = None\n        self.cells = self.member_data = None\n        self.attr_color = None\n        self.colors = self.thresholds = self.bin_labels = None\n        if self.elements is not None:\n            self.scene.removeItem(self.elements)\n            self.elements = None\n        self.clear_selection()\n        self.controls.attr_color.model().set_domain(None)\n        self.Warning.clear()\n        self.Error.clear()\n\n    def recompute_dimensions(self):\n        if not self.auto_dimension or self.cont_x is None:\n            return\n        dim = max(5, int(np.ceil(np.sqrt(5 * np.sqrt(self.cont_x.shape[0])))))\n        self.opt_controls.spin_x.setValue(dim)\n        self.opt_controls.spin_y.setValue(dim)\n\n    def on_auto_dimension_changed(self):\n        self.manual_box.setEnabled(not self.auto_dimension)\n        if self.auto_dimension:\n            self.recompute_dimensions()\n        else:\n            spin_x = self.opt_controls.spin_x\n            spin_y = self.opt_controls.spin_y\n            dimx = int(5 * np.round(spin_x.value() / 5))\n            dimy = int(5 * np.round(spin_y.value() / 5))\n            spin_x.setValue(dimx)\n            spin_y.setValue(dimy)\n\n    def on_attr_color_change(self):\n        self.controls.pie_charts.setEnabled(self.attr_color is not None)\n        self.set_color_bins()\n        self.create_legend()\n        self.rescale()\n        self._redraw()\n\n    def on_attr_size_change(self):\n        self._redraw()\n\n    def on_pie_chart_change(self):\n        self._redraw()\n\n    def clear_selection(self):\n        self.selection = None\n        self.redraw_selection()\n\n    def on_selection_change(self, selection, action=SomView.SelectionSet):\n        if self.data is None:  # clicks on empty canvas\n            return\n        if self.selection is None:\n            self.selection = np.zeros(self.grid_cells.T.shape, dtype=np.int16)\n        if action == SomView.SelectionSet:\n            self.selection[:] = 0\n            self.selection[selection] = 1\n        elif action == SomView.SelectionAddToGroup:\n            self.selection[selection] = max(1, np.max(self.selection))\n        elif action == SomView.SelectionNewGroup:\n            self.selection[selection] = 1 + np.max(self.selection)\n        elif action & SomView.SelectionRemove:\n            self.selection[selection] = 0\n        self.redraw_selection()\n        self.update_output()\n\n    def on_selection_move(self, event: QKeyEvent):\n        if self.selection is None or not np.any(self.selection):\n            if event.key() in (Qt.Key_Right, Qt.Key_Down):\n                x = y = 0\n            else:\n                x = self.size_x - 1\n                y = self.size_y - 1\n        else:\n            x, y = np.nonzero(self.selection)\n            if len(x) > 1:\n                return\n            if event.key() == Qt.Key_Up and y > 0:\n                y -= 1\n            if event.key() == Qt.Key_Down and y < self.size_y - 1:\n                y += 1\n            if event.key() == Qt.Key_Left and x:\n                x -= 1\n            if event.key() == Qt.Key_Right and x < self.size_x - 1:\n                x += 1\n            x -= self.hexagonal and x == self.size_x - 1 and y % 2\n\n        if self.selection is not None and self.selection[x, y]:\n            return\n        selection = np.zeros(self.grid_cells.shape, dtype=bool)\n        selection[x, y] = True\n        self.on_selection_change(selection)\n\n    def on_selection_mark_change(self, marks):\n        self.redraw_selection(marks=marks)\n\n    def redraw_selection(self, marks=None):\n        if self.grid_cells is None:\n            return\n\n        sel_pen = QPen(QBrush(QColor(128, 128, 128)), 2)\n        sel_pen.setCosmetic(True)\n        mark_pen = QPen(QBrush(QColor(128, 128, 128)), 4)\n        mark_pen.setCosmetic(True)\n        pens = [self._grid_pen, sel_pen]\n\n        mark_brush = QBrush(QColor(224, 255, 255))\n        sels = self.selection is not None and np.max(self.selection)\n        palette = LimitedDiscretePalette(number_of_colors=sels + 1)\n        brushes = [QBrush(Qt.NoBrush)] + \\\n                  [QBrush(palette[i].lighter(165)) for i in range(sels)]\n\n        for y in range(self.size_y):\n            for x in range(self.size_x - (y % 2) * self.hexagonal):\n                cell = self.grid_cells[y, x]\n                marked = marks is not None and marks[x, y]\n                sel_group = self.selection is not None and self.selection[x, y]\n                if marked:\n                    cell.setBrush(mark_brush)\n                    cell.setPen(mark_pen)\n                else:\n                    cell.setBrush(brushes[sel_group])\n                    cell.setPen(pens[bool(sel_group)])\n                cell.setZValue(marked or sel_group)\n\n    def restart_som_pressed(self):\n        if self._optimizer_thread is not None:\n            self.stop_optimization = True\n            self._optimizer.stop_optimization = True\n        else:\n            self.start_som()\n\n    def start_som(self):\n        self.read_controls()\n        self.update_layout()\n        self.clear_selection()\n        if self.cont_x is not None:\n            self.enable_controls(False)\n            self._recompute_som()\n        else:\n            self.update_output()\n\n    def read_controls(self):\n        c = self.opt_controls\n        self.hexagonal = c.shape.currentIndex() == 0\n        self.size_x = c.spin_x.value()\n        self.size_y = c.spin_y.value()\n\n    def enable_controls(self, enable):\n        c = self.opt_controls\n        c.shape.setEnabled(enable)\n        c.auto_dim.setEnabled(enable)\n        c.start.setText(\"Start\" if enable else \"Stop\")\n\n    def update_layout(self):\n        self.set_legend_pos()\n        if self.elements:  # Prevent having redrawn grid but with old elements\n            self.scene.removeItem(self.elements)\n            self.elements = None\n        self.redraw_grid()\n        self.rescale()\n\n    def _redraw(self):\n        self.Warning.missing_colors.clear()\n        if self.elements:\n            self.scene.removeItem(self.elements)\n            self.elements = None\n        self.view.set_dimensions(self.size_x, self.size_y, self.hexagonal)\n\n        if self.cells is None:\n            return\n        sizes = self.cells[:, :, 1] - self.cells[:, :, 0]\n        sizes = sizes.astype(float)\n        if not self.size_by_instances:\n            sizes[sizes != 0] = 0.8\n        else:\n            sizes *= 0.8 / np.max(sizes)\n\n        self.elements = QGraphicsItemGroup()\n        self.scene.addItem(self.elements)\n        if self.attr_color is None:\n            self._draw_same_color(sizes)\n        elif self.pie_charts:\n            self._draw_pie_charts(sizes)\n        else:\n            self._draw_colored_circles(sizes)\n\n    @property\n    def _grid_factors(self):\n        return (0.5, sqrt3_2) if self.hexagonal else (0, 1)\n\n    def _draw_same_color(self, sizes):\n        fx, fy = self._grid_factors\n        color = QColor(64, 64, 64)\n        for y in range(self.size_y):\n            for x in range(self.size_x - self.hexagonal * (y % 2)):\n                r = sizes[x, y]\n                n = len(self.get_member_indices(x, y))\n                if not r:\n                    continue\n                ellipse = ColoredCircle(r / 2, color, 0)\n                ellipse.setPos(x + (y % 2) * fx, y * fy)\n                ellipse.setToolTip(f\"{n} instances\")\n                self.elements.addToGroup(ellipse)\n\n    def _get_color_column(self):\n        color_column = \\\n            self.data.get_column_view(self.attr_color)[0].astype(float,\n                                                                 copy=False)\n        if self.attr_color.is_discrete:\n            with np.errstate(invalid=\"ignore\"):\n                int_col = color_column.astype(int)\n            int_col[np.isnan(color_column)] = len(self.colors)\n        else:\n            int_col = np.zeros(len(color_column), dtype=int)\n            # The following line is unnecessary because rows with missing\n            # numeric data are excluded. Uncomment it if you change SOM to\n            # tolerate missing values.\n            # int_col[np.isnan(color_column)] = len(self.colors)\n            for i, thresh in enumerate(self.thresholds, start=1):\n                int_col[color_column >= thresh] = i\n        return int_col\n\n    def _tooltip(self, colors, distribution):\n        if self.attr_color.is_discrete:\n            values = self.attr_color.values\n        else:\n            values = self._bin_names()\n        tot = np.sum(distribution)\n        nbhp = \"\\N{NON-BREAKING HYPHEN}\"\n        return '<table style=\"white-space: nowrap\">' + \"\".join(f\"\"\"\n            <tr>\n                <td>\n                    <font color={color.name()}>■</font>\n                    <b>{escape(val).replace(\"-\", nbhp)}</b>:\n                </td>\n                <td>\n                    {n} ({n / tot * 100:.1f}&nbsp;%)\n                </td>\n            </tr>\n            \"\"\" for color, val, n in zip(colors, values, distribution) if n) \\\n            + \"</table>\"\n\n    def _draw_pie_charts(self, sizes):\n        fx, fy = self._grid_factors\n        color_column = self._get_color_column()\n        colors = self.colors.qcolors_w_nan\n        for y in range(self.size_y):\n            for x in range(self.size_x - self.hexagonal * (y % 2)):\n                r = sizes[x, y]\n                if not r:\n                    self.grid_cells[y, x].setToolTip(\"\")\n                    continue\n                members = self.get_member_indices(x, y)\n                color_dist = np.bincount(color_column[members],\n                                         minlength=len(colors))\n                rel_color_dist = color_dist.astype(float) / len(members)\n                pie = PieChart(rel_color_dist, r / 2, colors)\n                pie.setToolTip(self._tooltip(colors, color_dist))\n                self.elements.addToGroup(pie)\n                pie.setPos(x + (y % 2) * fx, y * fy)\n\n    def _draw_colored_circles(self, sizes):\n        fx, fy = self._grid_factors\n        color_column = self._get_color_column()\n        qcolors = self.colors.qcolors_w_nan\n        for y in range(self.size_y):\n            for x in range(self.size_x - self.hexagonal * (y % 2)):\n                r = sizes[x, y]\n                if not r:\n                    continue\n                members = self.get_member_indices(x, y)\n                color_dist = color_column[members]\n                color_dist = color_dist[color_dist < len(self.colors)]\n                if len(color_dist) != len(members):\n                    self.Warning.missing_colors(self.attr_color.name)\n                bc = np.bincount(color_dist, minlength=len(self.colors))\n                color = qcolors[np.argmax(bc)]\n                ellipse = ColoredCircle(r / 2, color, np.max(bc) / len(members))\n                ellipse.setPos(x + (y % 2) * fx, y * fy)\n                ellipse.setToolTip(self._tooltip(qcolors, bc))\n                self.elements.addToGroup(ellipse)\n\n    def redraw_grid(self):\n        if self.grid is not None:\n            self.scene.removeItem(self.grid)\n        self.grid = QGraphicsItemGroup()\n        self.grid.setZValue(-200)\n        self.grid_cells = np.full((self.size_y, self.size_x), None)\n        for y in range(self.size_y):\n            for x in range(self.size_x - (y % 2) * self.hexagonal):\n                if self.hexagonal:\n                    cell = QGraphicsPathItem(_hexagon_path)\n                    cell.setPos(x + (y % 2) / 2, y * sqrt3_2)\n                else:\n                    cell = QGraphicsRectItem(x - 0.5, y - 0.5, 1, 1)\n                self.grid_cells[y, x] = cell\n                cell.setPen(self._grid_pen)\n                self.grid.addToGroup(cell)\n        self.scene.addItem(self.grid)\n\n    def get_member_indices(self, x, y):\n        i, j = self.cells[x, y]\n        return self.member_data[i:j]\n\n    def _recompute_som(self):\n        if self.cont_x is None:\n            return\n\n        som = SOM(\n            self.size_x, self.size_y,\n            hexagonal=self.hexagonal,\n            pca_init=self.initialization == 0,\n            random_seed=0 if self.initialization == 2 else None\n        )\n\n        class Optimizer(QObject):\n            update = Signal(float, np.ndarray, np.ndarray)\n            done = Signal(SOM)\n            stopped = Signal()\n            stop_optimization = False\n\n            def __init__(self, data, som):\n                super().__init__()\n                self.som = som\n                self.data = data\n\n            def callback(self, progress):\n                self.update.emit(\n                    progress,\n                    self.som.weights.copy(), self.som.ssum_weights.copy())\n                return not self.stop_optimization\n\n            def run(self):\n                try:\n                    self.som.fit(self.data, N_ITERATIONS,\n                                 callback=self.callback)\n                    # Report an exception, but still remove the thread\n                finally:\n                    self.done.emit(self.som)\n                    self.stopped.emit()\n\n        def thread_finished():\n            self._optimizer = None\n            self._optimizer_thread = None\n\n        self.progressBarInit()\n\n        self._optimizer = Optimizer(self.cont_x, som)\n        self._optimizer_thread = QThread()\n        self._optimizer_thread.setStackSize(5 * 2 ** 20)\n        self._optimizer.update.connect(self.__update)\n        self._optimizer.done.connect(self.__done)\n        self._optimizer.stopped.connect(self._optimizer_thread.quit)\n        self._optimizer.moveToThread(self._optimizer_thread)\n        self._optimizer_thread.started.connect(self._optimizer.run)\n        self._optimizer_thread.finished.connect(thread_finished)\n        self.stop_optimization = False\n        self._optimizer_thread.start()\n\n    @Slot(float, object, object)\n    def __update(self, _progress, weights, ssum_weights):\n        self.progressBarSet(_progress)\n        self._assign_instances(weights, ssum_weights)\n        self._redraw()\n\n    @Slot(object)\n    def __done(self, som):\n        self.enable_controls(True)\n        self.progressBarFinished()\n        self._assign_instances(som.weights, som.ssum_weights)\n        self._redraw()\n        # This is the first time we know what was selected (assuming that\n        # initialization is not set to random)\n        if self.__pending_selection is not None:\n            self.on_selection_change(self.__pending_selection)\n            self.__pending_selection = None\n        self.update_output()\n\n    def stop_optimization_and_wait(self):\n        if self._optimizer_thread is not None:\n            self.stop_optimization = True\n            self._optimizer.stop_optimization = True\n            self._optimizer_thread.quit()\n            self._optimizer_thread.wait()\n            self._optimizer_thread = None\n\n    def onDeleteWidget(self):\n        self.stop_optimization_and_wait()\n        self.clear()\n        super().onDeleteWidget()\n\n    def _assign_instances(self, weights, ssum_weights):\n        if self.cont_x is None:\n            return  # the widget is shutting down while signals still processed\n        assignments = SOM.winner_from_weights(\n            self.cont_x, weights, ssum_weights, self.hexagonal)\n        members = defaultdict(list)\n        for i, (x, y) in enumerate(assignments):\n            members[(x, y)].append(i)\n        members.pop(None, None)\n        self.cells = np.empty((self.size_x, self.size_y, 2), dtype=int)\n        self.member_data = np.empty(self.cont_x.shape[0], dtype=int)\n        index = 0\n        for x in range(self.size_x):\n            for y in range(self.size_y):\n                nmembers = len(members[(x, y)])\n                self.member_data[index:index + nmembers] = members[(x, y)]\n                self.cells[x, y] = [index, index + nmembers]\n                index += nmembers\n\n    def resizeEvent(self, event):\n        super().resizeEvent(event)\n        self.create_legend()  # re-wrap lines if necessary\n        self.rescale()\n\n    def rescale(self):\n        if self.legend:\n            leg_height = self.legend.boundingRect().height()\n            leg_extra = 1.5\n        else:\n            leg_height = 0\n            leg_extra = 1\n\n        vw, vh = self.view.width(), self.view.height() - leg_height\n        scale = min(vw / (self.size_x + 1),\n                    vh / ((self.size_y + leg_extra) * self._grid_factors[1]))\n        self.view.setTransform(QTransform.fromScale(scale, scale))\n        if self.hexagonal:\n            self.view.setSceneRect(\n                0, -1, self.size_x - 1,\n                (self.size_y + leg_extra) * sqrt3_2 + leg_height / scale)\n        else:\n            self.view.setSceneRect(\n                -0.25, -0.25, self.size_x - 0.5,\n                self.size_y - 0.5 + leg_height / scale)\n\n    def update_output(self):\n        if self.data is None:\n            self.Outputs.selected_data.send(None)\n            self.Outputs.annotated_data.send(None)\n            return\n\n        indices = np.zeros(len(self.data), dtype=int)\n        if self.selection is not None and np.any(self.selection):\n            for y in range(self.size_y):\n                for x in range(self.size_x):\n                    rows = self.get_member_indices(x, y)\n                    indices[rows] = self.selection[x, y]\n\n        if np.any(indices):\n            sel_data = create_groups_table(self.data, indices, False, \"Group\")\n            self.Outputs.selected_data.send(sel_data)\n        else:\n            self.Outputs.selected_data.send(None)\n\n        if np.max(indices) > 1:\n            annotated = create_groups_table(self.data, indices)\n        else:\n            annotated = create_annotated_table(\n                self.data, np.flatnonzero(indices))\n        self.Outputs.annotated_data.send(annotated)\n\n    def set_color_bins(self):\n        if self.attr_color is None:\n            self.thresholds = self.bin_labels = self.colors = None\n        elif self.attr_color.is_discrete:\n            self.thresholds = self.bin_labels = None\n            self.colors = self.attr_color.palette\n        else:\n            col = self.data.get_column_view(self.attr_color)[0].astype(float)\n            if self.attr_color.is_time:\n                binning = time_binnings(col, min_bins=4)[-1]\n            else:\n                binning = decimal_binnings(col, min_bins=4)[-1]\n            self.thresholds = binning.thresholds[1:-1]\n            self.bin_labels = (binning.labels[1:-1], binning.short_labels[1:-1])\n            palette = BinnedContinuousPalette.from_palette(\n                self.attr_color.palette, binning.thresholds)\n            self.colors = palette\n\n    def create_legend(self):\n        if self.legend is not None:\n            self.scene.removeItem(self.legend)\n            self.legend = None\n        if self.attr_color is None:\n            return\n\n        if self.attr_color.is_discrete:\n            names = self.attr_color.values\n        else:\n            names = self._bin_names()\n\n        items = []\n        size = 8\n        for name, color in zip(names, self.colors.qcolors):\n            item = QGraphicsItemGroup()\n            item.addToGroup(\n                CanvasRectangle(None, -size / 2, -size / 2, size, size,\n                                Qt.gray, color))\n            item.addToGroup(CanvasText(None, name, size, 0, Qt.AlignVCenter))\n            items.append(item)\n\n        self.legend = wrap_legend_items(\n            items, hspacing=20, vspacing=16 + size,\n            max_width=self.view.width() - 25)\n        self.legend.setFlags(self.legend.ItemIgnoresTransformations)\n        self.legend.setTransform(\n            QTransform.fromTranslate(-self.legend.boundingRect().width() / 2,\n                                     0))\n        self.scene.addItem(self.legend)\n        self.set_legend_pos()\n\n    def _bin_names(self):\n        labels, short_labels = self.bin_labels\n        return \\\n            [f\"< {labels[0]}\"] \\\n            + [f\"{x} - {y}\" for x, y in zip(labels, short_labels[1:])] \\\n            + [f\"≥ {labels[-1]}\"]\n\n    def set_legend_pos(self):\n        if self.legend is None:\n            return\n        self.legend.setPos(\n            self.size_x / 2,\n            (self.size_y + 0.2 + 0.3 * self.hexagonal) * self._grid_factors[1])\n\n    def send_report(self):\n        self.report_plot()\n        if self.attr_color:\n            self.report_caption(\n                f\"Self-organizing map colored by '{self.attr_color.name}'\")",
                            "ast_type_str": "ClassDef",
                            "start_lineno": 174,
                            "end_lineno": 890,
                            "parent": null
                        }
                    ],
                    "added_entities": [],
                    "deleted_entities": []
                },
                {
                    "descriptor": {
                        "old_range": {
                            "start": 345,
                            "length": 6
                        },
                        "new_range": {
                            "start": 344,
                            "length": 12
                        },
                        "section": "class OWSOM(OWWidget):"
                    },
                    "line_group": {
                        "all_lines": [
                            {
                                "content": "            else:",
                                "type": "context"
                            },
                            {
                                "content": "                prepare_data()",
                                "type": "context"
                            },
                            {
                                "content": "",
                                "type": "context"
                            },
                            {
                                "content": "        invalidated = cont_x is None or self.cont_x is None \\",
                                "type": "added"
                            },
                            {
                                "content": "            or not array_equal(cont_x, self.cont_x)",
                                "type": "added"
                            },
                            {
                                "content": "        if invalidated:",
                                "type": "added"
                            },
                            {
                                "content": "            self.stop_optimization_and_wait()",
                                "type": "added"
                            },
                            {
                                "content": "            self.clear()",
                                "type": "added"
                            },
                            {
                                "content": "",
                                "type": "added"
                            },
                            {
                                "content": "        if self.data is not None:",
                                "type": "context"
                            },
                            {
                                "content": "            self.controls.attr_color.model().set_domain(data.domain)",
                                "type": "context"
                            },
                            {
                                "content": "            self.attr_color = data.domain.class_var",
                                "type": "context"
                            }
                        ]
                    },
                    "modified_entities": [
                        {
                            "file_name": "Orange/widgets/unsupervised/owsom.py",
                            "type": "method",
                            "name": "OWSOM.set_data",
                            "content": "class OWSOM(OWWidget):\n    name = \"Self-Organizing Map\"\n    description = \"Computation of self-organizing map.\"\n    icon = \"icons/SOM.svg\"\n    keywords = [\"SOM\"]\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    settingsHandler = DomainContextHandler()\n    auto_dimension = Setting(True)\n    size_x = Setting(10)\n    size_y = Setting(10)\n    hexagonal = Setting(1)\n    initialization = Setting(0)\n\n    attr_color = ContextSetting(None)\n    size_by_instances = Setting(True)\n    pie_charts = Setting(False)\n    selection = Setting(None, schema_only=True)\n\n    graph_name = \"view\"\n\n    _grid_pen = QPen(QBrush(QColor(224, 224, 224)), 2)\n    _grid_pen.setCosmetic(True)\n\n    OptControls = namedtuple(\n        \"OptControls\",\n        (\"shape\", \"auto_dim\", \"spin_x\", \"spin_y\", \"initialization\", \"start\")\n    )\n\n    class Warning(OWWidget.Warning):\n        ignoring_disc_variables = Msg(\"SOM ignores categorical variables.\")\n        missing_colors = \\\n            Msg(\"Some data instances have undefined value of '{}'.\")\n        missing_values = \\\n            Msg(\"{} data instance{} with undefined value(s) {} not shown.\")\n        single_attribute = Msg(\"Data contains a single numeric column.\")\n\n    class Error(OWWidget.Error):\n        no_numeric_variables = Msg(\"Data contains no numeric columns.\")\n        no_defined_rows = Msg(\"All rows contain at least one undefined value.\")\n\n    def __init__(self):\n        super().__init__()\n        self.__pending_selection = self.selection\n        self._optimizer = None\n        self._optimizer_thread = None\n        self.stop_optimization = False\n\n        self.data = self.cont_x = None\n        self.cells = self.member_data = None\n        self.selection = None\n        self.colors = self.thresholds = self.bin_labels = None\n\n        box = gui.vBox(self.controlArea, box=\"SOM\")\n        shape = gui.comboBox(\n            box, self, \"\", items=(\"Hexagonal grid\", \"Square grid\"))\n        shape.setCurrentIndex(1 - self.hexagonal)\n\n        box2 = gui.indentedBox(box, 10)\n        auto_dim = gui.checkBox(\n            box2, self, \"auto_dimension\", \"Set dimensions automatically\",\n            callback=self.on_auto_dimension_changed)\n        self.manual_box = box3 = gui.hBox(box2)\n        spinargs = dict(\n            value=\"\", widget=box3, master=self, minv=5, maxv=100, step=5,\n            alignment=Qt.AlignRight)\n        spin_x = gui.spin(**spinargs)\n        spin_x.setValue(self.size_x)\n        gui.widgetLabel(box3, \"×\")\n        spin_y = gui.spin(**spinargs)\n        spin_y.setValue(self.size_y)\n        gui.rubber(box3)\n        self.manual_box.setEnabled(not self.auto_dimension)\n\n        initialization = gui.comboBox(\n            box, self, \"initialization\",\n            items=(\"Initialize with PCA\", \"Random initialization\",\n                   \"Replicable random\"))\n\n        start = gui.button(\n            box, self, \"Restart\", callback=self.restart_som_pressed,\n            sizePolicy=(QSizePolicy.MinimumExpanding, QSizePolicy.Fixed))\n\n        self.opt_controls = self.OptControls(\n            shape, auto_dim, spin_x, spin_y, initialization, start)\n\n        box = gui.vBox(self.controlArea, \"Color\")\n        gui.comboBox(\n            box, self, \"attr_color\", searchable=True,\n            callback=self.on_attr_color_change,\n            model=DomainModel(placeholder=\"(Same color)\",\n                              valid_types=DomainModel.PRIMITIVE))\n        gui.checkBox(\n            box, self, \"pie_charts\", label=\"Show pie charts\",\n            callback=self.on_pie_chart_change)\n        gui.checkBox(\n            box, self, \"size_by_instances\", label=\"Size by number of instances\",\n            callback=self.on_attr_size_change)\n\n        gui.rubber(self.controlArea)\n\n        self.scene = QGraphicsScene(self)\n\n        self.view = SomView(self.scene)\n        self.view.setMinimumWidth(400)\n        self.view.setMinimumHeight(400)\n        self.view.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)\n        self.view.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)\n        self.view.setRenderHint(QPainter.Antialiasing)\n        self.view.selection_changed.connect(self.on_selection_change)\n        self.view.selection_moved.connect(self.on_selection_move)\n        self.view.selection_mark_changed.connect(self.on_selection_mark_change)\n        self.mainArea.layout().addWidget(self.view)\n\n        self.elements = None\n        self.grid = None\n        self.grid_cells = None\n        self.legend = None\n\n    @Inputs.data\n    def set_data(self, data):\n        def prepare_data():\n            if len(cont_attrs) < len(attrs):\n                self.Warning.ignoring_disc_variables()\n            if len(cont_attrs) == 1:\n                self.Warning.single_attribute()\n            x = Table.from_table(Domain(cont_attrs), data).X\n            if sp.issparse(x):\n                self.data = data\n                self.cont_x = x.tocsr()\n            else:\n                mask = np.all(np.isfinite(x), axis=1)\n                if not np.any(mask):\n                    self.Error.no_defined_rows()\n                else:\n                    if np.all(mask):\n                        self.data = data\n                        self.cont_x = x.copy()\n                    else:\n                        self.data = data[mask]\n                        self.cont_x = x[mask]\n                    self.cont_x -= np.min(self.cont_x, axis=0)[None, :]\n                    sums = np.sum(self.cont_x, axis=0)[None, :]\n                    sums[sums == 0] = 1\n                    self.cont_x /= sums\n\n        def set_warnings():\n            missing = len(data) - len(self.data)\n            if missing == 1:\n                self.Warning.missing_values(1, \"\", \"is\")\n            elif missing > 1:\n                self.Warning.missing_values(missing, \"s\", \"are\")\n\n        cont_x = self.cont_x.copy() if self.cont_x is not None else None\n        self.data = self.cont_x = None\n        self.closeContext()\n        self.clear_messages()\n\n        if data is not None:\n            attrs = data.domain.attributes\n            cont_attrs = [var for var in attrs if var.is_continuous]\n            if not cont_attrs:\n                self.Error.no_numeric_variables()\n            else:\n                prepare_data()\n\n        invalidated = cont_x is None or self.cont_x is None \\\n            or not array_equal(cont_x, self.cont_x)\n        if invalidated:\n            self.stop_optimization_and_wait()\n            self.clear()\n\n        if self.data is not None:\n            self.controls.attr_color.model().set_domain(data.domain)\n            self.attr_color = data.domain.class_var\n            set_warnings()\n\n        self.openContext(self.data)\n        self.set_color_bins()\n        self.create_legend()\n        if invalidated:\n            self.recompute_dimensions()\n            self.start_som()\n        else:\n            self._redraw()\n\n    def clear(self):\n        self.cells = self.member_data = None\n        self.attr_color = None\n        self.colors = self.thresholds = self.bin_labels = None\n        if self.elements is not None:\n            self.scene.removeItem(self.elements)\n            self.elements = None\n        self.clear_selection()\n        self.controls.attr_color.model().set_domain(None)\n\n    def recompute_dimensions(self):\n        if not self.auto_dimension or self.cont_x is None:\n            return\n        dim = max(5, int(np.ceil(np.sqrt(5 * np.sqrt(self.cont_x.shape[0])))))\n        self.opt_controls.spin_x.setValue(dim)\n        self.opt_controls.spin_y.setValue(dim)\n\n    def on_auto_dimension_changed(self):\n        self.manual_box.setEnabled(not self.auto_dimension)\n        if self.auto_dimension:\n            self.recompute_dimensions()\n        else:\n            spin_x = self.opt_controls.spin_x\n            spin_y = self.opt_controls.spin_y\n            dimx = int(5 * np.round(spin_x.value() / 5))\n            dimy = int(5 * np.round(spin_y.value() / 5))\n            spin_x.setValue(dimx)\n            spin_y.setValue(dimy)\n\n    def on_attr_color_change(self):\n        self.controls.pie_charts.setEnabled(self.attr_color is not None)\n        self.set_color_bins()\n        self.create_legend()\n        self.rescale()\n        self._redraw()\n\n    def on_attr_size_change(self):\n        self._redraw()\n\n    def on_pie_chart_change(self):\n        self._redraw()\n\n    def clear_selection(self):\n        self.selection = None\n        self.redraw_selection()\n\n    def on_selection_change(self, selection, action=SomView.SelectionSet):\n        if self.data is None:  # clicks on empty canvas\n            return\n        if self.selection is None:\n            self.selection = np.zeros(self.grid_cells.T.shape, dtype=np.int16)\n        if action == SomView.SelectionSet:\n            self.selection[:] = 0\n            self.selection[selection] = 1\n        elif action == SomView.SelectionAddToGroup:\n            self.selection[selection] = max(1, np.max(self.selection))\n        elif action == SomView.SelectionNewGroup:\n            self.selection[selection] = 1 + np.max(self.selection)\n        elif action & SomView.SelectionRemove:\n            self.selection[selection] = 0\n        self.redraw_selection()\n        self.update_output()\n\n    def on_selection_move(self, event: QKeyEvent):\n        if self.selection is None or not np.any(self.selection):\n            if event.key() in (Qt.Key_Right, Qt.Key_Down):\n                x = y = 0\n            else:\n                x = self.size_x - 1\n                y = self.size_y - 1\n        else:\n            x, y = np.nonzero(self.selection)\n            if len(x) > 1:\n                return\n            if event.key() == Qt.Key_Up and y > 0:\n                y -= 1\n            if event.key() == Qt.Key_Down and y < self.size_y - 1:\n                y += 1\n            if event.key() == Qt.Key_Left and x:\n                x -= 1\n            if event.key() == Qt.Key_Right and x < self.size_x - 1:\n                x += 1\n            x -= self.hexagonal and x == self.size_x - 1 and y % 2\n\n        if self.selection is not None and self.selection[x, y]:\n            return\n        selection = np.zeros(self.grid_cells.shape, dtype=bool)\n        selection[x, y] = True\n        self.on_selection_change(selection)\n\n    def on_selection_mark_change(self, marks):\n        self.redraw_selection(marks=marks)\n\n    def redraw_selection(self, marks=None):\n        if self.grid_cells is None:\n            return\n\n        sel_pen = QPen(QBrush(QColor(128, 128, 128)), 2)\n        sel_pen.setCosmetic(True)\n        mark_pen = QPen(QBrush(QColor(128, 128, 128)), 4)\n        mark_pen.setCosmetic(True)\n        pens = [self._grid_pen, sel_pen]\n\n        mark_brush = QBrush(QColor(224, 255, 255))\n        sels = self.selection is not None and np.max(self.selection)\n        palette = LimitedDiscretePalette(number_of_colors=sels + 1)\n        brushes = [QBrush(Qt.NoBrush)] + \\\n                  [QBrush(palette[i].lighter(165)) for i in range(sels)]\n\n        for y in range(self.size_y):\n            for x in range(self.size_x - (y % 2) * self.hexagonal):\n                cell = self.grid_cells[y, x]\n                marked = marks is not None and marks[x, y]\n                sel_group = self.selection is not None and self.selection[x, y]\n                if marked:\n                    cell.setBrush(mark_brush)\n                    cell.setPen(mark_pen)\n                else:\n                    cell.setBrush(brushes[sel_group])\n                    cell.setPen(pens[bool(sel_group)])\n                cell.setZValue(marked or sel_group)\n\n    def restart_som_pressed(self):\n        if self._optimizer_thread is not None:\n            self.stop_optimization = True\n            self._optimizer.stop_optimization = True\n        else:\n            self.start_som()\n\n    def start_som(self):\n        self.read_controls()\n        self.update_layout()\n        self.clear_selection()\n        if self.cont_x is not None:\n            self.enable_controls(False)\n            self._recompute_som()\n        else:\n            self.update_output()\n\n    def read_controls(self):\n        c = self.opt_controls\n        self.hexagonal = c.shape.currentIndex() == 0\n        self.size_x = c.spin_x.value()\n        self.size_y = c.spin_y.value()\n\n    def enable_controls(self, enable):\n        c = self.opt_controls\n        c.shape.setEnabled(enable)\n        c.auto_dim.setEnabled(enable)\n        c.start.setText(\"Start\" if enable else \"Stop\")\n\n    def update_layout(self):\n        self.set_legend_pos()\n        if self.elements:  # Prevent having redrawn grid but with old elements\n            self.scene.removeItem(self.elements)\n            self.elements = None\n        self.redraw_grid()\n        self.rescale()\n\n    def _redraw(self):\n        self.Warning.missing_colors.clear()\n        if self.elements:\n            self.scene.removeItem(self.elements)\n            self.elements = None\n        self.view.set_dimensions(self.size_x, self.size_y, self.hexagonal)\n\n        if self.cells is None:\n            return\n        sizes = self.cells[:, :, 1] - self.cells[:, :, 0]\n        sizes = sizes.astype(float)\n        if not self.size_by_instances:\n            sizes[sizes != 0] = 0.8\n        else:\n            sizes *= 0.8 / np.max(sizes)\n\n        self.elements = QGraphicsItemGroup()\n        self.scene.addItem(self.elements)\n        if self.attr_color is None:\n            self._draw_same_color(sizes)\n        elif self.pie_charts:\n            self._draw_pie_charts(sizes)\n        else:\n            self._draw_colored_circles(sizes)\n\n    @property\n    def _grid_factors(self):\n        return (0.5, sqrt3_2) if self.hexagonal else (0, 1)\n\n    def _draw_same_color(self, sizes):\n        fx, fy = self._grid_factors\n        color = QColor(64, 64, 64)\n        for y in range(self.size_y):\n            for x in range(self.size_x - self.hexagonal * (y % 2)):\n                r = sizes[x, y]\n                n = len(self.get_member_indices(x, y))\n                if not r:\n                    continue\n                ellipse = ColoredCircle(r / 2, color, 0)\n                ellipse.setPos(x + (y % 2) * fx, y * fy)\n                ellipse.setToolTip(f\"{n} instances\")\n                self.elements.addToGroup(ellipse)\n\n    def _get_color_column(self):\n        color_column = \\\n            self.data.get_column_view(self.attr_color)[0].astype(float,\n                                                                 copy=False)\n        if self.attr_color.is_discrete:\n            with np.errstate(invalid=\"ignore\"):\n                int_col = color_column.astype(int)\n            int_col[np.isnan(color_column)] = len(self.colors)\n        else:\n            int_col = np.zeros(len(color_column), dtype=int)\n            # The following line is unnecessary because rows with missing\n            # numeric data are excluded. Uncomment it if you change SOM to\n            # tolerate missing values.\n            # int_col[np.isnan(color_column)] = len(self.colors)\n            for i, thresh in enumerate(self.thresholds, start=1):\n                int_col[color_column >= thresh] = i\n        return int_col\n\n    def _tooltip(self, colors, distribution):\n        if self.attr_color.is_discrete:\n            values = self.attr_color.values\n        else:\n            values = self._bin_names()\n        tot = np.sum(distribution)\n        nbhp = \"\\N{NON-BREAKING HYPHEN}\"\n        return '<table style=\"white-space: nowrap\">' + \"\".join(f\"\"\"\n            <tr>\n                <td>\n                    <font color={color.name()}>■</font>\n                    <b>{escape(val).replace(\"-\", nbhp)}</b>:\n                </td>\n                <td>\n                    {n} ({n / tot * 100:.1f}&nbsp;%)\n                </td>\n            </tr>\n            \"\"\" for color, val, n in zip(colors, values, distribution) if n) \\\n            + \"</table>\"\n\n    def _draw_pie_charts(self, sizes):\n        fx, fy = self._grid_factors\n        color_column = self._get_color_column()\n        colors = self.colors.qcolors_w_nan\n        for y in range(self.size_y):\n            for x in range(self.size_x - self.hexagonal * (y % 2)):\n                r = sizes[x, y]\n                if not r:\n                    self.grid_cells[y, x].setToolTip(\"\")\n                    continue\n                members = self.get_member_indices(x, y)\n                color_dist = np.bincount(color_column[members],\n                                         minlength=len(colors))\n                rel_color_dist = color_dist.astype(float) / len(members)\n                pie = PieChart(rel_color_dist, r / 2, colors)\n                pie.setToolTip(self._tooltip(colors, color_dist))\n                self.elements.addToGroup(pie)\n                pie.setPos(x + (y % 2) * fx, y * fy)\n\n    def _draw_colored_circles(self, sizes):\n        fx, fy = self._grid_factors\n        color_column = self._get_color_column()\n        qcolors = self.colors.qcolors_w_nan\n        for y in range(self.size_y):\n            for x in range(self.size_x - self.hexagonal * (y % 2)):\n                r = sizes[x, y]\n                if not r:\n                    continue\n                members = self.get_member_indices(x, y)\n                color_dist = color_column[members]\n                color_dist = color_dist[color_dist < len(self.colors)]\n                if len(color_dist) != len(members):\n                    self.Warning.missing_colors(self.attr_color.name)\n                bc = np.bincount(color_dist, minlength=len(self.colors))\n                color = qcolors[np.argmax(bc)]\n                ellipse = ColoredCircle(r / 2, color, np.max(bc) / len(members))\n                ellipse.setPos(x + (y % 2) * fx, y * fy)\n                ellipse.setToolTip(self._tooltip(qcolors, bc))\n                self.elements.addToGroup(ellipse)\n\n    def redraw_grid(self):\n        if self.grid is not None:\n            self.scene.removeItem(self.grid)\n        self.grid = QGraphicsItemGroup()\n        self.grid.setZValue(-200)\n        self.grid_cells = np.full((self.size_y, self.size_x), None)\n        for y in range(self.size_y):\n            for x in range(self.size_x - (y % 2) * self.hexagonal):\n                if self.hexagonal:\n                    cell = QGraphicsPathItem(_hexagon_path)\n                    cell.setPos(x + (y % 2) / 2, y * sqrt3_2)\n                else:\n                    cell = QGraphicsRectItem(x - 0.5, y - 0.5, 1, 1)\n                self.grid_cells[y, x] = cell\n                cell.setPen(self._grid_pen)\n                self.grid.addToGroup(cell)\n        self.scene.addItem(self.grid)\n\n    def get_member_indices(self, x, y):\n        i, j = self.cells[x, y]\n        return self.member_data[i:j]\n\n    def _recompute_som(self):\n        if self.cont_x is None:\n            return\n\n        som = SOM(\n            self.size_x, self.size_y,\n            hexagonal=self.hexagonal,\n            pca_init=self.initialization == 0,\n            random_seed=0 if self.initialization == 2 else None\n        )\n\n        class Optimizer(QObject):\n            update = Signal(float, np.ndarray, np.ndarray)\n            done = Signal(SOM)\n            stopped = Signal()\n            stop_optimization = False\n\n            def __init__(self, data, som):\n                super().__init__()\n                self.som = som\n                self.data = data\n\n            def callback(self, progress):\n                self.update.emit(\n                    progress,\n                    self.som.weights.copy(), self.som.ssum_weights.copy())\n                return not self.stop_optimization\n\n            def run(self):\n                try:\n                    self.som.fit(self.data, N_ITERATIONS,\n                                 callback=self.callback)\n                    # Report an exception, but still remove the thread\n                finally:\n                    self.done.emit(self.som)\n                    self.stopped.emit()\n\n        def thread_finished():\n            self._optimizer = None\n            self._optimizer_thread = None\n\n        self.progressBarInit()\n\n        self._optimizer = Optimizer(self.cont_x, som)\n        self._optimizer_thread = QThread()\n        self._optimizer_thread.setStackSize(5 * 2 ** 20)\n        self._optimizer.update.connect(self.__update)\n        self._optimizer.done.connect(self.__done)\n        self._optimizer.stopped.connect(self._optimizer_thread.quit)\n        self._optimizer.moveToThread(self._optimizer_thread)\n        self._optimizer_thread.started.connect(self._optimizer.run)\n        self._optimizer_thread.finished.connect(thread_finished)\n        self.stop_optimization = False\n        self._optimizer_thread.start()\n\n    @Slot(float, object, object)\n    def __update(self, _progress, weights, ssum_weights):\n        self.progressBarSet(_progress)\n        self._assign_instances(weights, ssum_weights)\n        self._redraw()\n\n    @Slot(object)\n    def __done(self, som):\n        self.enable_controls(True)\n        self.progressBarFinished()\n        self._assign_instances(som.weights, som.ssum_weights)\n        self._redraw()\n        # This is the first time we know what was selected (assuming that\n        # initialization is not set to random)\n        if self.__pending_selection is not None:\n            self.on_selection_change(self.__pending_selection)\n            self.__pending_selection = None\n        self.update_output()\n\n    def stop_optimization_and_wait(self):\n        if self._optimizer_thread is not None:\n            self.stop_optimization = True\n            self._optimizer.stop_optimization = True\n            self._optimizer_thread.quit()\n            self._optimizer_thread.wait()\n            self._optimizer_thread = None\n\n    def onDeleteWidget(self):\n        self.stop_optimization_and_wait()\n        self.clear()\n        super().onDeleteWidget()\n\n    def _assign_instances(self, weights, ssum_weights):\n        if self.cont_x is None:\n            return  # the widget is shutting down while signals still processed\n        assignments = SOM.winner_from_weights(\n            self.cont_x, weights, ssum_weights, self.hexagonal)\n        members = defaultdict(list)\n        for i, (x, y) in enumerate(assignments):\n            members[(x, y)].append(i)\n        members.pop(None, None)\n        self.cells = np.empty((self.size_x, self.size_y, 2), dtype=int)\n        self.member_data = np.empty(self.cont_x.shape[0], dtype=int)\n        index = 0\n        for x in range(self.size_x):\n            for y in range(self.size_y):\n                nmembers = len(members[(x, y)])\n                self.member_data[index:index + nmembers] = members[(x, y)]\n                self.cells[x, y] = [index, index + nmembers]\n                index += nmembers\n\n    def resizeEvent(self, event):\n        super().resizeEvent(event)\n        self.create_legend()  # re-wrap lines if necessary\n        self.rescale()\n\n    def rescale(self):\n        if self.legend:\n            leg_height = self.legend.boundingRect().height()\n            leg_extra = 1.5\n        else:\n            leg_height = 0\n            leg_extra = 1\n\n        vw, vh = self.view.width(), self.view.height() - leg_height\n        scale = min(vw / (self.size_x + 1),\n                    vh / ((self.size_y + leg_extra) * self._grid_factors[1]))\n        self.view.setTransform(QTransform.fromScale(scale, scale))\n        if self.hexagonal:\n            self.view.setSceneRect(\n                0, -1, self.size_x - 1,\n                (self.size_y + leg_extra) * sqrt3_2 + leg_height / scale)\n        else:\n            self.view.setSceneRect(\n                -0.25, -0.25, self.size_x - 0.5,\n                self.size_y - 0.5 + leg_height / scale)\n\n    def update_output(self):\n        if self.data is None:\n            self.Outputs.selected_data.send(None)\n            self.Outputs.annotated_data.send(None)\n            return\n\n        indices = np.zeros(len(self.data), dtype=int)\n        if self.selection is not None and np.any(self.selection):\n            for y in range(self.size_y):\n                for x in range(self.size_x):\n                    rows = self.get_member_indices(x, y)\n                    indices[rows] = self.selection[x, y]\n\n        if np.any(indices):\n            sel_data = create_groups_table(self.data, indices, False, \"Group\")\n            self.Outputs.selected_data.send(sel_data)\n        else:\n            self.Outputs.selected_data.send(None)\n\n        if np.max(indices) > 1:\n            annotated = create_groups_table(self.data, indices)\n        else:\n            annotated = create_annotated_table(\n                self.data, np.flatnonzero(indices))\n        self.Outputs.annotated_data.send(annotated)\n\n    def set_color_bins(self):\n        if self.attr_color is None:\n            self.thresholds = self.bin_labels = self.colors = None\n        elif self.attr_color.is_discrete:\n            self.thresholds = self.bin_labels = None\n            self.colors = self.attr_color.palette\n        else:\n            col = self.data.get_column_view(self.attr_color)[0].astype(float)\n            if self.attr_color.is_time:\n                binning = time_binnings(col, min_bins=4)[-1]\n            else:\n                binning = decimal_binnings(col, min_bins=4)[-1]\n            self.thresholds = binning.thresholds[1:-1]\n            self.bin_labels = (binning.labels[1:-1], binning.short_labels[1:-1])\n            palette = BinnedContinuousPalette.from_palette(\n                self.attr_color.palette, binning.thresholds)\n            self.colors = palette\n\n    def create_legend(self):\n        if self.legend is not None:\n            self.scene.removeItem(self.legend)\n            self.legend = None\n        if self.attr_color is None:\n            return\n\n        if self.attr_color.is_discrete:\n            names = self.attr_color.values\n        else:\n            names = self._bin_names()\n\n        items = []\n        size = 8\n        for name, color in zip(names, self.colors.qcolors):\n            item = QGraphicsItemGroup()\n            item.addToGroup(\n                CanvasRectangle(None, -size / 2, -size / 2, size, size,\n                                Qt.gray, color))\n            item.addToGroup(CanvasText(None, name, size, 0, Qt.AlignVCenter))\n            items.append(item)\n\n        self.legend = wrap_legend_items(\n            items, hspacing=20, vspacing=16 + size,\n            max_width=self.view.width() - 25)\n        self.legend.setFlags(self.legend.ItemIgnoresTransformations)\n        self.legend.setTransform(\n            QTransform.fromTranslate(-self.legend.boundingRect().width() / 2,\n                                     0))\n        self.scene.addItem(self.legend)\n        self.set_legend_pos()\n\n    def _bin_names(self):\n        labels, short_labels = self.bin_labels\n        return \\\n            [f\"< {labels[0]}\"] \\\n            + [f\"{x} - {y}\" for x, y in zip(labels, short_labels[1:])] \\\n            + [f\"≥ {labels[-1]}\"]\n\n    def set_legend_pos(self):\n        if self.legend is None:\n            return\n        self.legend.setPos(\n            self.size_x / 2,\n            (self.size_y + 0.2 + 0.3 * self.hexagonal) * self._grid_factors[1])\n\n    def send_report(self):\n        self.report_plot()\n        if self.attr_color:\n            self.report_caption(\n                f\"Self-organizing map colored by '{self.attr_color.name}'\")",
                            "ast_type_str": "ClassDef",
                            "start_lineno": 301,
                            "end_lineno": 365,
                            "parent": {
                                "file_name": "Orange/widgets/unsupervised/owsom.py",
                                "type": "class",
                                "name": "OWSOM",
                                "content": "class OWSOM(OWWidget):\n    name = \"Self-Organizing Map\"\n    description = \"Computation of self-organizing map.\"\n    icon = \"icons/SOM.svg\"\n    keywords = [\"SOM\"]\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    settingsHandler = DomainContextHandler()\n    auto_dimension = Setting(True)\n    size_x = Setting(10)\n    size_y = Setting(10)\n    hexagonal = Setting(1)\n    initialization = Setting(0)\n\n    attr_color = ContextSetting(None)\n    size_by_instances = Setting(True)\n    pie_charts = Setting(False)\n    selection = Setting(None, schema_only=True)\n\n    graph_name = \"view\"\n\n    _grid_pen = QPen(QBrush(QColor(224, 224, 224)), 2)\n    _grid_pen.setCosmetic(True)\n\n    OptControls = namedtuple(\n        \"OptControls\",\n        (\"shape\", \"auto_dim\", \"spin_x\", \"spin_y\", \"initialization\", \"start\")\n    )\n\n    class Warning(OWWidget.Warning):\n        ignoring_disc_variables = Msg(\"SOM ignores categorical variables.\")\n        missing_colors = \\\n            Msg(\"Some data instances have undefined value of '{}'.\")\n        missing_values = \\\n            Msg(\"{} data instance{} with undefined value(s) {} not shown.\")\n        single_attribute = Msg(\"Data contains a single numeric column.\")\n\n    class Error(OWWidget.Error):\n        no_numeric_variables = Msg(\"Data contains no numeric columns.\")\n        no_defined_rows = Msg(\"All rows contain at least one undefined value.\")\n\n    def __init__(self):\n        super().__init__()\n        self.__pending_selection = self.selection\n        self._optimizer = None\n        self._optimizer_thread = None\n        self.stop_optimization = False\n\n        self.data = self.cont_x = None\n        self.cells = self.member_data = None\n        self.selection = None\n        self.colors = self.thresholds = self.bin_labels = None\n\n        box = gui.vBox(self.controlArea, box=\"SOM\")\n        shape = gui.comboBox(\n            box, self, \"\", items=(\"Hexagonal grid\", \"Square grid\"))\n        shape.setCurrentIndex(1 - self.hexagonal)\n\n        box2 = gui.indentedBox(box, 10)\n        auto_dim = gui.checkBox(\n            box2, self, \"auto_dimension\", \"Set dimensions automatically\",\n            callback=self.on_auto_dimension_changed)\n        self.manual_box = box3 = gui.hBox(box2)\n        spinargs = dict(\n            value=\"\", widget=box3, master=self, minv=5, maxv=100, step=5,\n            alignment=Qt.AlignRight)\n        spin_x = gui.spin(**spinargs)\n        spin_x.setValue(self.size_x)\n        gui.widgetLabel(box3, \"×\")\n        spin_y = gui.spin(**spinargs)\n        spin_y.setValue(self.size_y)\n        gui.rubber(box3)\n        self.manual_box.setEnabled(not self.auto_dimension)\n\n        initialization = gui.comboBox(\n            box, self, \"initialization\",\n            items=(\"Initialize with PCA\", \"Random initialization\",\n                   \"Replicable random\"))\n\n        start = gui.button(\n            box, self, \"Restart\", callback=self.restart_som_pressed,\n            sizePolicy=(QSizePolicy.MinimumExpanding, QSizePolicy.Fixed))\n\n        self.opt_controls = self.OptControls(\n            shape, auto_dim, spin_x, spin_y, initialization, start)\n\n        box = gui.vBox(self.controlArea, \"Color\")\n        gui.comboBox(\n            box, self, \"attr_color\", searchable=True,\n            callback=self.on_attr_color_change,\n            model=DomainModel(placeholder=\"(Same color)\",\n                              valid_types=DomainModel.PRIMITIVE))\n        gui.checkBox(\n            box, self, \"pie_charts\", label=\"Show pie charts\",\n            callback=self.on_pie_chart_change)\n        gui.checkBox(\n            box, self, \"size_by_instances\", label=\"Size by number of instances\",\n            callback=self.on_attr_size_change)\n\n        gui.rubber(self.controlArea)\n\n        self.scene = QGraphicsScene(self)\n\n        self.view = SomView(self.scene)\n        self.view.setMinimumWidth(400)\n        self.view.setMinimumHeight(400)\n        self.view.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)\n        self.view.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)\n        self.view.setRenderHint(QPainter.Antialiasing)\n        self.view.selection_changed.connect(self.on_selection_change)\n        self.view.selection_moved.connect(self.on_selection_move)\n        self.view.selection_mark_changed.connect(self.on_selection_mark_change)\n        self.mainArea.layout().addWidget(self.view)\n\n        self.elements = None\n        self.grid = None\n        self.grid_cells = None\n        self.legend = None\n\n    @Inputs.data\n    def set_data(self, data):\n        def prepare_data():\n            if len(cont_attrs) < len(attrs):\n                self.Warning.ignoring_disc_variables()\n            if len(cont_attrs) == 1:\n                self.Warning.single_attribute()\n            x = Table.from_table(Domain(cont_attrs), data).X\n            if sp.issparse(x):\n                self.data = data\n                self.cont_x = x.tocsr()\n            else:\n                mask = np.all(np.isfinite(x), axis=1)\n                if not np.any(mask):\n                    self.Error.no_defined_rows()\n                else:\n                    if np.all(mask):\n                        self.data = data\n                        self.cont_x = x.copy()\n                    else:\n                        self.data = data[mask]\n                        self.cont_x = x[mask]\n                    self.cont_x -= np.min(self.cont_x, axis=0)[None, :]\n                    sums = np.sum(self.cont_x, axis=0)[None, :]\n                    sums[sums == 0] = 1\n                    self.cont_x /= sums\n\n        def set_warnings():\n            missing = len(data) - len(self.data)\n            if missing == 1:\n                self.Warning.missing_values(1, \"\", \"is\")\n            elif missing > 1:\n                self.Warning.missing_values(missing, \"s\", \"are\")\n\n        cont_x = self.cont_x.copy() if self.cont_x is not None else None\n        self.data = self.cont_x = None\n        self.closeContext()\n        self.clear_messages()\n\n        if data is not None:\n            attrs = data.domain.attributes\n            cont_attrs = [var for var in attrs if var.is_continuous]\n            if not cont_attrs:\n                self.Error.no_numeric_variables()\n            else:\n                prepare_data()\n\n        invalidated = cont_x is None or self.cont_x is None \\\n            or not array_equal(cont_x, self.cont_x)\n        if invalidated:\n            self.stop_optimization_and_wait()\n            self.clear()\n\n        if self.data is not None:\n            self.controls.attr_color.model().set_domain(data.domain)\n            self.attr_color = data.domain.class_var\n            set_warnings()\n\n        self.openContext(self.data)\n        self.set_color_bins()\n        self.create_legend()\n        if invalidated:\n            self.recompute_dimensions()\n            self.start_som()\n        else:\n            self._redraw()\n\n    def clear(self):\n        self.cells = self.member_data = None\n        self.attr_color = None\n        self.colors = self.thresholds = self.bin_labels = None\n        if self.elements is not None:\n            self.scene.removeItem(self.elements)\n            self.elements = None\n        self.clear_selection()\n        self.controls.attr_color.model().set_domain(None)\n\n    def recompute_dimensions(self):\n        if not self.auto_dimension or self.cont_x is None:\n            return\n        dim = max(5, int(np.ceil(np.sqrt(5 * np.sqrt(self.cont_x.shape[0])))))\n        self.opt_controls.spin_x.setValue(dim)\n        self.opt_controls.spin_y.setValue(dim)\n\n    def on_auto_dimension_changed(self):\n        self.manual_box.setEnabled(not self.auto_dimension)\n        if self.auto_dimension:\n            self.recompute_dimensions()\n        else:\n            spin_x = self.opt_controls.spin_x\n            spin_y = self.opt_controls.spin_y\n            dimx = int(5 * np.round(spin_x.value() / 5))\n            dimy = int(5 * np.round(spin_y.value() / 5))\n            spin_x.setValue(dimx)\n            spin_y.setValue(dimy)\n\n    def on_attr_color_change(self):\n        self.controls.pie_charts.setEnabled(self.attr_color is not None)\n        self.set_color_bins()\n        self.create_legend()\n        self.rescale()\n        self._redraw()\n\n    def on_attr_size_change(self):\n        self._redraw()\n\n    def on_pie_chart_change(self):\n        self._redraw()\n\n    def clear_selection(self):\n        self.selection = None\n        self.redraw_selection()\n\n    def on_selection_change(self, selection, action=SomView.SelectionSet):\n        if self.data is None:  # clicks on empty canvas\n            return\n        if self.selection is None:\n            self.selection = np.zeros(self.grid_cells.T.shape, dtype=np.int16)\n        if action == SomView.SelectionSet:\n            self.selection[:] = 0\n            self.selection[selection] = 1\n        elif action == SomView.SelectionAddToGroup:\n            self.selection[selection] = max(1, np.max(self.selection))\n        elif action == SomView.SelectionNewGroup:\n            self.selection[selection] = 1 + np.max(self.selection)\n        elif action & SomView.SelectionRemove:\n            self.selection[selection] = 0\n        self.redraw_selection()\n        self.update_output()\n\n    def on_selection_move(self, event: QKeyEvent):\n        if self.selection is None or not np.any(self.selection):\n            if event.key() in (Qt.Key_Right, Qt.Key_Down):\n                x = y = 0\n            else:\n                x = self.size_x - 1\n                y = self.size_y - 1\n        else:\n            x, y = np.nonzero(self.selection)\n            if len(x) > 1:\n                return\n            if event.key() == Qt.Key_Up and y > 0:\n                y -= 1\n            if event.key() == Qt.Key_Down and y < self.size_y - 1:\n                y += 1\n            if event.key() == Qt.Key_Left and x:\n                x -= 1\n            if event.key() == Qt.Key_Right and x < self.size_x - 1:\n                x += 1\n            x -= self.hexagonal and x == self.size_x - 1 and y % 2\n\n        if self.selection is not None and self.selection[x, y]:\n            return\n        selection = np.zeros(self.grid_cells.shape, dtype=bool)\n        selection[x, y] = True\n        self.on_selection_change(selection)\n\n    def on_selection_mark_change(self, marks):\n        self.redraw_selection(marks=marks)\n\n    def redraw_selection(self, marks=None):\n        if self.grid_cells is None:\n            return\n\n        sel_pen = QPen(QBrush(QColor(128, 128, 128)), 2)\n        sel_pen.setCosmetic(True)\n        mark_pen = QPen(QBrush(QColor(128, 128, 128)), 4)\n        mark_pen.setCosmetic(True)\n        pens = [self._grid_pen, sel_pen]\n\n        mark_brush = QBrush(QColor(224, 255, 255))\n        sels = self.selection is not None and np.max(self.selection)\n        palette = LimitedDiscretePalette(number_of_colors=sels + 1)\n        brushes = [QBrush(Qt.NoBrush)] + \\\n                  [QBrush(palette[i].lighter(165)) for i in range(sels)]\n\n        for y in range(self.size_y):\n            for x in range(self.size_x - (y % 2) * self.hexagonal):\n                cell = self.grid_cells[y, x]\n                marked = marks is not None and marks[x, y]\n                sel_group = self.selection is not None and self.selection[x, y]\n                if marked:\n                    cell.setBrush(mark_brush)\n                    cell.setPen(mark_pen)\n                else:\n                    cell.setBrush(brushes[sel_group])\n                    cell.setPen(pens[bool(sel_group)])\n                cell.setZValue(marked or sel_group)\n\n    def restart_som_pressed(self):\n        if self._optimizer_thread is not None:\n            self.stop_optimization = True\n            self._optimizer.stop_optimization = True\n        else:\n            self.start_som()\n\n    def start_som(self):\n        self.read_controls()\n        self.update_layout()\n        self.clear_selection()\n        if self.cont_x is not None:\n            self.enable_controls(False)\n            self._recompute_som()\n        else:\n            self.update_output()\n\n    def read_controls(self):\n        c = self.opt_controls\n        self.hexagonal = c.shape.currentIndex() == 0\n        self.size_x = c.spin_x.value()\n        self.size_y = c.spin_y.value()\n\n    def enable_controls(self, enable):\n        c = self.opt_controls\n        c.shape.setEnabled(enable)\n        c.auto_dim.setEnabled(enable)\n        c.start.setText(\"Start\" if enable else \"Stop\")\n\n    def update_layout(self):\n        self.set_legend_pos()\n        if self.elements:  # Prevent having redrawn grid but with old elements\n            self.scene.removeItem(self.elements)\n            self.elements = None\n        self.redraw_grid()\n        self.rescale()\n\n    def _redraw(self):\n        self.Warning.missing_colors.clear()\n        if self.elements:\n            self.scene.removeItem(self.elements)\n            self.elements = None\n        self.view.set_dimensions(self.size_x, self.size_y, self.hexagonal)\n\n        if self.cells is None:\n            return\n        sizes = self.cells[:, :, 1] - self.cells[:, :, 0]\n        sizes = sizes.astype(float)\n        if not self.size_by_instances:\n            sizes[sizes != 0] = 0.8\n        else:\n            sizes *= 0.8 / np.max(sizes)\n\n        self.elements = QGraphicsItemGroup()\n        self.scene.addItem(self.elements)\n        if self.attr_color is None:\n            self._draw_same_color(sizes)\n        elif self.pie_charts:\n            self._draw_pie_charts(sizes)\n        else:\n            self._draw_colored_circles(sizes)\n\n    @property\n    def _grid_factors(self):\n        return (0.5, sqrt3_2) if self.hexagonal else (0, 1)\n\n    def _draw_same_color(self, sizes):\n        fx, fy = self._grid_factors\n        color = QColor(64, 64, 64)\n        for y in range(self.size_y):\n            for x in range(self.size_x - self.hexagonal * (y % 2)):\n                r = sizes[x, y]\n                n = len(self.get_member_indices(x, y))\n                if not r:\n                    continue\n                ellipse = ColoredCircle(r / 2, color, 0)\n                ellipse.setPos(x + (y % 2) * fx, y * fy)\n                ellipse.setToolTip(f\"{n} instances\")\n                self.elements.addToGroup(ellipse)\n\n    def _get_color_column(self):\n        color_column = \\\n            self.data.get_column_view(self.attr_color)[0].astype(float,\n                                                                 copy=False)\n        if self.attr_color.is_discrete:\n            with np.errstate(invalid=\"ignore\"):\n                int_col = color_column.astype(int)\n            int_col[np.isnan(color_column)] = len(self.colors)\n        else:\n            int_col = np.zeros(len(color_column), dtype=int)\n            # The following line is unnecessary because rows with missing\n            # numeric data are excluded. Uncomment it if you change SOM to\n            # tolerate missing values.\n            # int_col[np.isnan(color_column)] = len(self.colors)\n            for i, thresh in enumerate(self.thresholds, start=1):\n                int_col[color_column >= thresh] = i\n        return int_col\n\n    def _tooltip(self, colors, distribution):\n        if self.attr_color.is_discrete:\n            values = self.attr_color.values\n        else:\n            values = self._bin_names()\n        tot = np.sum(distribution)\n        nbhp = \"\\N{NON-BREAKING HYPHEN}\"\n        return '<table style=\"white-space: nowrap\">' + \"\".join(f\"\"\"\n            <tr>\n                <td>\n                    <font color={color.name()}>■</font>\n                    <b>{escape(val).replace(\"-\", nbhp)}</b>:\n                </td>\n                <td>\n                    {n} ({n / tot * 100:.1f}&nbsp;%)\n                </td>\n            </tr>\n            \"\"\" for color, val, n in zip(colors, values, distribution) if n) \\\n            + \"</table>\"\n\n    def _draw_pie_charts(self, sizes):\n        fx, fy = self._grid_factors\n        color_column = self._get_color_column()\n        colors = self.colors.qcolors_w_nan\n        for y in range(self.size_y):\n            for x in range(self.size_x - self.hexagonal * (y % 2)):\n                r = sizes[x, y]\n                if not r:\n                    self.grid_cells[y, x].setToolTip(\"\")\n                    continue\n                members = self.get_member_indices(x, y)\n                color_dist = np.bincount(color_column[members],\n                                         minlength=len(colors))\n                rel_color_dist = color_dist.astype(float) / len(members)\n                pie = PieChart(rel_color_dist, r / 2, colors)\n                pie.setToolTip(self._tooltip(colors, color_dist))\n                self.elements.addToGroup(pie)\n                pie.setPos(x + (y % 2) * fx, y * fy)\n\n    def _draw_colored_circles(self, sizes):\n        fx, fy = self._grid_factors\n        color_column = self._get_color_column()\n        qcolors = self.colors.qcolors_w_nan\n        for y in range(self.size_y):\n            for x in range(self.size_x - self.hexagonal * (y % 2)):\n                r = sizes[x, y]\n                if not r:\n                    continue\n                members = self.get_member_indices(x, y)\n                color_dist = color_column[members]\n                color_dist = color_dist[color_dist < len(self.colors)]\n                if len(color_dist) != len(members):\n                    self.Warning.missing_colors(self.attr_color.name)\n                bc = np.bincount(color_dist, minlength=len(self.colors))\n                color = qcolors[np.argmax(bc)]\n                ellipse = ColoredCircle(r / 2, color, np.max(bc) / len(members))\n                ellipse.setPos(x + (y % 2) * fx, y * fy)\n                ellipse.setToolTip(self._tooltip(qcolors, bc))\n                self.elements.addToGroup(ellipse)\n\n    def redraw_grid(self):\n        if self.grid is not None:\n            self.scene.removeItem(self.grid)\n        self.grid = QGraphicsItemGroup()\n        self.grid.setZValue(-200)\n        self.grid_cells = np.full((self.size_y, self.size_x), None)\n        for y in range(self.size_y):\n            for x in range(self.size_x - (y % 2) * self.hexagonal):\n                if self.hexagonal:\n                    cell = QGraphicsPathItem(_hexagon_path)\n                    cell.setPos(x + (y % 2) / 2, y * sqrt3_2)\n                else:\n                    cell = QGraphicsRectItem(x - 0.5, y - 0.5, 1, 1)\n                self.grid_cells[y, x] = cell\n                cell.setPen(self._grid_pen)\n                self.grid.addToGroup(cell)\n        self.scene.addItem(self.grid)\n\n    def get_member_indices(self, x, y):\n        i, j = self.cells[x, y]\n        return self.member_data[i:j]\n\n    def _recompute_som(self):\n        if self.cont_x is None:\n            return\n\n        som = SOM(\n            self.size_x, self.size_y,\n            hexagonal=self.hexagonal,\n            pca_init=self.initialization == 0,\n            random_seed=0 if self.initialization == 2 else None\n        )\n\n        class Optimizer(QObject):\n            update = Signal(float, np.ndarray, np.ndarray)\n            done = Signal(SOM)\n            stopped = Signal()\n            stop_optimization = False\n\n            def __init__(self, data, som):\n                super().__init__()\n                self.som = som\n                self.data = data\n\n            def callback(self, progress):\n                self.update.emit(\n                    progress,\n                    self.som.weights.copy(), self.som.ssum_weights.copy())\n                return not self.stop_optimization\n\n            def run(self):\n                try:\n                    self.som.fit(self.data, N_ITERATIONS,\n                                 callback=self.callback)\n                    # Report an exception, but still remove the thread\n                finally:\n                    self.done.emit(self.som)\n                    self.stopped.emit()\n\n        def thread_finished():\n            self._optimizer = None\n            self._optimizer_thread = None\n\n        self.progressBarInit()\n\n        self._optimizer = Optimizer(self.cont_x, som)\n        self._optimizer_thread = QThread()\n        self._optimizer_thread.setStackSize(5 * 2 ** 20)\n        self._optimizer.update.connect(self.__update)\n        self._optimizer.done.connect(self.__done)\n        self._optimizer.stopped.connect(self._optimizer_thread.quit)\n        self._optimizer.moveToThread(self._optimizer_thread)\n        self._optimizer_thread.started.connect(self._optimizer.run)\n        self._optimizer_thread.finished.connect(thread_finished)\n        self.stop_optimization = False\n        self._optimizer_thread.start()\n\n    @Slot(float, object, object)\n    def __update(self, _progress, weights, ssum_weights):\n        self.progressBarSet(_progress)\n        self._assign_instances(weights, ssum_weights)\n        self._redraw()\n\n    @Slot(object)\n    def __done(self, som):\n        self.enable_controls(True)\n        self.progressBarFinished()\n        self._assign_instances(som.weights, som.ssum_weights)\n        self._redraw()\n        # This is the first time we know what was selected (assuming that\n        # initialization is not set to random)\n        if self.__pending_selection is not None:\n            self.on_selection_change(self.__pending_selection)\n            self.__pending_selection = None\n        self.update_output()\n\n    def stop_optimization_and_wait(self):\n        if self._optimizer_thread is not None:\n            self.stop_optimization = True\n            self._optimizer.stop_optimization = True\n            self._optimizer_thread.quit()\n            self._optimizer_thread.wait()\n            self._optimizer_thread = None\n\n    def onDeleteWidget(self):\n        self.stop_optimization_and_wait()\n        self.clear()\n        super().onDeleteWidget()\n\n    def _assign_instances(self, weights, ssum_weights):\n        if self.cont_x is None:\n            return  # the widget is shutting down while signals still processed\n        assignments = SOM.winner_from_weights(\n            self.cont_x, weights, ssum_weights, self.hexagonal)\n        members = defaultdict(list)\n        for i, (x, y) in enumerate(assignments):\n            members[(x, y)].append(i)\n        members.pop(None, None)\n        self.cells = np.empty((self.size_x, self.size_y, 2), dtype=int)\n        self.member_data = np.empty(self.cont_x.shape[0], dtype=int)\n        index = 0\n        for x in range(self.size_x):\n            for y in range(self.size_y):\n                nmembers = len(members[(x, y)])\n                self.member_data[index:index + nmembers] = members[(x, y)]\n                self.cells[x, y] = [index, index + nmembers]\n                index += nmembers\n\n    def resizeEvent(self, event):\n        super().resizeEvent(event)\n        self.create_legend()  # re-wrap lines if necessary\n        self.rescale()\n\n    def rescale(self):\n        if self.legend:\n            leg_height = self.legend.boundingRect().height()\n            leg_extra = 1.5\n        else:\n            leg_height = 0\n            leg_extra = 1\n\n        vw, vh = self.view.width(), self.view.height() - leg_height\n        scale = min(vw / (self.size_x + 1),\n                    vh / ((self.size_y + leg_extra) * self._grid_factors[1]))\n        self.view.setTransform(QTransform.fromScale(scale, scale))\n        if self.hexagonal:\n            self.view.setSceneRect(\n                0, -1, self.size_x - 1,\n                (self.size_y + leg_extra) * sqrt3_2 + leg_height / scale)\n        else:\n            self.view.setSceneRect(\n                -0.25, -0.25, self.size_x - 0.5,\n                self.size_y - 0.5 + leg_height / scale)\n\n    def update_output(self):\n        if self.data is None:\n            self.Outputs.selected_data.send(None)\n            self.Outputs.annotated_data.send(None)\n            return\n\n        indices = np.zeros(len(self.data), dtype=int)\n        if self.selection is not None and np.any(self.selection):\n            for y in range(self.size_y):\n                for x in range(self.size_x):\n                    rows = self.get_member_indices(x, y)\n                    indices[rows] = self.selection[x, y]\n\n        if np.any(indices):\n            sel_data = create_groups_table(self.data, indices, False, \"Group\")\n            self.Outputs.selected_data.send(sel_data)\n        else:\n            self.Outputs.selected_data.send(None)\n\n        if np.max(indices) > 1:\n            annotated = create_groups_table(self.data, indices)\n        else:\n            annotated = create_annotated_table(\n                self.data, np.flatnonzero(indices))\n        self.Outputs.annotated_data.send(annotated)\n\n    def set_color_bins(self):\n        if self.attr_color is None:\n            self.thresholds = self.bin_labels = self.colors = None\n        elif self.attr_color.is_discrete:\n            self.thresholds = self.bin_labels = None\n            self.colors = self.attr_color.palette\n        else:\n            col = self.data.get_column_view(self.attr_color)[0].astype(float)\n            if self.attr_color.is_time:\n                binning = time_binnings(col, min_bins=4)[-1]\n            else:\n                binning = decimal_binnings(col, min_bins=4)[-1]\n            self.thresholds = binning.thresholds[1:-1]\n            self.bin_labels = (binning.labels[1:-1], binning.short_labels[1:-1])\n            palette = BinnedContinuousPalette.from_palette(\n                self.attr_color.palette, binning.thresholds)\n            self.colors = palette\n\n    def create_legend(self):\n        if self.legend is not None:\n            self.scene.removeItem(self.legend)\n            self.legend = None\n        if self.attr_color is None:\n            return\n\n        if self.attr_color.is_discrete:\n            names = self.attr_color.values\n        else:\n            names = self._bin_names()\n\n        items = []\n        size = 8\n        for name, color in zip(names, self.colors.qcolors):\n            item = QGraphicsItemGroup()\n            item.addToGroup(\n                CanvasRectangle(None, -size / 2, -size / 2, size, size,\n                                Qt.gray, color))\n            item.addToGroup(CanvasText(None, name, size, 0, Qt.AlignVCenter))\n            items.append(item)\n\n        self.legend = wrap_legend_items(\n            items, hspacing=20, vspacing=16 + size,\n            max_width=self.view.width() - 25)\n        self.legend.setFlags(self.legend.ItemIgnoresTransformations)\n        self.legend.setTransform(\n            QTransform.fromTranslate(-self.legend.boundingRect().width() / 2,\n                                     0))\n        self.scene.addItem(self.legend)\n        self.set_legend_pos()\n\n    def _bin_names(self):\n        labels, short_labels = self.bin_labels\n        return \\\n            [f\"< {labels[0]}\"] \\\n            + [f\"{x} - {y}\" for x, y in zip(labels, short_labels[1:])] \\\n            + [f\"≥ {labels[-1]}\"]\n\n    def set_legend_pos(self):\n        if self.legend is None:\n            return\n        self.legend.setPos(\n            self.size_x / 2,\n            (self.size_y + 0.2 + 0.3 * self.hexagonal) * self._grid_factors[1])\n\n    def send_report(self):\n        self.report_plot()\n        if self.attr_color:\n            self.report_caption(\n                f\"Self-organizing map colored by '{self.attr_color.name}'\")",
                                "ast_type_str": "ClassDef",
                                "start_lineno": 175,
                                "end_lineno": 895,
                                "parent": null
                            }
                        },
                        {
                            "file_name": "Orange/widgets/unsupervised/owsom.py",
                            "type": "class",
                            "name": "OWSOM",
                            "content": "class OWSOM(OWWidget):\n    name = \"Self-Organizing Map\"\n    description = \"Computation of self-organizing map.\"\n    icon = \"icons/SOM.svg\"\n    keywords = [\"SOM\"]\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    settingsHandler = DomainContextHandler()\n    auto_dimension = Setting(True)\n    size_x = Setting(10)\n    size_y = Setting(10)\n    hexagonal = Setting(1)\n    initialization = Setting(0)\n\n    attr_color = ContextSetting(None)\n    size_by_instances = Setting(True)\n    pie_charts = Setting(False)\n    selection = Setting(None, schema_only=True)\n\n    graph_name = \"view\"\n\n    _grid_pen = QPen(QBrush(QColor(224, 224, 224)), 2)\n    _grid_pen.setCosmetic(True)\n\n    OptControls = namedtuple(\n        \"OptControls\",\n        (\"shape\", \"auto_dim\", \"spin_x\", \"spin_y\", \"initialization\", \"start\")\n    )\n\n    class Warning(OWWidget.Warning):\n        ignoring_disc_variables = Msg(\"SOM ignores categorical variables.\")\n        missing_colors = \\\n            Msg(\"Some data instances have undefined value of '{}'.\")\n        missing_values = \\\n            Msg(\"{} data instance{} with undefined value(s) {} not shown.\")\n        single_attribute = Msg(\"Data contains a single numeric column.\")\n\n    class Error(OWWidget.Error):\n        no_numeric_variables = Msg(\"Data contains no numeric columns.\")\n        no_defined_rows = Msg(\"All rows contain at least one undefined value.\")\n\n    def __init__(self):\n        super().__init__()\n        self.__pending_selection = self.selection\n        self._optimizer = None\n        self._optimizer_thread = None\n        self.stop_optimization = False\n\n        self.data = self.cont_x = None\n        self.cells = self.member_data = None\n        self.selection = None\n        self.colors = self.thresholds = self.bin_labels = None\n\n        box = gui.vBox(self.controlArea, box=\"SOM\")\n        shape = gui.comboBox(\n            box, self, \"\", items=(\"Hexagonal grid\", \"Square grid\"))\n        shape.setCurrentIndex(1 - self.hexagonal)\n\n        box2 = gui.indentedBox(box, 10)\n        auto_dim = gui.checkBox(\n            box2, self, \"auto_dimension\", \"Set dimensions automatically\",\n            callback=self.on_auto_dimension_changed)\n        self.manual_box = box3 = gui.hBox(box2)\n        spinargs = dict(\n            value=\"\", widget=box3, master=self, minv=5, maxv=100, step=5,\n            alignment=Qt.AlignRight)\n        spin_x = gui.spin(**spinargs)\n        spin_x.setValue(self.size_x)\n        gui.widgetLabel(box3, \"×\")\n        spin_y = gui.spin(**spinargs)\n        spin_y.setValue(self.size_y)\n        gui.rubber(box3)\n        self.manual_box.setEnabled(not self.auto_dimension)\n\n        initialization = gui.comboBox(\n            box, self, \"initialization\",\n            items=(\"Initialize with PCA\", \"Random initialization\",\n                   \"Replicable random\"))\n\n        start = gui.button(\n            box, self, \"Restart\", callback=self.restart_som_pressed,\n            sizePolicy=(QSizePolicy.MinimumExpanding, QSizePolicy.Fixed))\n\n        self.opt_controls = self.OptControls(\n            shape, auto_dim, spin_x, spin_y, initialization, start)\n\n        box = gui.vBox(self.controlArea, \"Color\")\n        gui.comboBox(\n            box, self, \"attr_color\", searchable=True,\n            callback=self.on_attr_color_change,\n            model=DomainModel(placeholder=\"(Same color)\",\n                              valid_types=DomainModel.PRIMITIVE))\n        gui.checkBox(\n            box, self, \"pie_charts\", label=\"Show pie charts\",\n            callback=self.on_pie_chart_change)\n        gui.checkBox(\n            box, self, \"size_by_instances\", label=\"Size by number of instances\",\n            callback=self.on_attr_size_change)\n\n        gui.rubber(self.controlArea)\n\n        self.scene = QGraphicsScene(self)\n\n        self.view = SomView(self.scene)\n        self.view.setMinimumWidth(400)\n        self.view.setMinimumHeight(400)\n        self.view.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)\n        self.view.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)\n        self.view.setRenderHint(QPainter.Antialiasing)\n        self.view.selection_changed.connect(self.on_selection_change)\n        self.view.selection_moved.connect(self.on_selection_move)\n        self.view.selection_mark_changed.connect(self.on_selection_mark_change)\n        self.mainArea.layout().addWidget(self.view)\n\n        self.elements = None\n        self.grid = None\n        self.grid_cells = None\n        self.legend = None\n\n    @Inputs.data\n    def set_data(self, data):\n        def prepare_data():\n            if len(cont_attrs) < len(attrs):\n                self.Warning.ignoring_disc_variables()\n            if len(cont_attrs) == 1:\n                self.Warning.single_attribute()\n            x = Table.from_table(Domain(cont_attrs), data).X\n            if sp.issparse(x):\n                self.data = data\n                self.cont_x = x.tocsr()\n            else:\n                mask = np.all(np.isfinite(x), axis=1)\n                if not np.any(mask):\n                    self.Error.no_defined_rows()\n                else:\n                    if np.all(mask):\n                        self.data = data\n                        self.cont_x = x.copy()\n                    else:\n                        self.data = data[mask]\n                        self.cont_x = x[mask]\n                    self.cont_x -= np.min(self.cont_x, axis=0)[None, :]\n                    sums = np.sum(self.cont_x, axis=0)[None, :]\n                    sums[sums == 0] = 1\n                    self.cont_x /= sums\n\n        def set_warnings():\n            missing = len(data) - len(self.data)\n            if missing == 1:\n                self.Warning.missing_values(1, \"\", \"is\")\n            elif missing > 1:\n                self.Warning.missing_values(missing, \"s\", \"are\")\n\n        cont_x = self.cont_x.copy() if self.cont_x is not None else None\n        self.data = self.cont_x = None\n        self.closeContext()\n        self.clear_messages()\n\n        if data is not None:\n            attrs = data.domain.attributes\n            cont_attrs = [var for var in attrs if var.is_continuous]\n            if not cont_attrs:\n                self.Error.no_numeric_variables()\n            else:\n                prepare_data()\n\n        invalidated = cont_x is None or self.cont_x is None \\\n            or not array_equal(cont_x, self.cont_x)\n        if invalidated:\n            self.stop_optimization_and_wait()\n            self.clear()\n\n        if self.data is not None:\n            self.controls.attr_color.model().set_domain(data.domain)\n            self.attr_color = data.domain.class_var\n            set_warnings()\n\n        self.openContext(self.data)\n        self.set_color_bins()\n        self.create_legend()\n        if invalidated:\n            self.recompute_dimensions()\n            self.start_som()\n        else:\n            self._redraw()\n\n    def clear(self):\n        self.cells = self.member_data = None\n        self.attr_color = None\n        self.colors = self.thresholds = self.bin_labels = None\n        if self.elements is not None:\n            self.scene.removeItem(self.elements)\n            self.elements = None\n        self.clear_selection()\n        self.controls.attr_color.model().set_domain(None)\n\n    def recompute_dimensions(self):\n        if not self.auto_dimension or self.cont_x is None:\n            return\n        dim = max(5, int(np.ceil(np.sqrt(5 * np.sqrt(self.cont_x.shape[0])))))\n        self.opt_controls.spin_x.setValue(dim)\n        self.opt_controls.spin_y.setValue(dim)\n\n    def on_auto_dimension_changed(self):\n        self.manual_box.setEnabled(not self.auto_dimension)\n        if self.auto_dimension:\n            self.recompute_dimensions()\n        else:\n            spin_x = self.opt_controls.spin_x\n            spin_y = self.opt_controls.spin_y\n            dimx = int(5 * np.round(spin_x.value() / 5))\n            dimy = int(5 * np.round(spin_y.value() / 5))\n            spin_x.setValue(dimx)\n            spin_y.setValue(dimy)\n\n    def on_attr_color_change(self):\n        self.controls.pie_charts.setEnabled(self.attr_color is not None)\n        self.set_color_bins()\n        self.create_legend()\n        self.rescale()\n        self._redraw()\n\n    def on_attr_size_change(self):\n        self._redraw()\n\n    def on_pie_chart_change(self):\n        self._redraw()\n\n    def clear_selection(self):\n        self.selection = None\n        self.redraw_selection()\n\n    def on_selection_change(self, selection, action=SomView.SelectionSet):\n        if self.data is None:  # clicks on empty canvas\n            return\n        if self.selection is None:\n            self.selection = np.zeros(self.grid_cells.T.shape, dtype=np.int16)\n        if action == SomView.SelectionSet:\n            self.selection[:] = 0\n            self.selection[selection] = 1\n        elif action == SomView.SelectionAddToGroup:\n            self.selection[selection] = max(1, np.max(self.selection))\n        elif action == SomView.SelectionNewGroup:\n            self.selection[selection] = 1 + np.max(self.selection)\n        elif action & SomView.SelectionRemove:\n            self.selection[selection] = 0\n        self.redraw_selection()\n        self.update_output()\n\n    def on_selection_move(self, event: QKeyEvent):\n        if self.selection is None or not np.any(self.selection):\n            if event.key() in (Qt.Key_Right, Qt.Key_Down):\n                x = y = 0\n            else:\n                x = self.size_x - 1\n                y = self.size_y - 1\n        else:\n            x, y = np.nonzero(self.selection)\n            if len(x) > 1:\n                return\n            if event.key() == Qt.Key_Up and y > 0:\n                y -= 1\n            if event.key() == Qt.Key_Down and y < self.size_y - 1:\n                y += 1\n            if event.key() == Qt.Key_Left and x:\n                x -= 1\n            if event.key() == Qt.Key_Right and x < self.size_x - 1:\n                x += 1\n            x -= self.hexagonal and x == self.size_x - 1 and y % 2\n\n        if self.selection is not None and self.selection[x, y]:\n            return\n        selection = np.zeros(self.grid_cells.shape, dtype=bool)\n        selection[x, y] = True\n        self.on_selection_change(selection)\n\n    def on_selection_mark_change(self, marks):\n        self.redraw_selection(marks=marks)\n\n    def redraw_selection(self, marks=None):\n        if self.grid_cells is None:\n            return\n\n        sel_pen = QPen(QBrush(QColor(128, 128, 128)), 2)\n        sel_pen.setCosmetic(True)\n        mark_pen = QPen(QBrush(QColor(128, 128, 128)), 4)\n        mark_pen.setCosmetic(True)\n        pens = [self._grid_pen, sel_pen]\n\n        mark_brush = QBrush(QColor(224, 255, 255))\n        sels = self.selection is not None and np.max(self.selection)\n        palette = LimitedDiscretePalette(number_of_colors=sels + 1)\n        brushes = [QBrush(Qt.NoBrush)] + \\\n                  [QBrush(palette[i].lighter(165)) for i in range(sels)]\n\n        for y in range(self.size_y):\n            for x in range(self.size_x - (y % 2) * self.hexagonal):\n                cell = self.grid_cells[y, x]\n                marked = marks is not None and marks[x, y]\n                sel_group = self.selection is not None and self.selection[x, y]\n                if marked:\n                    cell.setBrush(mark_brush)\n                    cell.setPen(mark_pen)\n                else:\n                    cell.setBrush(brushes[sel_group])\n                    cell.setPen(pens[bool(sel_group)])\n                cell.setZValue(marked or sel_group)\n\n    def restart_som_pressed(self):\n        if self._optimizer_thread is not None:\n            self.stop_optimization = True\n            self._optimizer.stop_optimization = True\n        else:\n            self.start_som()\n\n    def start_som(self):\n        self.read_controls()\n        self.update_layout()\n        self.clear_selection()\n        if self.cont_x is not None:\n            self.enable_controls(False)\n            self._recompute_som()\n        else:\n            self.update_output()\n\n    def read_controls(self):\n        c = self.opt_controls\n        self.hexagonal = c.shape.currentIndex() == 0\n        self.size_x = c.spin_x.value()\n        self.size_y = c.spin_y.value()\n\n    def enable_controls(self, enable):\n        c = self.opt_controls\n        c.shape.setEnabled(enable)\n        c.auto_dim.setEnabled(enable)\n        c.start.setText(\"Start\" if enable else \"Stop\")\n\n    def update_layout(self):\n        self.set_legend_pos()\n        if self.elements:  # Prevent having redrawn grid but with old elements\n            self.scene.removeItem(self.elements)\n            self.elements = None\n        self.redraw_grid()\n        self.rescale()\n\n    def _redraw(self):\n        self.Warning.missing_colors.clear()\n        if self.elements:\n            self.scene.removeItem(self.elements)\n            self.elements = None\n        self.view.set_dimensions(self.size_x, self.size_y, self.hexagonal)\n\n        if self.cells is None:\n            return\n        sizes = self.cells[:, :, 1] - self.cells[:, :, 0]\n        sizes = sizes.astype(float)\n        if not self.size_by_instances:\n            sizes[sizes != 0] = 0.8\n        else:\n            sizes *= 0.8 / np.max(sizes)\n\n        self.elements = QGraphicsItemGroup()\n        self.scene.addItem(self.elements)\n        if self.attr_color is None:\n            self._draw_same_color(sizes)\n        elif self.pie_charts:\n            self._draw_pie_charts(sizes)\n        else:\n            self._draw_colored_circles(sizes)\n\n    @property\n    def _grid_factors(self):\n        return (0.5, sqrt3_2) if self.hexagonal else (0, 1)\n\n    def _draw_same_color(self, sizes):\n        fx, fy = self._grid_factors\n        color = QColor(64, 64, 64)\n        for y in range(self.size_y):\n            for x in range(self.size_x - self.hexagonal * (y % 2)):\n                r = sizes[x, y]\n                n = len(self.get_member_indices(x, y))\n                if not r:\n                    continue\n                ellipse = ColoredCircle(r / 2, color, 0)\n                ellipse.setPos(x + (y % 2) * fx, y * fy)\n                ellipse.setToolTip(f\"{n} instances\")\n                self.elements.addToGroup(ellipse)\n\n    def _get_color_column(self):\n        color_column = \\\n            self.data.get_column_view(self.attr_color)[0].astype(float,\n                                                                 copy=False)\n        if self.attr_color.is_discrete:\n            with np.errstate(invalid=\"ignore\"):\n                int_col = color_column.astype(int)\n            int_col[np.isnan(color_column)] = len(self.colors)\n        else:\n            int_col = np.zeros(len(color_column), dtype=int)\n            # The following line is unnecessary because rows with missing\n            # numeric data are excluded. Uncomment it if you change SOM to\n            # tolerate missing values.\n            # int_col[np.isnan(color_column)] = len(self.colors)\n            for i, thresh in enumerate(self.thresholds, start=1):\n                int_col[color_column >= thresh] = i\n        return int_col\n\n    def _tooltip(self, colors, distribution):\n        if self.attr_color.is_discrete:\n            values = self.attr_color.values\n        else:\n            values = self._bin_names()\n        tot = np.sum(distribution)\n        nbhp = \"\\N{NON-BREAKING HYPHEN}\"\n        return '<table style=\"white-space: nowrap\">' + \"\".join(f\"\"\"\n            <tr>\n                <td>\n                    <font color={color.name()}>■</font>\n                    <b>{escape(val).replace(\"-\", nbhp)}</b>:\n                </td>\n                <td>\n                    {n} ({n / tot * 100:.1f}&nbsp;%)\n                </td>\n            </tr>\n            \"\"\" for color, val, n in zip(colors, values, distribution) if n) \\\n            + \"</table>\"\n\n    def _draw_pie_charts(self, sizes):\n        fx, fy = self._grid_factors\n        color_column = self._get_color_column()\n        colors = self.colors.qcolors_w_nan\n        for y in range(self.size_y):\n            for x in range(self.size_x - self.hexagonal * (y % 2)):\n                r = sizes[x, y]\n                if not r:\n                    self.grid_cells[y, x].setToolTip(\"\")\n                    continue\n                members = self.get_member_indices(x, y)\n                color_dist = np.bincount(color_column[members],\n                                         minlength=len(colors))\n                rel_color_dist = color_dist.astype(float) / len(members)\n                pie = PieChart(rel_color_dist, r / 2, colors)\n                pie.setToolTip(self._tooltip(colors, color_dist))\n                self.elements.addToGroup(pie)\n                pie.setPos(x + (y % 2) * fx, y * fy)\n\n    def _draw_colored_circles(self, sizes):\n        fx, fy = self._grid_factors\n        color_column = self._get_color_column()\n        qcolors = self.colors.qcolors_w_nan\n        for y in range(self.size_y):\n            for x in range(self.size_x - self.hexagonal * (y % 2)):\n                r = sizes[x, y]\n                if not r:\n                    continue\n                members = self.get_member_indices(x, y)\n                color_dist = color_column[members]\n                color_dist = color_dist[color_dist < len(self.colors)]\n                if len(color_dist) != len(members):\n                    self.Warning.missing_colors(self.attr_color.name)\n                bc = np.bincount(color_dist, minlength=len(self.colors))\n                color = qcolors[np.argmax(bc)]\n                ellipse = ColoredCircle(r / 2, color, np.max(bc) / len(members))\n                ellipse.setPos(x + (y % 2) * fx, y * fy)\n                ellipse.setToolTip(self._tooltip(qcolors, bc))\n                self.elements.addToGroup(ellipse)\n\n    def redraw_grid(self):\n        if self.grid is not None:\n            self.scene.removeItem(self.grid)\n        self.grid = QGraphicsItemGroup()\n        self.grid.setZValue(-200)\n        self.grid_cells = np.full((self.size_y, self.size_x), None)\n        for y in range(self.size_y):\n            for x in range(self.size_x - (y % 2) * self.hexagonal):\n                if self.hexagonal:\n                    cell = QGraphicsPathItem(_hexagon_path)\n                    cell.setPos(x + (y % 2) / 2, y * sqrt3_2)\n                else:\n                    cell = QGraphicsRectItem(x - 0.5, y - 0.5, 1, 1)\n                self.grid_cells[y, x] = cell\n                cell.setPen(self._grid_pen)\n                self.grid.addToGroup(cell)\n        self.scene.addItem(self.grid)\n\n    def get_member_indices(self, x, y):\n        i, j = self.cells[x, y]\n        return self.member_data[i:j]\n\n    def _recompute_som(self):\n        if self.cont_x is None:\n            return\n\n        som = SOM(\n            self.size_x, self.size_y,\n            hexagonal=self.hexagonal,\n            pca_init=self.initialization == 0,\n            random_seed=0 if self.initialization == 2 else None\n        )\n\n        class Optimizer(QObject):\n            update = Signal(float, np.ndarray, np.ndarray)\n            done = Signal(SOM)\n            stopped = Signal()\n            stop_optimization = False\n\n            def __init__(self, data, som):\n                super().__init__()\n                self.som = som\n                self.data = data\n\n            def callback(self, progress):\n                self.update.emit(\n                    progress,\n                    self.som.weights.copy(), self.som.ssum_weights.copy())\n                return not self.stop_optimization\n\n            def run(self):\n                try:\n                    self.som.fit(self.data, N_ITERATIONS,\n                                 callback=self.callback)\n                    # Report an exception, but still remove the thread\n                finally:\n                    self.done.emit(self.som)\n                    self.stopped.emit()\n\n        def thread_finished():\n            self._optimizer = None\n            self._optimizer_thread = None\n\n        self.progressBarInit()\n\n        self._optimizer = Optimizer(self.cont_x, som)\n        self._optimizer_thread = QThread()\n        self._optimizer_thread.setStackSize(5 * 2 ** 20)\n        self._optimizer.update.connect(self.__update)\n        self._optimizer.done.connect(self.__done)\n        self._optimizer.stopped.connect(self._optimizer_thread.quit)\n        self._optimizer.moveToThread(self._optimizer_thread)\n        self._optimizer_thread.started.connect(self._optimizer.run)\n        self._optimizer_thread.finished.connect(thread_finished)\n        self.stop_optimization = False\n        self._optimizer_thread.start()\n\n    @Slot(float, object, object)\n    def __update(self, _progress, weights, ssum_weights):\n        self.progressBarSet(_progress)\n        self._assign_instances(weights, ssum_weights)\n        self._redraw()\n\n    @Slot(object)\n    def __done(self, som):\n        self.enable_controls(True)\n        self.progressBarFinished()\n        self._assign_instances(som.weights, som.ssum_weights)\n        self._redraw()\n        # This is the first time we know what was selected (assuming that\n        # initialization is not set to random)\n        if self.__pending_selection is not None:\n            self.on_selection_change(self.__pending_selection)\n            self.__pending_selection = None\n        self.update_output()\n\n    def stop_optimization_and_wait(self):\n        if self._optimizer_thread is not None:\n            self.stop_optimization = True\n            self._optimizer.stop_optimization = True\n            self._optimizer_thread.quit()\n            self._optimizer_thread.wait()\n            self._optimizer_thread = None\n\n    def onDeleteWidget(self):\n        self.stop_optimization_and_wait()\n        self.clear()\n        super().onDeleteWidget()\n\n    def _assign_instances(self, weights, ssum_weights):\n        if self.cont_x is None:\n            return  # the widget is shutting down while signals still processed\n        assignments = SOM.winner_from_weights(\n            self.cont_x, weights, ssum_weights, self.hexagonal)\n        members = defaultdict(list)\n        for i, (x, y) in enumerate(assignments):\n            members[(x, y)].append(i)\n        members.pop(None, None)\n        self.cells = np.empty((self.size_x, self.size_y, 2), dtype=int)\n        self.member_data = np.empty(self.cont_x.shape[0], dtype=int)\n        index = 0\n        for x in range(self.size_x):\n            for y in range(self.size_y):\n                nmembers = len(members[(x, y)])\n                self.member_data[index:index + nmembers] = members[(x, y)]\n                self.cells[x, y] = [index, index + nmembers]\n                index += nmembers\n\n    def resizeEvent(self, event):\n        super().resizeEvent(event)\n        self.create_legend()  # re-wrap lines if necessary\n        self.rescale()\n\n    def rescale(self):\n        if self.legend:\n            leg_height = self.legend.boundingRect().height()\n            leg_extra = 1.5\n        else:\n            leg_height = 0\n            leg_extra = 1\n\n        vw, vh = self.view.width(), self.view.height() - leg_height\n        scale = min(vw / (self.size_x + 1),\n                    vh / ((self.size_y + leg_extra) * self._grid_factors[1]))\n        self.view.setTransform(QTransform.fromScale(scale, scale))\n        if self.hexagonal:\n            self.view.setSceneRect(\n                0, -1, self.size_x - 1,\n                (self.size_y + leg_extra) * sqrt3_2 + leg_height / scale)\n        else:\n            self.view.setSceneRect(\n                -0.25, -0.25, self.size_x - 0.5,\n                self.size_y - 0.5 + leg_height / scale)\n\n    def update_output(self):\n        if self.data is None:\n            self.Outputs.selected_data.send(None)\n            self.Outputs.annotated_data.send(None)\n            return\n\n        indices = np.zeros(len(self.data), dtype=int)\n        if self.selection is not None and np.any(self.selection):\n            for y in range(self.size_y):\n                for x in range(self.size_x):\n                    rows = self.get_member_indices(x, y)\n                    indices[rows] = self.selection[x, y]\n\n        if np.any(indices):\n            sel_data = create_groups_table(self.data, indices, False, \"Group\")\n            self.Outputs.selected_data.send(sel_data)\n        else:\n            self.Outputs.selected_data.send(None)\n\n        if np.max(indices) > 1:\n            annotated = create_groups_table(self.data, indices)\n        else:\n            annotated = create_annotated_table(\n                self.data, np.flatnonzero(indices))\n        self.Outputs.annotated_data.send(annotated)\n\n    def set_color_bins(self):\n        if self.attr_color is None:\n            self.thresholds = self.bin_labels = self.colors = None\n        elif self.attr_color.is_discrete:\n            self.thresholds = self.bin_labels = None\n            self.colors = self.attr_color.palette\n        else:\n            col = self.data.get_column_view(self.attr_color)[0].astype(float)\n            if self.attr_color.is_time:\n                binning = time_binnings(col, min_bins=4)[-1]\n            else:\n                binning = decimal_binnings(col, min_bins=4)[-1]\n            self.thresholds = binning.thresholds[1:-1]\n            self.bin_labels = (binning.labels[1:-1], binning.short_labels[1:-1])\n            palette = BinnedContinuousPalette.from_palette(\n                self.attr_color.palette, binning.thresholds)\n            self.colors = palette\n\n    def create_legend(self):\n        if self.legend is not None:\n            self.scene.removeItem(self.legend)\n            self.legend = None\n        if self.attr_color is None:\n            return\n\n        if self.attr_color.is_discrete:\n            names = self.attr_color.values\n        else:\n            names = self._bin_names()\n\n        items = []\n        size = 8\n        for name, color in zip(names, self.colors.qcolors):\n            item = QGraphicsItemGroup()\n            item.addToGroup(\n                CanvasRectangle(None, -size / 2, -size / 2, size, size,\n                                Qt.gray, color))\n            item.addToGroup(CanvasText(None, name, size, 0, Qt.AlignVCenter))\n            items.append(item)\n\n        self.legend = wrap_legend_items(\n            items, hspacing=20, vspacing=16 + size,\n            max_width=self.view.width() - 25)\n        self.legend.setFlags(self.legend.ItemIgnoresTransformations)\n        self.legend.setTransform(\n            QTransform.fromTranslate(-self.legend.boundingRect().width() / 2,\n                                     0))\n        self.scene.addItem(self.legend)\n        self.set_legend_pos()\n\n    def _bin_names(self):\n        labels, short_labels = self.bin_labels\n        return \\\n            [f\"< {labels[0]}\"] \\\n            + [f\"{x} - {y}\" for x, y in zip(labels, short_labels[1:])] \\\n            + [f\"≥ {labels[-1]}\"]\n\n    def set_legend_pos(self):\n        if self.legend is None:\n            return\n        self.legend.setPos(\n            self.size_x / 2,\n            (self.size_y + 0.2 + 0.3 * self.hexagonal) * self._grid_factors[1])\n\n    def send_report(self):\n        self.report_plot()\n        if self.attr_color:\n            self.report_caption(\n                f\"Self-organizing map colored by '{self.attr_color.name}'\")",
                            "ast_type_str": "ClassDef",
                            "start_lineno": 175,
                            "end_lineno": 895,
                            "parent": null
                        }
                    ],
                    "added_entities": [],
                    "deleted_entities": []
                },
                {
                    "descriptor": {
                        "old_range": {
                            "start": 353,
                            "length": 11
                        },
                        "new_range": {
                            "start": 358,
                            "length": 13
                        },
                        "section": "class OWSOM(OWWidget):"
                    },
                    "line_group": {
                        "all_lines": [
                            {
                                "content": "        self.openContext(self.data)",
                                "type": "context"
                            },
                            {
                                "content": "        self.set_color_bins()",
                                "type": "context"
                            },
                            {
                                "content": "        self.create_legend()",
                                "type": "context"
                            },
                            {
                                "content": "        self.recompute_dimensions()",
                                "type": "deleted"
                            },
                            {
                                "content": "        self.start_som()",
                                "type": "deleted"
                            },
                            {
                                "content": "        if invalidated:",
                                "type": "added"
                            },
                            {
                                "content": "            self.recompute_dimensions()",
                                "type": "added"
                            },
                            {
                                "content": "            self.start_som()",
                                "type": "added"
                            },
                            {
                                "content": "        else:",
                                "type": "added"
                            },
                            {
                                "content": "            self._redraw()",
                                "type": "added"
                            },
                            {
                                "content": "",
                                "type": "context"
                            },
                            {
                                "content": "    def clear(self):",
                                "type": "context"
                            },
                            {
                                "content": "        self.data = self.cont_x = None",
                                "type": "deleted"
                            },
                            {
                                "content": "        self.cells = self.member_data = None",
                                "type": "context"
                            },
                            {
                                "content": "        self.attr_color = None",
                                "type": "context"
                            },
                            {
                                "content": "        self.colors = self.thresholds = self.bin_labels = None",
                                "type": "context"
                            }
                        ]
                    },
                    "modified_entities": [
                        {
                            "file_name": "Orange/widgets/unsupervised/owsom.py",
                            "type": "method",
                            "name": "OWSOM.set_data",
                            "content": "class OWSOM(OWWidget):\n    name = \"Self-Organizing Map\"\n    description = \"Computation of self-organizing map.\"\n    icon = \"icons/SOM.svg\"\n    keywords = [\"SOM\"]\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    settingsHandler = DomainContextHandler()\n    auto_dimension = Setting(True)\n    size_x = Setting(10)\n    size_y = Setting(10)\n    hexagonal = Setting(1)\n    initialization = Setting(0)\n\n    attr_color = ContextSetting(None)\n    size_by_instances = Setting(True)\n    pie_charts = Setting(False)\n    selection = Setting(None, schema_only=True)\n\n    graph_name = \"view\"\n\n    _grid_pen = QPen(QBrush(QColor(224, 224, 224)), 2)\n    _grid_pen.setCosmetic(True)\n\n    OptControls = namedtuple(\n        \"OptControls\",\n        (\"shape\", \"auto_dim\", \"spin_x\", \"spin_y\", \"initialization\", \"start\")\n    )\n\n    class Warning(OWWidget.Warning):\n        ignoring_disc_variables = Msg(\"SOM ignores categorical variables.\")\n        missing_colors = \\\n            Msg(\"Some data instances have undefined value of '{}'.\")\n        missing_values = \\\n            Msg(\"{} data instance{} with undefined value(s) {} not shown.\")\n        single_attribute = Msg(\"Data contains a single numeric column.\")\n\n    class Error(OWWidget.Error):\n        no_numeric_variables = Msg(\"Data contains no numeric columns.\")\n        no_defined_rows = Msg(\"All rows contain at least one undefined value.\")\n\n    def __init__(self):\n        super().__init__()\n        self.__pending_selection = self.selection\n        self._optimizer = None\n        self._optimizer_thread = None\n        self.stop_optimization = False\n\n        self.data = self.cont_x = None\n        self.cells = self.member_data = None\n        self.selection = None\n        self.colors = self.thresholds = self.bin_labels = None\n\n        box = gui.vBox(self.controlArea, box=\"SOM\")\n        shape = gui.comboBox(\n            box, self, \"\", items=(\"Hexagonal grid\", \"Square grid\"))\n        shape.setCurrentIndex(1 - self.hexagonal)\n\n        box2 = gui.indentedBox(box, 10)\n        auto_dim = gui.checkBox(\n            box2, self, \"auto_dimension\", \"Set dimensions automatically\",\n            callback=self.on_auto_dimension_changed)\n        self.manual_box = box3 = gui.hBox(box2)\n        spinargs = dict(\n            value=\"\", widget=box3, master=self, minv=5, maxv=100, step=5,\n            alignment=Qt.AlignRight)\n        spin_x = gui.spin(**spinargs)\n        spin_x.setValue(self.size_x)\n        gui.widgetLabel(box3, \"×\")\n        spin_y = gui.spin(**spinargs)\n        spin_y.setValue(self.size_y)\n        gui.rubber(box3)\n        self.manual_box.setEnabled(not self.auto_dimension)\n\n        initialization = gui.comboBox(\n            box, self, \"initialization\",\n            items=(\"Initialize with PCA\", \"Random initialization\",\n                   \"Replicable random\"))\n\n        start = gui.button(\n            box, self, \"Restart\", callback=self.restart_som_pressed,\n            sizePolicy=(QSizePolicy.MinimumExpanding, QSizePolicy.Fixed))\n\n        self.opt_controls = self.OptControls(\n            shape, auto_dim, spin_x, spin_y, initialization, start)\n\n        box = gui.vBox(self.controlArea, \"Color\")\n        gui.comboBox(\n            box, self, \"attr_color\", searchable=True,\n            callback=self.on_attr_color_change,\n            model=DomainModel(placeholder=\"(Same color)\",\n                              valid_types=DomainModel.PRIMITIVE))\n        gui.checkBox(\n            box, self, \"pie_charts\", label=\"Show pie charts\",\n            callback=self.on_pie_chart_change)\n        gui.checkBox(\n            box, self, \"size_by_instances\", label=\"Size by number of instances\",\n            callback=self.on_attr_size_change)\n\n        gui.rubber(self.controlArea)\n\n        self.scene = QGraphicsScene(self)\n\n        self.view = SomView(self.scene)\n        self.view.setMinimumWidth(400)\n        self.view.setMinimumHeight(400)\n        self.view.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)\n        self.view.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)\n        self.view.setRenderHint(QPainter.Antialiasing)\n        self.view.selection_changed.connect(self.on_selection_change)\n        self.view.selection_moved.connect(self.on_selection_move)\n        self.view.selection_mark_changed.connect(self.on_selection_mark_change)\n        self.mainArea.layout().addWidget(self.view)\n\n        self.elements = None\n        self.grid = None\n        self.grid_cells = None\n        self.legend = None\n\n    @Inputs.data\n    def set_data(self, data):\n        def prepare_data():\n            if len(cont_attrs) < len(attrs):\n                self.Warning.ignoring_disc_variables()\n            if len(cont_attrs) == 1:\n                self.Warning.single_attribute()\n            x = Table.from_table(Domain(cont_attrs), data).X\n            if sp.issparse(x):\n                self.data = data\n                self.cont_x = x.tocsr()\n            else:\n                mask = np.all(np.isfinite(x), axis=1)\n                if not np.any(mask):\n                    self.Error.no_defined_rows()\n                else:\n                    if np.all(mask):\n                        self.data = data\n                        self.cont_x = x.copy()\n                    else:\n                        self.data = data[mask]\n                        self.cont_x = x[mask]\n                    self.cont_x -= np.min(self.cont_x, axis=0)[None, :]\n                    sums = np.sum(self.cont_x, axis=0)[None, :]\n                    sums[sums == 0] = 1\n                    self.cont_x /= sums\n\n        def set_warnings():\n            missing = len(data) - len(self.data)\n            if missing == 1:\n                self.Warning.missing_values(1, \"\", \"is\")\n            elif missing > 1:\n                self.Warning.missing_values(missing, \"s\", \"are\")\n\n        self.stop_optimization_and_wait()\n\n        self.closeContext()\n        self.clear()\n        self.Error.clear()\n        self.Warning.clear()\n\n        if data is not None:\n            attrs = data.domain.attributes\n            cont_attrs = [var for var in attrs if var.is_continuous]\n            if not cont_attrs:\n                self.Error.no_numeric_variables()\n            else:\n                prepare_data()\n\n        if self.data is not None:\n            self.controls.attr_color.model().set_domain(data.domain)\n            self.attr_color = data.domain.class_var\n            set_warnings()\n\n        self.openContext(self.data)\n        self.set_color_bins()\n        self.create_legend()\n        self.recompute_dimensions()\n        self.start_som()\n\n    def clear(self):\n        self.data = self.cont_x = None\n        self.cells = self.member_data = None\n        self.attr_color = None\n        self.colors = self.thresholds = self.bin_labels = None\n        if self.elements is not None:\n            self.scene.removeItem(self.elements)\n            self.elements = None\n        self.clear_selection()\n        self.controls.attr_color.model().set_domain(None)\n        self.Warning.clear()\n        self.Error.clear()\n\n    def recompute_dimensions(self):\n        if not self.auto_dimension or self.cont_x is None:\n            return\n        dim = max(5, int(np.ceil(np.sqrt(5 * np.sqrt(self.cont_x.shape[0])))))\n        self.opt_controls.spin_x.setValue(dim)\n        self.opt_controls.spin_y.setValue(dim)\n\n    def on_auto_dimension_changed(self):\n        self.manual_box.setEnabled(not self.auto_dimension)\n        if self.auto_dimension:\n            self.recompute_dimensions()\n        else:\n            spin_x = self.opt_controls.spin_x\n            spin_y = self.opt_controls.spin_y\n            dimx = int(5 * np.round(spin_x.value() / 5))\n            dimy = int(5 * np.round(spin_y.value() / 5))\n            spin_x.setValue(dimx)\n            spin_y.setValue(dimy)\n\n    def on_attr_color_change(self):\n        self.controls.pie_charts.setEnabled(self.attr_color is not None)\n        self.set_color_bins()\n        self.create_legend()\n        self.rescale()\n        self._redraw()\n\n    def on_attr_size_change(self):\n        self._redraw()\n\n    def on_pie_chart_change(self):\n        self._redraw()\n\n    def clear_selection(self):\n        self.selection = None\n        self.redraw_selection()\n\n    def on_selection_change(self, selection, action=SomView.SelectionSet):\n        if self.data is None:  # clicks on empty canvas\n            return\n        if self.selection is None:\n            self.selection = np.zeros(self.grid_cells.T.shape, dtype=np.int16)\n        if action == SomView.SelectionSet:\n            self.selection[:] = 0\n            self.selection[selection] = 1\n        elif action == SomView.SelectionAddToGroup:\n            self.selection[selection] = max(1, np.max(self.selection))\n        elif action == SomView.SelectionNewGroup:\n            self.selection[selection] = 1 + np.max(self.selection)\n        elif action & SomView.SelectionRemove:\n            self.selection[selection] = 0\n        self.redraw_selection()\n        self.update_output()\n\n    def on_selection_move(self, event: QKeyEvent):\n        if self.selection is None or not np.any(self.selection):\n            if event.key() in (Qt.Key_Right, Qt.Key_Down):\n                x = y = 0\n            else:\n                x = self.size_x - 1\n                y = self.size_y - 1\n        else:\n            x, y = np.nonzero(self.selection)\n            if len(x) > 1:\n                return\n            if event.key() == Qt.Key_Up and y > 0:\n                y -= 1\n            if event.key() == Qt.Key_Down and y < self.size_y - 1:\n                y += 1\n            if event.key() == Qt.Key_Left and x:\n                x -= 1\n            if event.key() == Qt.Key_Right and x < self.size_x - 1:\n                x += 1\n            x -= self.hexagonal and x == self.size_x - 1 and y % 2\n\n        if self.selection is not None and self.selection[x, y]:\n            return\n        selection = np.zeros(self.grid_cells.shape, dtype=bool)\n        selection[x, y] = True\n        self.on_selection_change(selection)\n\n    def on_selection_mark_change(self, marks):\n        self.redraw_selection(marks=marks)\n\n    def redraw_selection(self, marks=None):\n        if self.grid_cells is None:\n            return\n\n        sel_pen = QPen(QBrush(QColor(128, 128, 128)), 2)\n        sel_pen.setCosmetic(True)\n        mark_pen = QPen(QBrush(QColor(128, 128, 128)), 4)\n        mark_pen.setCosmetic(True)\n        pens = [self._grid_pen, sel_pen]\n\n        mark_brush = QBrush(QColor(224, 255, 255))\n        sels = self.selection is not None and np.max(self.selection)\n        palette = LimitedDiscretePalette(number_of_colors=sels + 1)\n        brushes = [QBrush(Qt.NoBrush)] + \\\n                  [QBrush(palette[i].lighter(165)) for i in range(sels)]\n\n        for y in range(self.size_y):\n            for x in range(self.size_x - (y % 2) * self.hexagonal):\n                cell = self.grid_cells[y, x]\n                marked = marks is not None and marks[x, y]\n                sel_group = self.selection is not None and self.selection[x, y]\n                if marked:\n                    cell.setBrush(mark_brush)\n                    cell.setPen(mark_pen)\n                else:\n                    cell.setBrush(brushes[sel_group])\n                    cell.setPen(pens[bool(sel_group)])\n                cell.setZValue(marked or sel_group)\n\n    def restart_som_pressed(self):\n        if self._optimizer_thread is not None:\n            self.stop_optimization = True\n            self._optimizer.stop_optimization = True\n        else:\n            self.start_som()\n\n    def start_som(self):\n        self.read_controls()\n        self.update_layout()\n        self.clear_selection()\n        if self.cont_x is not None:\n            self.enable_controls(False)\n            self._recompute_som()\n        else:\n            self.update_output()\n\n    def read_controls(self):\n        c = self.opt_controls\n        self.hexagonal = c.shape.currentIndex() == 0\n        self.size_x = c.spin_x.value()\n        self.size_y = c.spin_y.value()\n\n    def enable_controls(self, enable):\n        c = self.opt_controls\n        c.shape.setEnabled(enable)\n        c.auto_dim.setEnabled(enable)\n        c.start.setText(\"Start\" if enable else \"Stop\")\n\n    def update_layout(self):\n        self.set_legend_pos()\n        if self.elements:  # Prevent having redrawn grid but with old elements\n            self.scene.removeItem(self.elements)\n            self.elements = None\n        self.redraw_grid()\n        self.rescale()\n\n    def _redraw(self):\n        self.Warning.missing_colors.clear()\n        if self.elements:\n            self.scene.removeItem(self.elements)\n            self.elements = None\n        self.view.set_dimensions(self.size_x, self.size_y, self.hexagonal)\n\n        if self.cells is None:\n            return\n        sizes = self.cells[:, :, 1] - self.cells[:, :, 0]\n        sizes = sizes.astype(float)\n        if not self.size_by_instances:\n            sizes[sizes != 0] = 0.8\n        else:\n            sizes *= 0.8 / np.max(sizes)\n\n        self.elements = QGraphicsItemGroup()\n        self.scene.addItem(self.elements)\n        if self.attr_color is None:\n            self._draw_same_color(sizes)\n        elif self.pie_charts:\n            self._draw_pie_charts(sizes)\n        else:\n            self._draw_colored_circles(sizes)\n\n    @property\n    def _grid_factors(self):\n        return (0.5, sqrt3_2) if self.hexagonal else (0, 1)\n\n    def _draw_same_color(self, sizes):\n        fx, fy = self._grid_factors\n        color = QColor(64, 64, 64)\n        for y in range(self.size_y):\n            for x in range(self.size_x - self.hexagonal * (y % 2)):\n                r = sizes[x, y]\n                n = len(self.get_member_indices(x, y))\n                if not r:\n                    continue\n                ellipse = ColoredCircle(r / 2, color, 0)\n                ellipse.setPos(x + (y % 2) * fx, y * fy)\n                ellipse.setToolTip(f\"{n} instances\")\n                self.elements.addToGroup(ellipse)\n\n    def _get_color_column(self):\n        color_column = \\\n            self.data.get_column_view(self.attr_color)[0].astype(float,\n                                                                 copy=False)\n        if self.attr_color.is_discrete:\n            with np.errstate(invalid=\"ignore\"):\n                int_col = color_column.astype(int)\n            int_col[np.isnan(color_column)] = len(self.colors)\n        else:\n            int_col = np.zeros(len(color_column), dtype=int)\n            # The following line is unnecessary because rows with missing\n            # numeric data are excluded. Uncomment it if you change SOM to\n            # tolerate missing values.\n            # int_col[np.isnan(color_column)] = len(self.colors)\n            for i, thresh in enumerate(self.thresholds, start=1):\n                int_col[color_column >= thresh] = i\n        return int_col\n\n    def _tooltip(self, colors, distribution):\n        if self.attr_color.is_discrete:\n            values = self.attr_color.values\n        else:\n            values = self._bin_names()\n        tot = np.sum(distribution)\n        nbhp = \"\\N{NON-BREAKING HYPHEN}\"\n        return '<table style=\"white-space: nowrap\">' + \"\".join(f\"\"\"\n            <tr>\n                <td>\n                    <font color={color.name()}>■</font>\n                    <b>{escape(val).replace(\"-\", nbhp)}</b>:\n                </td>\n                <td>\n                    {n} ({n / tot * 100:.1f}&nbsp;%)\n                </td>\n            </tr>\n            \"\"\" for color, val, n in zip(colors, values, distribution) if n) \\\n            + \"</table>\"\n\n    def _draw_pie_charts(self, sizes):\n        fx, fy = self._grid_factors\n        color_column = self._get_color_column()\n        colors = self.colors.qcolors_w_nan\n        for y in range(self.size_y):\n            for x in range(self.size_x - self.hexagonal * (y % 2)):\n                r = sizes[x, y]\n                if not r:\n                    self.grid_cells[y, x].setToolTip(\"\")\n                    continue\n                members = self.get_member_indices(x, y)\n                color_dist = np.bincount(color_column[members],\n                                         minlength=len(colors))\n                rel_color_dist = color_dist.astype(float) / len(members)\n                pie = PieChart(rel_color_dist, r / 2, colors)\n                pie.setToolTip(self._tooltip(colors, color_dist))\n                self.elements.addToGroup(pie)\n                pie.setPos(x + (y % 2) * fx, y * fy)\n\n    def _draw_colored_circles(self, sizes):\n        fx, fy = self._grid_factors\n        color_column = self._get_color_column()\n        qcolors = self.colors.qcolors_w_nan\n        for y in range(self.size_y):\n            for x in range(self.size_x - self.hexagonal * (y % 2)):\n                r = sizes[x, y]\n                if not r:\n                    continue\n                members = self.get_member_indices(x, y)\n                color_dist = color_column[members]\n                color_dist = color_dist[color_dist < len(self.colors)]\n                if len(color_dist) != len(members):\n                    self.Warning.missing_colors(self.attr_color.name)\n                bc = np.bincount(color_dist, minlength=len(self.colors))\n                color = qcolors[np.argmax(bc)]\n                ellipse = ColoredCircle(r / 2, color, np.max(bc) / len(members))\n                ellipse.setPos(x + (y % 2) * fx, y * fy)\n                ellipse.setToolTip(self._tooltip(qcolors, bc))\n                self.elements.addToGroup(ellipse)\n\n    def redraw_grid(self):\n        if self.grid is not None:\n            self.scene.removeItem(self.grid)\n        self.grid = QGraphicsItemGroup()\n        self.grid.setZValue(-200)\n        self.grid_cells = np.full((self.size_y, self.size_x), None)\n        for y in range(self.size_y):\n            for x in range(self.size_x - (y % 2) * self.hexagonal):\n                if self.hexagonal:\n                    cell = QGraphicsPathItem(_hexagon_path)\n                    cell.setPos(x + (y % 2) / 2, y * sqrt3_2)\n                else:\n                    cell = QGraphicsRectItem(x - 0.5, y - 0.5, 1, 1)\n                self.grid_cells[y, x] = cell\n                cell.setPen(self._grid_pen)\n                self.grid.addToGroup(cell)\n        self.scene.addItem(self.grid)\n\n    def get_member_indices(self, x, y):\n        i, j = self.cells[x, y]\n        return self.member_data[i:j]\n\n    def _recompute_som(self):\n        if self.cont_x is None:\n            return\n\n        som = SOM(\n            self.size_x, self.size_y,\n            hexagonal=self.hexagonal,\n            pca_init=self.initialization == 0,\n            random_seed=0 if self.initialization == 2 else None\n        )\n\n        class Optimizer(QObject):\n            update = Signal(float, np.ndarray, np.ndarray)\n            done = Signal(SOM)\n            stopped = Signal()\n            stop_optimization = False\n\n            def __init__(self, data, som):\n                super().__init__()\n                self.som = som\n                self.data = data\n\n            def callback(self, progress):\n                self.update.emit(\n                    progress,\n                    self.som.weights.copy(), self.som.ssum_weights.copy())\n                return not self.stop_optimization\n\n            def run(self):\n                try:\n                    self.som.fit(self.data, N_ITERATIONS,\n                                 callback=self.callback)\n                    # Report an exception, but still remove the thread\n                finally:\n                    self.done.emit(self.som)\n                    self.stopped.emit()\n\n        def thread_finished():\n            self._optimizer = None\n            self._optimizer_thread = None\n\n        self.progressBarInit()\n\n        self._optimizer = Optimizer(self.cont_x, som)\n        self._optimizer_thread = QThread()\n        self._optimizer_thread.setStackSize(5 * 2 ** 20)\n        self._optimizer.update.connect(self.__update)\n        self._optimizer.done.connect(self.__done)\n        self._optimizer.stopped.connect(self._optimizer_thread.quit)\n        self._optimizer.moveToThread(self._optimizer_thread)\n        self._optimizer_thread.started.connect(self._optimizer.run)\n        self._optimizer_thread.finished.connect(thread_finished)\n        self.stop_optimization = False\n        self._optimizer_thread.start()\n\n    @Slot(float, object, object)\n    def __update(self, _progress, weights, ssum_weights):\n        self.progressBarSet(_progress)\n        self._assign_instances(weights, ssum_weights)\n        self._redraw()\n\n    @Slot(object)\n    def __done(self, som):\n        self.enable_controls(True)\n        self.progressBarFinished()\n        self._assign_instances(som.weights, som.ssum_weights)\n        self._redraw()\n        # This is the first time we know what was selected (assuming that\n        # initialization is not set to random)\n        if self.__pending_selection is not None:\n            self.on_selection_change(self.__pending_selection)\n            self.__pending_selection = None\n        self.update_output()\n\n    def stop_optimization_and_wait(self):\n        if self._optimizer_thread is not None:\n            self.stop_optimization = True\n            self._optimizer.stop_optimization = True\n            self._optimizer_thread.quit()\n            self._optimizer_thread.wait()\n            self._optimizer_thread = None\n\n    def onDeleteWidget(self):\n        self.stop_optimization_and_wait()\n        self.clear()\n        super().onDeleteWidget()\n\n    def _assign_instances(self, weights, ssum_weights):\n        if self.cont_x is None:\n            return  # the widget is shutting down while signals still processed\n        assignments = SOM.winner_from_weights(\n            self.cont_x, weights, ssum_weights, self.hexagonal)\n        members = defaultdict(list)\n        for i, (x, y) in enumerate(assignments):\n            members[(x, y)].append(i)\n        members.pop(None, None)\n        self.cells = np.empty((self.size_x, self.size_y, 2), dtype=int)\n        self.member_data = np.empty(self.cont_x.shape[0], dtype=int)\n        index = 0\n        for x in range(self.size_x):\n            for y in range(self.size_y):\n                nmembers = len(members[(x, y)])\n                self.member_data[index:index + nmembers] = members[(x, y)]\n                self.cells[x, y] = [index, index + nmembers]\n                index += nmembers\n\n    def resizeEvent(self, event):\n        super().resizeEvent(event)\n        self.create_legend()  # re-wrap lines if necessary\n        self.rescale()\n\n    def rescale(self):\n        if self.legend:\n            leg_height = self.legend.boundingRect().height()\n            leg_extra = 1.5\n        else:\n            leg_height = 0\n            leg_extra = 1\n\n        vw, vh = self.view.width(), self.view.height() - leg_height\n        scale = min(vw / (self.size_x + 1),\n                    vh / ((self.size_y + leg_extra) * self._grid_factors[1]))\n        self.view.setTransform(QTransform.fromScale(scale, scale))\n        if self.hexagonal:\n            self.view.setSceneRect(\n                0, -1, self.size_x - 1,\n                (self.size_y + leg_extra) * sqrt3_2 + leg_height / scale)\n        else:\n            self.view.setSceneRect(\n                -0.25, -0.25, self.size_x - 0.5,\n                self.size_y - 0.5 + leg_height / scale)\n\n    def update_output(self):\n        if self.data is None:\n            self.Outputs.selected_data.send(None)\n            self.Outputs.annotated_data.send(None)\n            return\n\n        indices = np.zeros(len(self.data), dtype=int)\n        if self.selection is not None and np.any(self.selection):\n            for y in range(self.size_y):\n                for x in range(self.size_x):\n                    rows = self.get_member_indices(x, y)\n                    indices[rows] = self.selection[x, y]\n\n        if np.any(indices):\n            sel_data = create_groups_table(self.data, indices, False, \"Group\")\n            self.Outputs.selected_data.send(sel_data)\n        else:\n            self.Outputs.selected_data.send(None)\n\n        if np.max(indices) > 1:\n            annotated = create_groups_table(self.data, indices)\n        else:\n            annotated = create_annotated_table(\n                self.data, np.flatnonzero(indices))\n        self.Outputs.annotated_data.send(annotated)\n\n    def set_color_bins(self):\n        if self.attr_color is None:\n            self.thresholds = self.bin_labels = self.colors = None\n        elif self.attr_color.is_discrete:\n            self.thresholds = self.bin_labels = None\n            self.colors = self.attr_color.palette\n        else:\n            col = self.data.get_column_view(self.attr_color)[0].astype(float)\n            if self.attr_color.is_time:\n                binning = time_binnings(col, min_bins=4)[-1]\n            else:\n                binning = decimal_binnings(col, min_bins=4)[-1]\n            self.thresholds = binning.thresholds[1:-1]\n            self.bin_labels = (binning.labels[1:-1], binning.short_labels[1:-1])\n            palette = BinnedContinuousPalette.from_palette(\n                self.attr_color.palette, binning.thresholds)\n            self.colors = palette\n\n    def create_legend(self):\n        if self.legend is not None:\n            self.scene.removeItem(self.legend)\n            self.legend = None\n        if self.attr_color is None:\n            return\n\n        if self.attr_color.is_discrete:\n            names = self.attr_color.values\n        else:\n            names = self._bin_names()\n\n        items = []\n        size = 8\n        for name, color in zip(names, self.colors.qcolors):\n            item = QGraphicsItemGroup()\n            item.addToGroup(\n                CanvasRectangle(None, -size / 2, -size / 2, size, size,\n                                Qt.gray, color))\n            item.addToGroup(CanvasText(None, name, size, 0, Qt.AlignVCenter))\n            items.append(item)\n\n        self.legend = wrap_legend_items(\n            items, hspacing=20, vspacing=16 + size,\n            max_width=self.view.width() - 25)\n        self.legend.setFlags(self.legend.ItemIgnoresTransformations)\n        self.legend.setTransform(\n            QTransform.fromTranslate(-self.legend.boundingRect().width() / 2,\n                                     0))\n        self.scene.addItem(self.legend)\n        self.set_legend_pos()\n\n    def _bin_names(self):\n        labels, short_labels = self.bin_labels\n        return \\\n            [f\"< {labels[0]}\"] \\\n            + [f\"{x} - {y}\" for x, y in zip(labels, short_labels[1:])] \\\n            + [f\"≥ {labels[-1]}\"]\n\n    def set_legend_pos(self):\n        if self.legend is None:\n            return\n        self.legend.setPos(\n            self.size_x / 2,\n            (self.size_y + 0.2 + 0.3 * self.hexagonal) * self._grid_factors[1])\n\n    def send_report(self):\n        self.report_plot()\n        if self.attr_color:\n            self.report_caption(\n                f\"Self-organizing map colored by '{self.attr_color.name}'\")",
                            "ast_type_str": "ClassDef",
                            "start_lineno": 300,
                            "end_lineno": 357,
                            "parent": {
                                "file_name": "Orange/widgets/unsupervised/owsom.py",
                                "type": "class",
                                "name": "OWSOM",
                                "content": "class OWSOM(OWWidget):\n    name = \"Self-Organizing Map\"\n    description = \"Computation of self-organizing map.\"\n    icon = \"icons/SOM.svg\"\n    keywords = [\"SOM\"]\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    settingsHandler = DomainContextHandler()\n    auto_dimension = Setting(True)\n    size_x = Setting(10)\n    size_y = Setting(10)\n    hexagonal = Setting(1)\n    initialization = Setting(0)\n\n    attr_color = ContextSetting(None)\n    size_by_instances = Setting(True)\n    pie_charts = Setting(False)\n    selection = Setting(None, schema_only=True)\n\n    graph_name = \"view\"\n\n    _grid_pen = QPen(QBrush(QColor(224, 224, 224)), 2)\n    _grid_pen.setCosmetic(True)\n\n    OptControls = namedtuple(\n        \"OptControls\",\n        (\"shape\", \"auto_dim\", \"spin_x\", \"spin_y\", \"initialization\", \"start\")\n    )\n\n    class Warning(OWWidget.Warning):\n        ignoring_disc_variables = Msg(\"SOM ignores categorical variables.\")\n        missing_colors = \\\n            Msg(\"Some data instances have undefined value of '{}'.\")\n        missing_values = \\\n            Msg(\"{} data instance{} with undefined value(s) {} not shown.\")\n        single_attribute = Msg(\"Data contains a single numeric column.\")\n\n    class Error(OWWidget.Error):\n        no_numeric_variables = Msg(\"Data contains no numeric columns.\")\n        no_defined_rows = Msg(\"All rows contain at least one undefined value.\")\n\n    def __init__(self):\n        super().__init__()\n        self.__pending_selection = self.selection\n        self._optimizer = None\n        self._optimizer_thread = None\n        self.stop_optimization = False\n\n        self.data = self.cont_x = None\n        self.cells = self.member_data = None\n        self.selection = None\n        self.colors = self.thresholds = self.bin_labels = None\n\n        box = gui.vBox(self.controlArea, box=\"SOM\")\n        shape = gui.comboBox(\n            box, self, \"\", items=(\"Hexagonal grid\", \"Square grid\"))\n        shape.setCurrentIndex(1 - self.hexagonal)\n\n        box2 = gui.indentedBox(box, 10)\n        auto_dim = gui.checkBox(\n            box2, self, \"auto_dimension\", \"Set dimensions automatically\",\n            callback=self.on_auto_dimension_changed)\n        self.manual_box = box3 = gui.hBox(box2)\n        spinargs = dict(\n            value=\"\", widget=box3, master=self, minv=5, maxv=100, step=5,\n            alignment=Qt.AlignRight)\n        spin_x = gui.spin(**spinargs)\n        spin_x.setValue(self.size_x)\n        gui.widgetLabel(box3, \"×\")\n        spin_y = gui.spin(**spinargs)\n        spin_y.setValue(self.size_y)\n        gui.rubber(box3)\n        self.manual_box.setEnabled(not self.auto_dimension)\n\n        initialization = gui.comboBox(\n            box, self, \"initialization\",\n            items=(\"Initialize with PCA\", \"Random initialization\",\n                   \"Replicable random\"))\n\n        start = gui.button(\n            box, self, \"Restart\", callback=self.restart_som_pressed,\n            sizePolicy=(QSizePolicy.MinimumExpanding, QSizePolicy.Fixed))\n\n        self.opt_controls = self.OptControls(\n            shape, auto_dim, spin_x, spin_y, initialization, start)\n\n        box = gui.vBox(self.controlArea, \"Color\")\n        gui.comboBox(\n            box, self, \"attr_color\", searchable=True,\n            callback=self.on_attr_color_change,\n            model=DomainModel(placeholder=\"(Same color)\",\n                              valid_types=DomainModel.PRIMITIVE))\n        gui.checkBox(\n            box, self, \"pie_charts\", label=\"Show pie charts\",\n            callback=self.on_pie_chart_change)\n        gui.checkBox(\n            box, self, \"size_by_instances\", label=\"Size by number of instances\",\n            callback=self.on_attr_size_change)\n\n        gui.rubber(self.controlArea)\n\n        self.scene = QGraphicsScene(self)\n\n        self.view = SomView(self.scene)\n        self.view.setMinimumWidth(400)\n        self.view.setMinimumHeight(400)\n        self.view.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)\n        self.view.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)\n        self.view.setRenderHint(QPainter.Antialiasing)\n        self.view.selection_changed.connect(self.on_selection_change)\n        self.view.selection_moved.connect(self.on_selection_move)\n        self.view.selection_mark_changed.connect(self.on_selection_mark_change)\n        self.mainArea.layout().addWidget(self.view)\n\n        self.elements = None\n        self.grid = None\n        self.grid_cells = None\n        self.legend = None\n\n    @Inputs.data\n    def set_data(self, data):\n        def prepare_data():\n            if len(cont_attrs) < len(attrs):\n                self.Warning.ignoring_disc_variables()\n            if len(cont_attrs) == 1:\n                self.Warning.single_attribute()\n            x = Table.from_table(Domain(cont_attrs), data).X\n            if sp.issparse(x):\n                self.data = data\n                self.cont_x = x.tocsr()\n            else:\n                mask = np.all(np.isfinite(x), axis=1)\n                if not np.any(mask):\n                    self.Error.no_defined_rows()\n                else:\n                    if np.all(mask):\n                        self.data = data\n                        self.cont_x = x.copy()\n                    else:\n                        self.data = data[mask]\n                        self.cont_x = x[mask]\n                    self.cont_x -= np.min(self.cont_x, axis=0)[None, :]\n                    sums = np.sum(self.cont_x, axis=0)[None, :]\n                    sums[sums == 0] = 1\n                    self.cont_x /= sums\n\n        def set_warnings():\n            missing = len(data) - len(self.data)\n            if missing == 1:\n                self.Warning.missing_values(1, \"\", \"is\")\n            elif missing > 1:\n                self.Warning.missing_values(missing, \"s\", \"are\")\n\n        self.stop_optimization_and_wait()\n\n        self.closeContext()\n        self.clear()\n        self.Error.clear()\n        self.Warning.clear()\n\n        if data is not None:\n            attrs = data.domain.attributes\n            cont_attrs = [var for var in attrs if var.is_continuous]\n            if not cont_attrs:\n                self.Error.no_numeric_variables()\n            else:\n                prepare_data()\n\n        if self.data is not None:\n            self.controls.attr_color.model().set_domain(data.domain)\n            self.attr_color = data.domain.class_var\n            set_warnings()\n\n        self.openContext(self.data)\n        self.set_color_bins()\n        self.create_legend()\n        self.recompute_dimensions()\n        self.start_som()\n\n    def clear(self):\n        self.data = self.cont_x = None\n        self.cells = self.member_data = None\n        self.attr_color = None\n        self.colors = self.thresholds = self.bin_labels = None\n        if self.elements is not None:\n            self.scene.removeItem(self.elements)\n            self.elements = None\n        self.clear_selection()\n        self.controls.attr_color.model().set_domain(None)\n        self.Warning.clear()\n        self.Error.clear()\n\n    def recompute_dimensions(self):\n        if not self.auto_dimension or self.cont_x is None:\n            return\n        dim = max(5, int(np.ceil(np.sqrt(5 * np.sqrt(self.cont_x.shape[0])))))\n        self.opt_controls.spin_x.setValue(dim)\n        self.opt_controls.spin_y.setValue(dim)\n\n    def on_auto_dimension_changed(self):\n        self.manual_box.setEnabled(not self.auto_dimension)\n        if self.auto_dimension:\n            self.recompute_dimensions()\n        else:\n            spin_x = self.opt_controls.spin_x\n            spin_y = self.opt_controls.spin_y\n            dimx = int(5 * np.round(spin_x.value() / 5))\n            dimy = int(5 * np.round(spin_y.value() / 5))\n            spin_x.setValue(dimx)\n            spin_y.setValue(dimy)\n\n    def on_attr_color_change(self):\n        self.controls.pie_charts.setEnabled(self.attr_color is not None)\n        self.set_color_bins()\n        self.create_legend()\n        self.rescale()\n        self._redraw()\n\n    def on_attr_size_change(self):\n        self._redraw()\n\n    def on_pie_chart_change(self):\n        self._redraw()\n\n    def clear_selection(self):\n        self.selection = None\n        self.redraw_selection()\n\n    def on_selection_change(self, selection, action=SomView.SelectionSet):\n        if self.data is None:  # clicks on empty canvas\n            return\n        if self.selection is None:\n            self.selection = np.zeros(self.grid_cells.T.shape, dtype=np.int16)\n        if action == SomView.SelectionSet:\n            self.selection[:] = 0\n            self.selection[selection] = 1\n        elif action == SomView.SelectionAddToGroup:\n            self.selection[selection] = max(1, np.max(self.selection))\n        elif action == SomView.SelectionNewGroup:\n            self.selection[selection] = 1 + np.max(self.selection)\n        elif action & SomView.SelectionRemove:\n            self.selection[selection] = 0\n        self.redraw_selection()\n        self.update_output()\n\n    def on_selection_move(self, event: QKeyEvent):\n        if self.selection is None or not np.any(self.selection):\n            if event.key() in (Qt.Key_Right, Qt.Key_Down):\n                x = y = 0\n            else:\n                x = self.size_x - 1\n                y = self.size_y - 1\n        else:\n            x, y = np.nonzero(self.selection)\n            if len(x) > 1:\n                return\n            if event.key() == Qt.Key_Up and y > 0:\n                y -= 1\n            if event.key() == Qt.Key_Down and y < self.size_y - 1:\n                y += 1\n            if event.key() == Qt.Key_Left and x:\n                x -= 1\n            if event.key() == Qt.Key_Right and x < self.size_x - 1:\n                x += 1\n            x -= self.hexagonal and x == self.size_x - 1 and y % 2\n\n        if self.selection is not None and self.selection[x, y]:\n            return\n        selection = np.zeros(self.grid_cells.shape, dtype=bool)\n        selection[x, y] = True\n        self.on_selection_change(selection)\n\n    def on_selection_mark_change(self, marks):\n        self.redraw_selection(marks=marks)\n\n    def redraw_selection(self, marks=None):\n        if self.grid_cells is None:\n            return\n\n        sel_pen = QPen(QBrush(QColor(128, 128, 128)), 2)\n        sel_pen.setCosmetic(True)\n        mark_pen = QPen(QBrush(QColor(128, 128, 128)), 4)\n        mark_pen.setCosmetic(True)\n        pens = [self._grid_pen, sel_pen]\n\n        mark_brush = QBrush(QColor(224, 255, 255))\n        sels = self.selection is not None and np.max(self.selection)\n        palette = LimitedDiscretePalette(number_of_colors=sels + 1)\n        brushes = [QBrush(Qt.NoBrush)] + \\\n                  [QBrush(palette[i].lighter(165)) for i in range(sels)]\n\n        for y in range(self.size_y):\n            for x in range(self.size_x - (y % 2) * self.hexagonal):\n                cell = self.grid_cells[y, x]\n                marked = marks is not None and marks[x, y]\n                sel_group = self.selection is not None and self.selection[x, y]\n                if marked:\n                    cell.setBrush(mark_brush)\n                    cell.setPen(mark_pen)\n                else:\n                    cell.setBrush(brushes[sel_group])\n                    cell.setPen(pens[bool(sel_group)])\n                cell.setZValue(marked or sel_group)\n\n    def restart_som_pressed(self):\n        if self._optimizer_thread is not None:\n            self.stop_optimization = True\n            self._optimizer.stop_optimization = True\n        else:\n            self.start_som()\n\n    def start_som(self):\n        self.read_controls()\n        self.update_layout()\n        self.clear_selection()\n        if self.cont_x is not None:\n            self.enable_controls(False)\n            self._recompute_som()\n        else:\n            self.update_output()\n\n    def read_controls(self):\n        c = self.opt_controls\n        self.hexagonal = c.shape.currentIndex() == 0\n        self.size_x = c.spin_x.value()\n        self.size_y = c.spin_y.value()\n\n    def enable_controls(self, enable):\n        c = self.opt_controls\n        c.shape.setEnabled(enable)\n        c.auto_dim.setEnabled(enable)\n        c.start.setText(\"Start\" if enable else \"Stop\")\n\n    def update_layout(self):\n        self.set_legend_pos()\n        if self.elements:  # Prevent having redrawn grid but with old elements\n            self.scene.removeItem(self.elements)\n            self.elements = None\n        self.redraw_grid()\n        self.rescale()\n\n    def _redraw(self):\n        self.Warning.missing_colors.clear()\n        if self.elements:\n            self.scene.removeItem(self.elements)\n            self.elements = None\n        self.view.set_dimensions(self.size_x, self.size_y, self.hexagonal)\n\n        if self.cells is None:\n            return\n        sizes = self.cells[:, :, 1] - self.cells[:, :, 0]\n        sizes = sizes.astype(float)\n        if not self.size_by_instances:\n            sizes[sizes != 0] = 0.8\n        else:\n            sizes *= 0.8 / np.max(sizes)\n\n        self.elements = QGraphicsItemGroup()\n        self.scene.addItem(self.elements)\n        if self.attr_color is None:\n            self._draw_same_color(sizes)\n        elif self.pie_charts:\n            self._draw_pie_charts(sizes)\n        else:\n            self._draw_colored_circles(sizes)\n\n    @property\n    def _grid_factors(self):\n        return (0.5, sqrt3_2) if self.hexagonal else (0, 1)\n\n    def _draw_same_color(self, sizes):\n        fx, fy = self._grid_factors\n        color = QColor(64, 64, 64)\n        for y in range(self.size_y):\n            for x in range(self.size_x - self.hexagonal * (y % 2)):\n                r = sizes[x, y]\n                n = len(self.get_member_indices(x, y))\n                if not r:\n                    continue\n                ellipse = ColoredCircle(r / 2, color, 0)\n                ellipse.setPos(x + (y % 2) * fx, y * fy)\n                ellipse.setToolTip(f\"{n} instances\")\n                self.elements.addToGroup(ellipse)\n\n    def _get_color_column(self):\n        color_column = \\\n            self.data.get_column_view(self.attr_color)[0].astype(float,\n                                                                 copy=False)\n        if self.attr_color.is_discrete:\n            with np.errstate(invalid=\"ignore\"):\n                int_col = color_column.astype(int)\n            int_col[np.isnan(color_column)] = len(self.colors)\n        else:\n            int_col = np.zeros(len(color_column), dtype=int)\n            # The following line is unnecessary because rows with missing\n            # numeric data are excluded. Uncomment it if you change SOM to\n            # tolerate missing values.\n            # int_col[np.isnan(color_column)] = len(self.colors)\n            for i, thresh in enumerate(self.thresholds, start=1):\n                int_col[color_column >= thresh] = i\n        return int_col\n\n    def _tooltip(self, colors, distribution):\n        if self.attr_color.is_discrete:\n            values = self.attr_color.values\n        else:\n            values = self._bin_names()\n        tot = np.sum(distribution)\n        nbhp = \"\\N{NON-BREAKING HYPHEN}\"\n        return '<table style=\"white-space: nowrap\">' + \"\".join(f\"\"\"\n            <tr>\n                <td>\n                    <font color={color.name()}>■</font>\n                    <b>{escape(val).replace(\"-\", nbhp)}</b>:\n                </td>\n                <td>\n                    {n} ({n / tot * 100:.1f}&nbsp;%)\n                </td>\n            </tr>\n            \"\"\" for color, val, n in zip(colors, values, distribution) if n) \\\n            + \"</table>\"\n\n    def _draw_pie_charts(self, sizes):\n        fx, fy = self._grid_factors\n        color_column = self._get_color_column()\n        colors = self.colors.qcolors_w_nan\n        for y in range(self.size_y):\n            for x in range(self.size_x - self.hexagonal * (y % 2)):\n                r = sizes[x, y]\n                if not r:\n                    self.grid_cells[y, x].setToolTip(\"\")\n                    continue\n                members = self.get_member_indices(x, y)\n                color_dist = np.bincount(color_column[members],\n                                         minlength=len(colors))\n                rel_color_dist = color_dist.astype(float) / len(members)\n                pie = PieChart(rel_color_dist, r / 2, colors)\n                pie.setToolTip(self._tooltip(colors, color_dist))\n                self.elements.addToGroup(pie)\n                pie.setPos(x + (y % 2) * fx, y * fy)\n\n    def _draw_colored_circles(self, sizes):\n        fx, fy = self._grid_factors\n        color_column = self._get_color_column()\n        qcolors = self.colors.qcolors_w_nan\n        for y in range(self.size_y):\n            for x in range(self.size_x - self.hexagonal * (y % 2)):\n                r = sizes[x, y]\n                if not r:\n                    continue\n                members = self.get_member_indices(x, y)\n                color_dist = color_column[members]\n                color_dist = color_dist[color_dist < len(self.colors)]\n                if len(color_dist) != len(members):\n                    self.Warning.missing_colors(self.attr_color.name)\n                bc = np.bincount(color_dist, minlength=len(self.colors))\n                color = qcolors[np.argmax(bc)]\n                ellipse = ColoredCircle(r / 2, color, np.max(bc) / len(members))\n                ellipse.setPos(x + (y % 2) * fx, y * fy)\n                ellipse.setToolTip(self._tooltip(qcolors, bc))\n                self.elements.addToGroup(ellipse)\n\n    def redraw_grid(self):\n        if self.grid is not None:\n            self.scene.removeItem(self.grid)\n        self.grid = QGraphicsItemGroup()\n        self.grid.setZValue(-200)\n        self.grid_cells = np.full((self.size_y, self.size_x), None)\n        for y in range(self.size_y):\n            for x in range(self.size_x - (y % 2) * self.hexagonal):\n                if self.hexagonal:\n                    cell = QGraphicsPathItem(_hexagon_path)\n                    cell.setPos(x + (y % 2) / 2, y * sqrt3_2)\n                else:\n                    cell = QGraphicsRectItem(x - 0.5, y - 0.5, 1, 1)\n                self.grid_cells[y, x] = cell\n                cell.setPen(self._grid_pen)\n                self.grid.addToGroup(cell)\n        self.scene.addItem(self.grid)\n\n    def get_member_indices(self, x, y):\n        i, j = self.cells[x, y]\n        return self.member_data[i:j]\n\n    def _recompute_som(self):\n        if self.cont_x is None:\n            return\n\n        som = SOM(\n            self.size_x, self.size_y,\n            hexagonal=self.hexagonal,\n            pca_init=self.initialization == 0,\n            random_seed=0 if self.initialization == 2 else None\n        )\n\n        class Optimizer(QObject):\n            update = Signal(float, np.ndarray, np.ndarray)\n            done = Signal(SOM)\n            stopped = Signal()\n            stop_optimization = False\n\n            def __init__(self, data, som):\n                super().__init__()\n                self.som = som\n                self.data = data\n\n            def callback(self, progress):\n                self.update.emit(\n                    progress,\n                    self.som.weights.copy(), self.som.ssum_weights.copy())\n                return not self.stop_optimization\n\n            def run(self):\n                try:\n                    self.som.fit(self.data, N_ITERATIONS,\n                                 callback=self.callback)\n                    # Report an exception, but still remove the thread\n                finally:\n                    self.done.emit(self.som)\n                    self.stopped.emit()\n\n        def thread_finished():\n            self._optimizer = None\n            self._optimizer_thread = None\n\n        self.progressBarInit()\n\n        self._optimizer = Optimizer(self.cont_x, som)\n        self._optimizer_thread = QThread()\n        self._optimizer_thread.setStackSize(5 * 2 ** 20)\n        self._optimizer.update.connect(self.__update)\n        self._optimizer.done.connect(self.__done)\n        self._optimizer.stopped.connect(self._optimizer_thread.quit)\n        self._optimizer.moveToThread(self._optimizer_thread)\n        self._optimizer_thread.started.connect(self._optimizer.run)\n        self._optimizer_thread.finished.connect(thread_finished)\n        self.stop_optimization = False\n        self._optimizer_thread.start()\n\n    @Slot(float, object, object)\n    def __update(self, _progress, weights, ssum_weights):\n        self.progressBarSet(_progress)\n        self._assign_instances(weights, ssum_weights)\n        self._redraw()\n\n    @Slot(object)\n    def __done(self, som):\n        self.enable_controls(True)\n        self.progressBarFinished()\n        self._assign_instances(som.weights, som.ssum_weights)\n        self._redraw()\n        # This is the first time we know what was selected (assuming that\n        # initialization is not set to random)\n        if self.__pending_selection is not None:\n            self.on_selection_change(self.__pending_selection)\n            self.__pending_selection = None\n        self.update_output()\n\n    def stop_optimization_and_wait(self):\n        if self._optimizer_thread is not None:\n            self.stop_optimization = True\n            self._optimizer.stop_optimization = True\n            self._optimizer_thread.quit()\n            self._optimizer_thread.wait()\n            self._optimizer_thread = None\n\n    def onDeleteWidget(self):\n        self.stop_optimization_and_wait()\n        self.clear()\n        super().onDeleteWidget()\n\n    def _assign_instances(self, weights, ssum_weights):\n        if self.cont_x is None:\n            return  # the widget is shutting down while signals still processed\n        assignments = SOM.winner_from_weights(\n            self.cont_x, weights, ssum_weights, self.hexagonal)\n        members = defaultdict(list)\n        for i, (x, y) in enumerate(assignments):\n            members[(x, y)].append(i)\n        members.pop(None, None)\n        self.cells = np.empty((self.size_x, self.size_y, 2), dtype=int)\n        self.member_data = np.empty(self.cont_x.shape[0], dtype=int)\n        index = 0\n        for x in range(self.size_x):\n            for y in range(self.size_y):\n                nmembers = len(members[(x, y)])\n                self.member_data[index:index + nmembers] = members[(x, y)]\n                self.cells[x, y] = [index, index + nmembers]\n                index += nmembers\n\n    def resizeEvent(self, event):\n        super().resizeEvent(event)\n        self.create_legend()  # re-wrap lines if necessary\n        self.rescale()\n\n    def rescale(self):\n        if self.legend:\n            leg_height = self.legend.boundingRect().height()\n            leg_extra = 1.5\n        else:\n            leg_height = 0\n            leg_extra = 1\n\n        vw, vh = self.view.width(), self.view.height() - leg_height\n        scale = min(vw / (self.size_x + 1),\n                    vh / ((self.size_y + leg_extra) * self._grid_factors[1]))\n        self.view.setTransform(QTransform.fromScale(scale, scale))\n        if self.hexagonal:\n            self.view.setSceneRect(\n                0, -1, self.size_x - 1,\n                (self.size_y + leg_extra) * sqrt3_2 + leg_height / scale)\n        else:\n            self.view.setSceneRect(\n                -0.25, -0.25, self.size_x - 0.5,\n                self.size_y - 0.5 + leg_height / scale)\n\n    def update_output(self):\n        if self.data is None:\n            self.Outputs.selected_data.send(None)\n            self.Outputs.annotated_data.send(None)\n            return\n\n        indices = np.zeros(len(self.data), dtype=int)\n        if self.selection is not None and np.any(self.selection):\n            for y in range(self.size_y):\n                for x in range(self.size_x):\n                    rows = self.get_member_indices(x, y)\n                    indices[rows] = self.selection[x, y]\n\n        if np.any(indices):\n            sel_data = create_groups_table(self.data, indices, False, \"Group\")\n            self.Outputs.selected_data.send(sel_data)\n        else:\n            self.Outputs.selected_data.send(None)\n\n        if np.max(indices) > 1:\n            annotated = create_groups_table(self.data, indices)\n        else:\n            annotated = create_annotated_table(\n                self.data, np.flatnonzero(indices))\n        self.Outputs.annotated_data.send(annotated)\n\n    def set_color_bins(self):\n        if self.attr_color is None:\n            self.thresholds = self.bin_labels = self.colors = None\n        elif self.attr_color.is_discrete:\n            self.thresholds = self.bin_labels = None\n            self.colors = self.attr_color.palette\n        else:\n            col = self.data.get_column_view(self.attr_color)[0].astype(float)\n            if self.attr_color.is_time:\n                binning = time_binnings(col, min_bins=4)[-1]\n            else:\n                binning = decimal_binnings(col, min_bins=4)[-1]\n            self.thresholds = binning.thresholds[1:-1]\n            self.bin_labels = (binning.labels[1:-1], binning.short_labels[1:-1])\n            palette = BinnedContinuousPalette.from_palette(\n                self.attr_color.palette, binning.thresholds)\n            self.colors = palette\n\n    def create_legend(self):\n        if self.legend is not None:\n            self.scene.removeItem(self.legend)\n            self.legend = None\n        if self.attr_color is None:\n            return\n\n        if self.attr_color.is_discrete:\n            names = self.attr_color.values\n        else:\n            names = self._bin_names()\n\n        items = []\n        size = 8\n        for name, color in zip(names, self.colors.qcolors):\n            item = QGraphicsItemGroup()\n            item.addToGroup(\n                CanvasRectangle(None, -size / 2, -size / 2, size, size,\n                                Qt.gray, color))\n            item.addToGroup(CanvasText(None, name, size, 0, Qt.AlignVCenter))\n            items.append(item)\n\n        self.legend = wrap_legend_items(\n            items, hspacing=20, vspacing=16 + size,\n            max_width=self.view.width() - 25)\n        self.legend.setFlags(self.legend.ItemIgnoresTransformations)\n        self.legend.setTransform(\n            QTransform.fromTranslate(-self.legend.boundingRect().width() / 2,\n                                     0))\n        self.scene.addItem(self.legend)\n        self.set_legend_pos()\n\n    def _bin_names(self):\n        labels, short_labels = self.bin_labels\n        return \\\n            [f\"< {labels[0]}\"] \\\n            + [f\"{x} - {y}\" for x, y in zip(labels, short_labels[1:])] \\\n            + [f\"≥ {labels[-1]}\"]\n\n    def set_legend_pos(self):\n        if self.legend is None:\n            return\n        self.legend.setPos(\n            self.size_x / 2,\n            (self.size_y + 0.2 + 0.3 * self.hexagonal) * self._grid_factors[1])\n\n    def send_report(self):\n        self.report_plot()\n        if self.attr_color:\n            self.report_caption(\n                f\"Self-organizing map colored by '{self.attr_color.name}'\")",
                                "ast_type_str": "ClassDef",
                                "start_lineno": 174,
                                "end_lineno": 890,
                                "parent": null
                            }
                        },
                        {
                            "file_name": "Orange/widgets/unsupervised/owsom.py",
                            "type": "class",
                            "name": "OWSOM",
                            "content": "class OWSOM(OWWidget):\n    name = \"Self-Organizing Map\"\n    description = \"Computation of self-organizing map.\"\n    icon = \"icons/SOM.svg\"\n    keywords = [\"SOM\"]\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    settingsHandler = DomainContextHandler()\n    auto_dimension = Setting(True)\n    size_x = Setting(10)\n    size_y = Setting(10)\n    hexagonal = Setting(1)\n    initialization = Setting(0)\n\n    attr_color = ContextSetting(None)\n    size_by_instances = Setting(True)\n    pie_charts = Setting(False)\n    selection = Setting(None, schema_only=True)\n\n    graph_name = \"view\"\n\n    _grid_pen = QPen(QBrush(QColor(224, 224, 224)), 2)\n    _grid_pen.setCosmetic(True)\n\n    OptControls = namedtuple(\n        \"OptControls\",\n        (\"shape\", \"auto_dim\", \"spin_x\", \"spin_y\", \"initialization\", \"start\")\n    )\n\n    class Warning(OWWidget.Warning):\n        ignoring_disc_variables = Msg(\"SOM ignores categorical variables.\")\n        missing_colors = \\\n            Msg(\"Some data instances have undefined value of '{}'.\")\n        missing_values = \\\n            Msg(\"{} data instance{} with undefined value(s) {} not shown.\")\n        single_attribute = Msg(\"Data contains a single numeric column.\")\n\n    class Error(OWWidget.Error):\n        no_numeric_variables = Msg(\"Data contains no numeric columns.\")\n        no_defined_rows = Msg(\"All rows contain at least one undefined value.\")\n\n    def __init__(self):\n        super().__init__()\n        self.__pending_selection = self.selection\n        self._optimizer = None\n        self._optimizer_thread = None\n        self.stop_optimization = False\n\n        self.data = self.cont_x = None\n        self.cells = self.member_data = None\n        self.selection = None\n        self.colors = self.thresholds = self.bin_labels = None\n\n        box = gui.vBox(self.controlArea, box=\"SOM\")\n        shape = gui.comboBox(\n            box, self, \"\", items=(\"Hexagonal grid\", \"Square grid\"))\n        shape.setCurrentIndex(1 - self.hexagonal)\n\n        box2 = gui.indentedBox(box, 10)\n        auto_dim = gui.checkBox(\n            box2, self, \"auto_dimension\", \"Set dimensions automatically\",\n            callback=self.on_auto_dimension_changed)\n        self.manual_box = box3 = gui.hBox(box2)\n        spinargs = dict(\n            value=\"\", widget=box3, master=self, minv=5, maxv=100, step=5,\n            alignment=Qt.AlignRight)\n        spin_x = gui.spin(**spinargs)\n        spin_x.setValue(self.size_x)\n        gui.widgetLabel(box3, \"×\")\n        spin_y = gui.spin(**spinargs)\n        spin_y.setValue(self.size_y)\n        gui.rubber(box3)\n        self.manual_box.setEnabled(not self.auto_dimension)\n\n        initialization = gui.comboBox(\n            box, self, \"initialization\",\n            items=(\"Initialize with PCA\", \"Random initialization\",\n                   \"Replicable random\"))\n\n        start = gui.button(\n            box, self, \"Restart\", callback=self.restart_som_pressed,\n            sizePolicy=(QSizePolicy.MinimumExpanding, QSizePolicy.Fixed))\n\n        self.opt_controls = self.OptControls(\n            shape, auto_dim, spin_x, spin_y, initialization, start)\n\n        box = gui.vBox(self.controlArea, \"Color\")\n        gui.comboBox(\n            box, self, \"attr_color\", searchable=True,\n            callback=self.on_attr_color_change,\n            model=DomainModel(placeholder=\"(Same color)\",\n                              valid_types=DomainModel.PRIMITIVE))\n        gui.checkBox(\n            box, self, \"pie_charts\", label=\"Show pie charts\",\n            callback=self.on_pie_chart_change)\n        gui.checkBox(\n            box, self, \"size_by_instances\", label=\"Size by number of instances\",\n            callback=self.on_attr_size_change)\n\n        gui.rubber(self.controlArea)\n\n        self.scene = QGraphicsScene(self)\n\n        self.view = SomView(self.scene)\n        self.view.setMinimumWidth(400)\n        self.view.setMinimumHeight(400)\n        self.view.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)\n        self.view.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)\n        self.view.setRenderHint(QPainter.Antialiasing)\n        self.view.selection_changed.connect(self.on_selection_change)\n        self.view.selection_moved.connect(self.on_selection_move)\n        self.view.selection_mark_changed.connect(self.on_selection_mark_change)\n        self.mainArea.layout().addWidget(self.view)\n\n        self.elements = None\n        self.grid = None\n        self.grid_cells = None\n        self.legend = None\n\n    @Inputs.data\n    def set_data(self, data):\n        def prepare_data():\n            if len(cont_attrs) < len(attrs):\n                self.Warning.ignoring_disc_variables()\n            if len(cont_attrs) == 1:\n                self.Warning.single_attribute()\n            x = Table.from_table(Domain(cont_attrs), data).X\n            if sp.issparse(x):\n                self.data = data\n                self.cont_x = x.tocsr()\n            else:\n                mask = np.all(np.isfinite(x), axis=1)\n                if not np.any(mask):\n                    self.Error.no_defined_rows()\n                else:\n                    if np.all(mask):\n                        self.data = data\n                        self.cont_x = x.copy()\n                    else:\n                        self.data = data[mask]\n                        self.cont_x = x[mask]\n                    self.cont_x -= np.min(self.cont_x, axis=0)[None, :]\n                    sums = np.sum(self.cont_x, axis=0)[None, :]\n                    sums[sums == 0] = 1\n                    self.cont_x /= sums\n\n        def set_warnings():\n            missing = len(data) - len(self.data)\n            if missing == 1:\n                self.Warning.missing_values(1, \"\", \"is\")\n            elif missing > 1:\n                self.Warning.missing_values(missing, \"s\", \"are\")\n\n        self.stop_optimization_and_wait()\n\n        self.closeContext()\n        self.clear()\n        self.Error.clear()\n        self.Warning.clear()\n\n        if data is not None:\n            attrs = data.domain.attributes\n            cont_attrs = [var for var in attrs if var.is_continuous]\n            if not cont_attrs:\n                self.Error.no_numeric_variables()\n            else:\n                prepare_data()\n\n        if self.data is not None:\n            self.controls.attr_color.model().set_domain(data.domain)\n            self.attr_color = data.domain.class_var\n            set_warnings()\n\n        self.openContext(self.data)\n        self.set_color_bins()\n        self.create_legend()\n        self.recompute_dimensions()\n        self.start_som()\n\n    def clear(self):\n        self.data = self.cont_x = None\n        self.cells = self.member_data = None\n        self.attr_color = None\n        self.colors = self.thresholds = self.bin_labels = None\n        if self.elements is not None:\n            self.scene.removeItem(self.elements)\n            self.elements = None\n        self.clear_selection()\n        self.controls.attr_color.model().set_domain(None)\n        self.Warning.clear()\n        self.Error.clear()\n\n    def recompute_dimensions(self):\n        if not self.auto_dimension or self.cont_x is None:\n            return\n        dim = max(5, int(np.ceil(np.sqrt(5 * np.sqrt(self.cont_x.shape[0])))))\n        self.opt_controls.spin_x.setValue(dim)\n        self.opt_controls.spin_y.setValue(dim)\n\n    def on_auto_dimension_changed(self):\n        self.manual_box.setEnabled(not self.auto_dimension)\n        if self.auto_dimension:\n            self.recompute_dimensions()\n        else:\n            spin_x = self.opt_controls.spin_x\n            spin_y = self.opt_controls.spin_y\n            dimx = int(5 * np.round(spin_x.value() / 5))\n            dimy = int(5 * np.round(spin_y.value() / 5))\n            spin_x.setValue(dimx)\n            spin_y.setValue(dimy)\n\n    def on_attr_color_change(self):\n        self.controls.pie_charts.setEnabled(self.attr_color is not None)\n        self.set_color_bins()\n        self.create_legend()\n        self.rescale()\n        self._redraw()\n\n    def on_attr_size_change(self):\n        self._redraw()\n\n    def on_pie_chart_change(self):\n        self._redraw()\n\n    def clear_selection(self):\n        self.selection = None\n        self.redraw_selection()\n\n    def on_selection_change(self, selection, action=SomView.SelectionSet):\n        if self.data is None:  # clicks on empty canvas\n            return\n        if self.selection is None:\n            self.selection = np.zeros(self.grid_cells.T.shape, dtype=np.int16)\n        if action == SomView.SelectionSet:\n            self.selection[:] = 0\n            self.selection[selection] = 1\n        elif action == SomView.SelectionAddToGroup:\n            self.selection[selection] = max(1, np.max(self.selection))\n        elif action == SomView.SelectionNewGroup:\n            self.selection[selection] = 1 + np.max(self.selection)\n        elif action & SomView.SelectionRemove:\n            self.selection[selection] = 0\n        self.redraw_selection()\n        self.update_output()\n\n    def on_selection_move(self, event: QKeyEvent):\n        if self.selection is None or not np.any(self.selection):\n            if event.key() in (Qt.Key_Right, Qt.Key_Down):\n                x = y = 0\n            else:\n                x = self.size_x - 1\n                y = self.size_y - 1\n        else:\n            x, y = np.nonzero(self.selection)\n            if len(x) > 1:\n                return\n            if event.key() == Qt.Key_Up and y > 0:\n                y -= 1\n            if event.key() == Qt.Key_Down and y < self.size_y - 1:\n                y += 1\n            if event.key() == Qt.Key_Left and x:\n                x -= 1\n            if event.key() == Qt.Key_Right and x < self.size_x - 1:\n                x += 1\n            x -= self.hexagonal and x == self.size_x - 1 and y % 2\n\n        if self.selection is not None and self.selection[x, y]:\n            return\n        selection = np.zeros(self.grid_cells.shape, dtype=bool)\n        selection[x, y] = True\n        self.on_selection_change(selection)\n\n    def on_selection_mark_change(self, marks):\n        self.redraw_selection(marks=marks)\n\n    def redraw_selection(self, marks=None):\n        if self.grid_cells is None:\n            return\n\n        sel_pen = QPen(QBrush(QColor(128, 128, 128)), 2)\n        sel_pen.setCosmetic(True)\n        mark_pen = QPen(QBrush(QColor(128, 128, 128)), 4)\n        mark_pen.setCosmetic(True)\n        pens = [self._grid_pen, sel_pen]\n\n        mark_brush = QBrush(QColor(224, 255, 255))\n        sels = self.selection is not None and np.max(self.selection)\n        palette = LimitedDiscretePalette(number_of_colors=sels + 1)\n        brushes = [QBrush(Qt.NoBrush)] + \\\n                  [QBrush(palette[i].lighter(165)) for i in range(sels)]\n\n        for y in range(self.size_y):\n            for x in range(self.size_x - (y % 2) * self.hexagonal):\n                cell = self.grid_cells[y, x]\n                marked = marks is not None and marks[x, y]\n                sel_group = self.selection is not None and self.selection[x, y]\n                if marked:\n                    cell.setBrush(mark_brush)\n                    cell.setPen(mark_pen)\n                else:\n                    cell.setBrush(brushes[sel_group])\n                    cell.setPen(pens[bool(sel_group)])\n                cell.setZValue(marked or sel_group)\n\n    def restart_som_pressed(self):\n        if self._optimizer_thread is not None:\n            self.stop_optimization = True\n            self._optimizer.stop_optimization = True\n        else:\n            self.start_som()\n\n    def start_som(self):\n        self.read_controls()\n        self.update_layout()\n        self.clear_selection()\n        if self.cont_x is not None:\n            self.enable_controls(False)\n            self._recompute_som()\n        else:\n            self.update_output()\n\n    def read_controls(self):\n        c = self.opt_controls\n        self.hexagonal = c.shape.currentIndex() == 0\n        self.size_x = c.spin_x.value()\n        self.size_y = c.spin_y.value()\n\n    def enable_controls(self, enable):\n        c = self.opt_controls\n        c.shape.setEnabled(enable)\n        c.auto_dim.setEnabled(enable)\n        c.start.setText(\"Start\" if enable else \"Stop\")\n\n    def update_layout(self):\n        self.set_legend_pos()\n        if self.elements:  # Prevent having redrawn grid but with old elements\n            self.scene.removeItem(self.elements)\n            self.elements = None\n        self.redraw_grid()\n        self.rescale()\n\n    def _redraw(self):\n        self.Warning.missing_colors.clear()\n        if self.elements:\n            self.scene.removeItem(self.elements)\n            self.elements = None\n        self.view.set_dimensions(self.size_x, self.size_y, self.hexagonal)\n\n        if self.cells is None:\n            return\n        sizes = self.cells[:, :, 1] - self.cells[:, :, 0]\n        sizes = sizes.astype(float)\n        if not self.size_by_instances:\n            sizes[sizes != 0] = 0.8\n        else:\n            sizes *= 0.8 / np.max(sizes)\n\n        self.elements = QGraphicsItemGroup()\n        self.scene.addItem(self.elements)\n        if self.attr_color is None:\n            self._draw_same_color(sizes)\n        elif self.pie_charts:\n            self._draw_pie_charts(sizes)\n        else:\n            self._draw_colored_circles(sizes)\n\n    @property\n    def _grid_factors(self):\n        return (0.5, sqrt3_2) if self.hexagonal else (0, 1)\n\n    def _draw_same_color(self, sizes):\n        fx, fy = self._grid_factors\n        color = QColor(64, 64, 64)\n        for y in range(self.size_y):\n            for x in range(self.size_x - self.hexagonal * (y % 2)):\n                r = sizes[x, y]\n                n = len(self.get_member_indices(x, y))\n                if not r:\n                    continue\n                ellipse = ColoredCircle(r / 2, color, 0)\n                ellipse.setPos(x + (y % 2) * fx, y * fy)\n                ellipse.setToolTip(f\"{n} instances\")\n                self.elements.addToGroup(ellipse)\n\n    def _get_color_column(self):\n        color_column = \\\n            self.data.get_column_view(self.attr_color)[0].astype(float,\n                                                                 copy=False)\n        if self.attr_color.is_discrete:\n            with np.errstate(invalid=\"ignore\"):\n                int_col = color_column.astype(int)\n            int_col[np.isnan(color_column)] = len(self.colors)\n        else:\n            int_col = np.zeros(len(color_column), dtype=int)\n            # The following line is unnecessary because rows with missing\n            # numeric data are excluded. Uncomment it if you change SOM to\n            # tolerate missing values.\n            # int_col[np.isnan(color_column)] = len(self.colors)\n            for i, thresh in enumerate(self.thresholds, start=1):\n                int_col[color_column >= thresh] = i\n        return int_col\n\n    def _tooltip(self, colors, distribution):\n        if self.attr_color.is_discrete:\n            values = self.attr_color.values\n        else:\n            values = self._bin_names()\n        tot = np.sum(distribution)\n        nbhp = \"\\N{NON-BREAKING HYPHEN}\"\n        return '<table style=\"white-space: nowrap\">' + \"\".join(f\"\"\"\n            <tr>\n                <td>\n                    <font color={color.name()}>■</font>\n                    <b>{escape(val).replace(\"-\", nbhp)}</b>:\n                </td>\n                <td>\n                    {n} ({n / tot * 100:.1f}&nbsp;%)\n                </td>\n            </tr>\n            \"\"\" for color, val, n in zip(colors, values, distribution) if n) \\\n            + \"</table>\"\n\n    def _draw_pie_charts(self, sizes):\n        fx, fy = self._grid_factors\n        color_column = self._get_color_column()\n        colors = self.colors.qcolors_w_nan\n        for y in range(self.size_y):\n            for x in range(self.size_x - self.hexagonal * (y % 2)):\n                r = sizes[x, y]\n                if not r:\n                    self.grid_cells[y, x].setToolTip(\"\")\n                    continue\n                members = self.get_member_indices(x, y)\n                color_dist = np.bincount(color_column[members],\n                                         minlength=len(colors))\n                rel_color_dist = color_dist.astype(float) / len(members)\n                pie = PieChart(rel_color_dist, r / 2, colors)\n                pie.setToolTip(self._tooltip(colors, color_dist))\n                self.elements.addToGroup(pie)\n                pie.setPos(x + (y % 2) * fx, y * fy)\n\n    def _draw_colored_circles(self, sizes):\n        fx, fy = self._grid_factors\n        color_column = self._get_color_column()\n        qcolors = self.colors.qcolors_w_nan\n        for y in range(self.size_y):\n            for x in range(self.size_x - self.hexagonal * (y % 2)):\n                r = sizes[x, y]\n                if not r:\n                    continue\n                members = self.get_member_indices(x, y)\n                color_dist = color_column[members]\n                color_dist = color_dist[color_dist < len(self.colors)]\n                if len(color_dist) != len(members):\n                    self.Warning.missing_colors(self.attr_color.name)\n                bc = np.bincount(color_dist, minlength=len(self.colors))\n                color = qcolors[np.argmax(bc)]\n                ellipse = ColoredCircle(r / 2, color, np.max(bc) / len(members))\n                ellipse.setPos(x + (y % 2) * fx, y * fy)\n                ellipse.setToolTip(self._tooltip(qcolors, bc))\n                self.elements.addToGroup(ellipse)\n\n    def redraw_grid(self):\n        if self.grid is not None:\n            self.scene.removeItem(self.grid)\n        self.grid = QGraphicsItemGroup()\n        self.grid.setZValue(-200)\n        self.grid_cells = np.full((self.size_y, self.size_x), None)\n        for y in range(self.size_y):\n            for x in range(self.size_x - (y % 2) * self.hexagonal):\n                if self.hexagonal:\n                    cell = QGraphicsPathItem(_hexagon_path)\n                    cell.setPos(x + (y % 2) / 2, y * sqrt3_2)\n                else:\n                    cell = QGraphicsRectItem(x - 0.5, y - 0.5, 1, 1)\n                self.grid_cells[y, x] = cell\n                cell.setPen(self._grid_pen)\n                self.grid.addToGroup(cell)\n        self.scene.addItem(self.grid)\n\n    def get_member_indices(self, x, y):\n        i, j = self.cells[x, y]\n        return self.member_data[i:j]\n\n    def _recompute_som(self):\n        if self.cont_x is None:\n            return\n\n        som = SOM(\n            self.size_x, self.size_y,\n            hexagonal=self.hexagonal,\n            pca_init=self.initialization == 0,\n            random_seed=0 if self.initialization == 2 else None\n        )\n\n        class Optimizer(QObject):\n            update = Signal(float, np.ndarray, np.ndarray)\n            done = Signal(SOM)\n            stopped = Signal()\n            stop_optimization = False\n\n            def __init__(self, data, som):\n                super().__init__()\n                self.som = som\n                self.data = data\n\n            def callback(self, progress):\n                self.update.emit(\n                    progress,\n                    self.som.weights.copy(), self.som.ssum_weights.copy())\n                return not self.stop_optimization\n\n            def run(self):\n                try:\n                    self.som.fit(self.data, N_ITERATIONS,\n                                 callback=self.callback)\n                    # Report an exception, but still remove the thread\n                finally:\n                    self.done.emit(self.som)\n                    self.stopped.emit()\n\n        def thread_finished():\n            self._optimizer = None\n            self._optimizer_thread = None\n\n        self.progressBarInit()\n\n        self._optimizer = Optimizer(self.cont_x, som)\n        self._optimizer_thread = QThread()\n        self._optimizer_thread.setStackSize(5 * 2 ** 20)\n        self._optimizer.update.connect(self.__update)\n        self._optimizer.done.connect(self.__done)\n        self._optimizer.stopped.connect(self._optimizer_thread.quit)\n        self._optimizer.moveToThread(self._optimizer_thread)\n        self._optimizer_thread.started.connect(self._optimizer.run)\n        self._optimizer_thread.finished.connect(thread_finished)\n        self.stop_optimization = False\n        self._optimizer_thread.start()\n\n    @Slot(float, object, object)\n    def __update(self, _progress, weights, ssum_weights):\n        self.progressBarSet(_progress)\n        self._assign_instances(weights, ssum_weights)\n        self._redraw()\n\n    @Slot(object)\n    def __done(self, som):\n        self.enable_controls(True)\n        self.progressBarFinished()\n        self._assign_instances(som.weights, som.ssum_weights)\n        self._redraw()\n        # This is the first time we know what was selected (assuming that\n        # initialization is not set to random)\n        if self.__pending_selection is not None:\n            self.on_selection_change(self.__pending_selection)\n            self.__pending_selection = None\n        self.update_output()\n\n    def stop_optimization_and_wait(self):\n        if self._optimizer_thread is not None:\n            self.stop_optimization = True\n            self._optimizer.stop_optimization = True\n            self._optimizer_thread.quit()\n            self._optimizer_thread.wait()\n            self._optimizer_thread = None\n\n    def onDeleteWidget(self):\n        self.stop_optimization_and_wait()\n        self.clear()\n        super().onDeleteWidget()\n\n    def _assign_instances(self, weights, ssum_weights):\n        if self.cont_x is None:\n            return  # the widget is shutting down while signals still processed\n        assignments = SOM.winner_from_weights(\n            self.cont_x, weights, ssum_weights, self.hexagonal)\n        members = defaultdict(list)\n        for i, (x, y) in enumerate(assignments):\n            members[(x, y)].append(i)\n        members.pop(None, None)\n        self.cells = np.empty((self.size_x, self.size_y, 2), dtype=int)\n        self.member_data = np.empty(self.cont_x.shape[0], dtype=int)\n        index = 0\n        for x in range(self.size_x):\n            for y in range(self.size_y):\n                nmembers = len(members[(x, y)])\n                self.member_data[index:index + nmembers] = members[(x, y)]\n                self.cells[x, y] = [index, index + nmembers]\n                index += nmembers\n\n    def resizeEvent(self, event):\n        super().resizeEvent(event)\n        self.create_legend()  # re-wrap lines if necessary\n        self.rescale()\n\n    def rescale(self):\n        if self.legend:\n            leg_height = self.legend.boundingRect().height()\n            leg_extra = 1.5\n        else:\n            leg_height = 0\n            leg_extra = 1\n\n        vw, vh = self.view.width(), self.view.height() - leg_height\n        scale = min(vw / (self.size_x + 1),\n                    vh / ((self.size_y + leg_extra) * self._grid_factors[1]))\n        self.view.setTransform(QTransform.fromScale(scale, scale))\n        if self.hexagonal:\n            self.view.setSceneRect(\n                0, -1, self.size_x - 1,\n                (self.size_y + leg_extra) * sqrt3_2 + leg_height / scale)\n        else:\n            self.view.setSceneRect(\n                -0.25, -0.25, self.size_x - 0.5,\n                self.size_y - 0.5 + leg_height / scale)\n\n    def update_output(self):\n        if self.data is None:\n            self.Outputs.selected_data.send(None)\n            self.Outputs.annotated_data.send(None)\n            return\n\n        indices = np.zeros(len(self.data), dtype=int)\n        if self.selection is not None and np.any(self.selection):\n            for y in range(self.size_y):\n                for x in range(self.size_x):\n                    rows = self.get_member_indices(x, y)\n                    indices[rows] = self.selection[x, y]\n\n        if np.any(indices):\n            sel_data = create_groups_table(self.data, indices, False, \"Group\")\n            self.Outputs.selected_data.send(sel_data)\n        else:\n            self.Outputs.selected_data.send(None)\n\n        if np.max(indices) > 1:\n            annotated = create_groups_table(self.data, indices)\n        else:\n            annotated = create_annotated_table(\n                self.data, np.flatnonzero(indices))\n        self.Outputs.annotated_data.send(annotated)\n\n    def set_color_bins(self):\n        if self.attr_color is None:\n            self.thresholds = self.bin_labels = self.colors = None\n        elif self.attr_color.is_discrete:\n            self.thresholds = self.bin_labels = None\n            self.colors = self.attr_color.palette\n        else:\n            col = self.data.get_column_view(self.attr_color)[0].astype(float)\n            if self.attr_color.is_time:\n                binning = time_binnings(col, min_bins=4)[-1]\n            else:\n                binning = decimal_binnings(col, min_bins=4)[-1]\n            self.thresholds = binning.thresholds[1:-1]\n            self.bin_labels = (binning.labels[1:-1], binning.short_labels[1:-1])\n            palette = BinnedContinuousPalette.from_palette(\n                self.attr_color.palette, binning.thresholds)\n            self.colors = palette\n\n    def create_legend(self):\n        if self.legend is not None:\n            self.scene.removeItem(self.legend)\n            self.legend = None\n        if self.attr_color is None:\n            return\n\n        if self.attr_color.is_discrete:\n            names = self.attr_color.values\n        else:\n            names = self._bin_names()\n\n        items = []\n        size = 8\n        for name, color in zip(names, self.colors.qcolors):\n            item = QGraphicsItemGroup()\n            item.addToGroup(\n                CanvasRectangle(None, -size / 2, -size / 2, size, size,\n                                Qt.gray, color))\n            item.addToGroup(CanvasText(None, name, size, 0, Qt.AlignVCenter))\n            items.append(item)\n\n        self.legend = wrap_legend_items(\n            items, hspacing=20, vspacing=16 + size,\n            max_width=self.view.width() - 25)\n        self.legend.setFlags(self.legend.ItemIgnoresTransformations)\n        self.legend.setTransform(\n            QTransform.fromTranslate(-self.legend.boundingRect().width() / 2,\n                                     0))\n        self.scene.addItem(self.legend)\n        self.set_legend_pos()\n\n    def _bin_names(self):\n        labels, short_labels = self.bin_labels\n        return \\\n            [f\"< {labels[0]}\"] \\\n            + [f\"{x} - {y}\" for x, y in zip(labels, short_labels[1:])] \\\n            + [f\"≥ {labels[-1]}\"]\n\n    def set_legend_pos(self):\n        if self.legend is None:\n            return\n        self.legend.setPos(\n            self.size_x / 2,\n            (self.size_y + 0.2 + 0.3 * self.hexagonal) * self._grid_factors[1])\n\n    def send_report(self):\n        self.report_plot()\n        if self.attr_color:\n            self.report_caption(\n                f\"Self-organizing map colored by '{self.attr_color.name}'\")",
                            "ast_type_str": "ClassDef",
                            "start_lineno": 174,
                            "end_lineno": 890,
                            "parent": null
                        },
                        {
                            "file_name": "Orange/widgets/unsupervised/owsom.py",
                            "type": "method",
                            "name": "OWSOM.clear",
                            "content": "class OWSOM(OWWidget):\n    name = \"Self-Organizing Map\"\n    description = \"Computation of self-organizing map.\"\n    icon = \"icons/SOM.svg\"\n    keywords = [\"SOM\"]\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    settingsHandler = DomainContextHandler()\n    auto_dimension = Setting(True)\n    size_x = Setting(10)\n    size_y = Setting(10)\n    hexagonal = Setting(1)\n    initialization = Setting(0)\n\n    attr_color = ContextSetting(None)\n    size_by_instances = Setting(True)\n    pie_charts = Setting(False)\n    selection = Setting(None, schema_only=True)\n\n    graph_name = \"view\"\n\n    _grid_pen = QPen(QBrush(QColor(224, 224, 224)), 2)\n    _grid_pen.setCosmetic(True)\n\n    OptControls = namedtuple(\n        \"OptControls\",\n        (\"shape\", \"auto_dim\", \"spin_x\", \"spin_y\", \"initialization\", \"start\")\n    )\n\n    class Warning(OWWidget.Warning):\n        ignoring_disc_variables = Msg(\"SOM ignores categorical variables.\")\n        missing_colors = \\\n            Msg(\"Some data instances have undefined value of '{}'.\")\n        missing_values = \\\n            Msg(\"{} data instance{} with undefined value(s) {} not shown.\")\n        single_attribute = Msg(\"Data contains a single numeric column.\")\n\n    class Error(OWWidget.Error):\n        no_numeric_variables = Msg(\"Data contains no numeric columns.\")\n        no_defined_rows = Msg(\"All rows contain at least one undefined value.\")\n\n    def __init__(self):\n        super().__init__()\n        self.__pending_selection = self.selection\n        self._optimizer = None\n        self._optimizer_thread = None\n        self.stop_optimization = False\n\n        self.data = self.cont_x = None\n        self.cells = self.member_data = None\n        self.selection = None\n        self.colors = self.thresholds = self.bin_labels = None\n\n        box = gui.vBox(self.controlArea, box=\"SOM\")\n        shape = gui.comboBox(\n            box, self, \"\", items=(\"Hexagonal grid\", \"Square grid\"))\n        shape.setCurrentIndex(1 - self.hexagonal)\n\n        box2 = gui.indentedBox(box, 10)\n        auto_dim = gui.checkBox(\n            box2, self, \"auto_dimension\", \"Set dimensions automatically\",\n            callback=self.on_auto_dimension_changed)\n        self.manual_box = box3 = gui.hBox(box2)\n        spinargs = dict(\n            value=\"\", widget=box3, master=self, minv=5, maxv=100, step=5,\n            alignment=Qt.AlignRight)\n        spin_x = gui.spin(**spinargs)\n        spin_x.setValue(self.size_x)\n        gui.widgetLabel(box3, \"×\")\n        spin_y = gui.spin(**spinargs)\n        spin_y.setValue(self.size_y)\n        gui.rubber(box3)\n        self.manual_box.setEnabled(not self.auto_dimension)\n\n        initialization = gui.comboBox(\n            box, self, \"initialization\",\n            items=(\"Initialize with PCA\", \"Random initialization\",\n                   \"Replicable random\"))\n\n        start = gui.button(\n            box, self, \"Restart\", callback=self.restart_som_pressed,\n            sizePolicy=(QSizePolicy.MinimumExpanding, QSizePolicy.Fixed))\n\n        self.opt_controls = self.OptControls(\n            shape, auto_dim, spin_x, spin_y, initialization, start)\n\n        box = gui.vBox(self.controlArea, \"Color\")\n        gui.comboBox(\n            box, self, \"attr_color\", searchable=True,\n            callback=self.on_attr_color_change,\n            model=DomainModel(placeholder=\"(Same color)\",\n                              valid_types=DomainModel.PRIMITIVE))\n        gui.checkBox(\n            box, self, \"pie_charts\", label=\"Show pie charts\",\n            callback=self.on_pie_chart_change)\n        gui.checkBox(\n            box, self, \"size_by_instances\", label=\"Size by number of instances\",\n            callback=self.on_attr_size_change)\n\n        gui.rubber(self.controlArea)\n\n        self.scene = QGraphicsScene(self)\n\n        self.view = SomView(self.scene)\n        self.view.setMinimumWidth(400)\n        self.view.setMinimumHeight(400)\n        self.view.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)\n        self.view.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)\n        self.view.setRenderHint(QPainter.Antialiasing)\n        self.view.selection_changed.connect(self.on_selection_change)\n        self.view.selection_moved.connect(self.on_selection_move)\n        self.view.selection_mark_changed.connect(self.on_selection_mark_change)\n        self.mainArea.layout().addWidget(self.view)\n\n        self.elements = None\n        self.grid = None\n        self.grid_cells = None\n        self.legend = None\n\n    @Inputs.data\n    def set_data(self, data):\n        def prepare_data():\n            if len(cont_attrs) < len(attrs):\n                self.Warning.ignoring_disc_variables()\n            if len(cont_attrs) == 1:\n                self.Warning.single_attribute()\n            x = Table.from_table(Domain(cont_attrs), data).X\n            if sp.issparse(x):\n                self.data = data\n                self.cont_x = x.tocsr()\n            else:\n                mask = np.all(np.isfinite(x), axis=1)\n                if not np.any(mask):\n                    self.Error.no_defined_rows()\n                else:\n                    if np.all(mask):\n                        self.data = data\n                        self.cont_x = x.copy()\n                    else:\n                        self.data = data[mask]\n                        self.cont_x = x[mask]\n                    self.cont_x -= np.min(self.cont_x, axis=0)[None, :]\n                    sums = np.sum(self.cont_x, axis=0)[None, :]\n                    sums[sums == 0] = 1\n                    self.cont_x /= sums\n\n        def set_warnings():\n            missing = len(data) - len(self.data)\n            if missing == 1:\n                self.Warning.missing_values(1, \"\", \"is\")\n            elif missing > 1:\n                self.Warning.missing_values(missing, \"s\", \"are\")\n\n        self.stop_optimization_and_wait()\n\n        self.closeContext()\n        self.clear()\n        self.Error.clear()\n        self.Warning.clear()\n\n        if data is not None:\n            attrs = data.domain.attributes\n            cont_attrs = [var for var in attrs if var.is_continuous]\n            if not cont_attrs:\n                self.Error.no_numeric_variables()\n            else:\n                prepare_data()\n\n        if self.data is not None:\n            self.controls.attr_color.model().set_domain(data.domain)\n            self.attr_color = data.domain.class_var\n            set_warnings()\n\n        self.openContext(self.data)\n        self.set_color_bins()\n        self.create_legend()\n        self.recompute_dimensions()\n        self.start_som()\n\n    def clear(self):\n        self.data = self.cont_x = None\n        self.cells = self.member_data = None\n        self.attr_color = None\n        self.colors = self.thresholds = self.bin_labels = None\n        if self.elements is not None:\n            self.scene.removeItem(self.elements)\n            self.elements = None\n        self.clear_selection()\n        self.controls.attr_color.model().set_domain(None)\n        self.Warning.clear()\n        self.Error.clear()\n\n    def recompute_dimensions(self):\n        if not self.auto_dimension or self.cont_x is None:\n            return\n        dim = max(5, int(np.ceil(np.sqrt(5 * np.sqrt(self.cont_x.shape[0])))))\n        self.opt_controls.spin_x.setValue(dim)\n        self.opt_controls.spin_y.setValue(dim)\n\n    def on_auto_dimension_changed(self):\n        self.manual_box.setEnabled(not self.auto_dimension)\n        if self.auto_dimension:\n            self.recompute_dimensions()\n        else:\n            spin_x = self.opt_controls.spin_x\n            spin_y = self.opt_controls.spin_y\n            dimx = int(5 * np.round(spin_x.value() / 5))\n            dimy = int(5 * np.round(spin_y.value() / 5))\n            spin_x.setValue(dimx)\n            spin_y.setValue(dimy)\n\n    def on_attr_color_change(self):\n        self.controls.pie_charts.setEnabled(self.attr_color is not None)\n        self.set_color_bins()\n        self.create_legend()\n        self.rescale()\n        self._redraw()\n\n    def on_attr_size_change(self):\n        self._redraw()\n\n    def on_pie_chart_change(self):\n        self._redraw()\n\n    def clear_selection(self):\n        self.selection = None\n        self.redraw_selection()\n\n    def on_selection_change(self, selection, action=SomView.SelectionSet):\n        if self.data is None:  # clicks on empty canvas\n            return\n        if self.selection is None:\n            self.selection = np.zeros(self.grid_cells.T.shape, dtype=np.int16)\n        if action == SomView.SelectionSet:\n            self.selection[:] = 0\n            self.selection[selection] = 1\n        elif action == SomView.SelectionAddToGroup:\n            self.selection[selection] = max(1, np.max(self.selection))\n        elif action == SomView.SelectionNewGroup:\n            self.selection[selection] = 1 + np.max(self.selection)\n        elif action & SomView.SelectionRemove:\n            self.selection[selection] = 0\n        self.redraw_selection()\n        self.update_output()\n\n    def on_selection_move(self, event: QKeyEvent):\n        if self.selection is None or not np.any(self.selection):\n            if event.key() in (Qt.Key_Right, Qt.Key_Down):\n                x = y = 0\n            else:\n                x = self.size_x - 1\n                y = self.size_y - 1\n        else:\n            x, y = np.nonzero(self.selection)\n            if len(x) > 1:\n                return\n            if event.key() == Qt.Key_Up and y > 0:\n                y -= 1\n            if event.key() == Qt.Key_Down and y < self.size_y - 1:\n                y += 1\n            if event.key() == Qt.Key_Left and x:\n                x -= 1\n            if event.key() == Qt.Key_Right and x < self.size_x - 1:\n                x += 1\n            x -= self.hexagonal and x == self.size_x - 1 and y % 2\n\n        if self.selection is not None and self.selection[x, y]:\n            return\n        selection = np.zeros(self.grid_cells.shape, dtype=bool)\n        selection[x, y] = True\n        self.on_selection_change(selection)\n\n    def on_selection_mark_change(self, marks):\n        self.redraw_selection(marks=marks)\n\n    def redraw_selection(self, marks=None):\n        if self.grid_cells is None:\n            return\n\n        sel_pen = QPen(QBrush(QColor(128, 128, 128)), 2)\n        sel_pen.setCosmetic(True)\n        mark_pen = QPen(QBrush(QColor(128, 128, 128)), 4)\n        mark_pen.setCosmetic(True)\n        pens = [self._grid_pen, sel_pen]\n\n        mark_brush = QBrush(QColor(224, 255, 255))\n        sels = self.selection is not None and np.max(self.selection)\n        palette = LimitedDiscretePalette(number_of_colors=sels + 1)\n        brushes = [QBrush(Qt.NoBrush)] + \\\n                  [QBrush(palette[i].lighter(165)) for i in range(sels)]\n\n        for y in range(self.size_y):\n            for x in range(self.size_x - (y % 2) * self.hexagonal):\n                cell = self.grid_cells[y, x]\n                marked = marks is not None and marks[x, y]\n                sel_group = self.selection is not None and self.selection[x, y]\n                if marked:\n                    cell.setBrush(mark_brush)\n                    cell.setPen(mark_pen)\n                else:\n                    cell.setBrush(brushes[sel_group])\n                    cell.setPen(pens[bool(sel_group)])\n                cell.setZValue(marked or sel_group)\n\n    def restart_som_pressed(self):\n        if self._optimizer_thread is not None:\n            self.stop_optimization = True\n            self._optimizer.stop_optimization = True\n        else:\n            self.start_som()\n\n    def start_som(self):\n        self.read_controls()\n        self.update_layout()\n        self.clear_selection()\n        if self.cont_x is not None:\n            self.enable_controls(False)\n            self._recompute_som()\n        else:\n            self.update_output()\n\n    def read_controls(self):\n        c = self.opt_controls\n        self.hexagonal = c.shape.currentIndex() == 0\n        self.size_x = c.spin_x.value()\n        self.size_y = c.spin_y.value()\n\n    def enable_controls(self, enable):\n        c = self.opt_controls\n        c.shape.setEnabled(enable)\n        c.auto_dim.setEnabled(enable)\n        c.start.setText(\"Start\" if enable else \"Stop\")\n\n    def update_layout(self):\n        self.set_legend_pos()\n        if self.elements:  # Prevent having redrawn grid but with old elements\n            self.scene.removeItem(self.elements)\n            self.elements = None\n        self.redraw_grid()\n        self.rescale()\n\n    def _redraw(self):\n        self.Warning.missing_colors.clear()\n        if self.elements:\n            self.scene.removeItem(self.elements)\n            self.elements = None\n        self.view.set_dimensions(self.size_x, self.size_y, self.hexagonal)\n\n        if self.cells is None:\n            return\n        sizes = self.cells[:, :, 1] - self.cells[:, :, 0]\n        sizes = sizes.astype(float)\n        if not self.size_by_instances:\n            sizes[sizes != 0] = 0.8\n        else:\n            sizes *= 0.8 / np.max(sizes)\n\n        self.elements = QGraphicsItemGroup()\n        self.scene.addItem(self.elements)\n        if self.attr_color is None:\n            self._draw_same_color(sizes)\n        elif self.pie_charts:\n            self._draw_pie_charts(sizes)\n        else:\n            self._draw_colored_circles(sizes)\n\n    @property\n    def _grid_factors(self):\n        return (0.5, sqrt3_2) if self.hexagonal else (0, 1)\n\n    def _draw_same_color(self, sizes):\n        fx, fy = self._grid_factors\n        color = QColor(64, 64, 64)\n        for y in range(self.size_y):\n            for x in range(self.size_x - self.hexagonal * (y % 2)):\n                r = sizes[x, y]\n                n = len(self.get_member_indices(x, y))\n                if not r:\n                    continue\n                ellipse = ColoredCircle(r / 2, color, 0)\n                ellipse.setPos(x + (y % 2) * fx, y * fy)\n                ellipse.setToolTip(f\"{n} instances\")\n                self.elements.addToGroup(ellipse)\n\n    def _get_color_column(self):\n        color_column = \\\n            self.data.get_column_view(self.attr_color)[0].astype(float,\n                                                                 copy=False)\n        if self.attr_color.is_discrete:\n            with np.errstate(invalid=\"ignore\"):\n                int_col = color_column.astype(int)\n            int_col[np.isnan(color_column)] = len(self.colors)\n        else:\n            int_col = np.zeros(len(color_column), dtype=int)\n            # The following line is unnecessary because rows with missing\n            # numeric data are excluded. Uncomment it if you change SOM to\n            # tolerate missing values.\n            # int_col[np.isnan(color_column)] = len(self.colors)\n            for i, thresh in enumerate(self.thresholds, start=1):\n                int_col[color_column >= thresh] = i\n        return int_col\n\n    def _tooltip(self, colors, distribution):\n        if self.attr_color.is_discrete:\n            values = self.attr_color.values\n        else:\n            values = self._bin_names()\n        tot = np.sum(distribution)\n        nbhp = \"\\N{NON-BREAKING HYPHEN}\"\n        return '<table style=\"white-space: nowrap\">' + \"\".join(f\"\"\"\n            <tr>\n                <td>\n                    <font color={color.name()}>■</font>\n                    <b>{escape(val).replace(\"-\", nbhp)}</b>:\n                </td>\n                <td>\n                    {n} ({n / tot * 100:.1f}&nbsp;%)\n                </td>\n            </tr>\n            \"\"\" for color, val, n in zip(colors, values, distribution) if n) \\\n            + \"</table>\"\n\n    def _draw_pie_charts(self, sizes):\n        fx, fy = self._grid_factors\n        color_column = self._get_color_column()\n        colors = self.colors.qcolors_w_nan\n        for y in range(self.size_y):\n            for x in range(self.size_x - self.hexagonal * (y % 2)):\n                r = sizes[x, y]\n                if not r:\n                    self.grid_cells[y, x].setToolTip(\"\")\n                    continue\n                members = self.get_member_indices(x, y)\n                color_dist = np.bincount(color_column[members],\n                                         minlength=len(colors))\n                rel_color_dist = color_dist.astype(float) / len(members)\n                pie = PieChart(rel_color_dist, r / 2, colors)\n                pie.setToolTip(self._tooltip(colors, color_dist))\n                self.elements.addToGroup(pie)\n                pie.setPos(x + (y % 2) * fx, y * fy)\n\n    def _draw_colored_circles(self, sizes):\n        fx, fy = self._grid_factors\n        color_column = self._get_color_column()\n        qcolors = self.colors.qcolors_w_nan\n        for y in range(self.size_y):\n            for x in range(self.size_x - self.hexagonal * (y % 2)):\n                r = sizes[x, y]\n                if not r:\n                    continue\n                members = self.get_member_indices(x, y)\n                color_dist = color_column[members]\n                color_dist = color_dist[color_dist < len(self.colors)]\n                if len(color_dist) != len(members):\n                    self.Warning.missing_colors(self.attr_color.name)\n                bc = np.bincount(color_dist, minlength=len(self.colors))\n                color = qcolors[np.argmax(bc)]\n                ellipse = ColoredCircle(r / 2, color, np.max(bc) / len(members))\n                ellipse.setPos(x + (y % 2) * fx, y * fy)\n                ellipse.setToolTip(self._tooltip(qcolors, bc))\n                self.elements.addToGroup(ellipse)\n\n    def redraw_grid(self):\n        if self.grid is not None:\n            self.scene.removeItem(self.grid)\n        self.grid = QGraphicsItemGroup()\n        self.grid.setZValue(-200)\n        self.grid_cells = np.full((self.size_y, self.size_x), None)\n        for y in range(self.size_y):\n            for x in range(self.size_x - (y % 2) * self.hexagonal):\n                if self.hexagonal:\n                    cell = QGraphicsPathItem(_hexagon_path)\n                    cell.setPos(x + (y % 2) / 2, y * sqrt3_2)\n                else:\n                    cell = QGraphicsRectItem(x - 0.5, y - 0.5, 1, 1)\n                self.grid_cells[y, x] = cell\n                cell.setPen(self._grid_pen)\n                self.grid.addToGroup(cell)\n        self.scene.addItem(self.grid)\n\n    def get_member_indices(self, x, y):\n        i, j = self.cells[x, y]\n        return self.member_data[i:j]\n\n    def _recompute_som(self):\n        if self.cont_x is None:\n            return\n\n        som = SOM(\n            self.size_x, self.size_y,\n            hexagonal=self.hexagonal,\n            pca_init=self.initialization == 0,\n            random_seed=0 if self.initialization == 2 else None\n        )\n\n        class Optimizer(QObject):\n            update = Signal(float, np.ndarray, np.ndarray)\n            done = Signal(SOM)\n            stopped = Signal()\n            stop_optimization = False\n\n            def __init__(self, data, som):\n                super().__init__()\n                self.som = som\n                self.data = data\n\n            def callback(self, progress):\n                self.update.emit(\n                    progress,\n                    self.som.weights.copy(), self.som.ssum_weights.copy())\n                return not self.stop_optimization\n\n            def run(self):\n                try:\n                    self.som.fit(self.data, N_ITERATIONS,\n                                 callback=self.callback)\n                    # Report an exception, but still remove the thread\n                finally:\n                    self.done.emit(self.som)\n                    self.stopped.emit()\n\n        def thread_finished():\n            self._optimizer = None\n            self._optimizer_thread = None\n\n        self.progressBarInit()\n\n        self._optimizer = Optimizer(self.cont_x, som)\n        self._optimizer_thread = QThread()\n        self._optimizer_thread.setStackSize(5 * 2 ** 20)\n        self._optimizer.update.connect(self.__update)\n        self._optimizer.done.connect(self.__done)\n        self._optimizer.stopped.connect(self._optimizer_thread.quit)\n        self._optimizer.moveToThread(self._optimizer_thread)\n        self._optimizer_thread.started.connect(self._optimizer.run)\n        self._optimizer_thread.finished.connect(thread_finished)\n        self.stop_optimization = False\n        self._optimizer_thread.start()\n\n    @Slot(float, object, object)\n    def __update(self, _progress, weights, ssum_weights):\n        self.progressBarSet(_progress)\n        self._assign_instances(weights, ssum_weights)\n        self._redraw()\n\n    @Slot(object)\n    def __done(self, som):\n        self.enable_controls(True)\n        self.progressBarFinished()\n        self._assign_instances(som.weights, som.ssum_weights)\n        self._redraw()\n        # This is the first time we know what was selected (assuming that\n        # initialization is not set to random)\n        if self.__pending_selection is not None:\n            self.on_selection_change(self.__pending_selection)\n            self.__pending_selection = None\n        self.update_output()\n\n    def stop_optimization_and_wait(self):\n        if self._optimizer_thread is not None:\n            self.stop_optimization = True\n            self._optimizer.stop_optimization = True\n            self._optimizer_thread.quit()\n            self._optimizer_thread.wait()\n            self._optimizer_thread = None\n\n    def onDeleteWidget(self):\n        self.stop_optimization_and_wait()\n        self.clear()\n        super().onDeleteWidget()\n\n    def _assign_instances(self, weights, ssum_weights):\n        if self.cont_x is None:\n            return  # the widget is shutting down while signals still processed\n        assignments = SOM.winner_from_weights(\n            self.cont_x, weights, ssum_weights, self.hexagonal)\n        members = defaultdict(list)\n        for i, (x, y) in enumerate(assignments):\n            members[(x, y)].append(i)\n        members.pop(None, None)\n        self.cells = np.empty((self.size_x, self.size_y, 2), dtype=int)\n        self.member_data = np.empty(self.cont_x.shape[0], dtype=int)\n        index = 0\n        for x in range(self.size_x):\n            for y in range(self.size_y):\n                nmembers = len(members[(x, y)])\n                self.member_data[index:index + nmembers] = members[(x, y)]\n                self.cells[x, y] = [index, index + nmembers]\n                index += nmembers\n\n    def resizeEvent(self, event):\n        super().resizeEvent(event)\n        self.create_legend()  # re-wrap lines if necessary\n        self.rescale()\n\n    def rescale(self):\n        if self.legend:\n            leg_height = self.legend.boundingRect().height()\n            leg_extra = 1.5\n        else:\n            leg_height = 0\n            leg_extra = 1\n\n        vw, vh = self.view.width(), self.view.height() - leg_height\n        scale = min(vw / (self.size_x + 1),\n                    vh / ((self.size_y + leg_extra) * self._grid_factors[1]))\n        self.view.setTransform(QTransform.fromScale(scale, scale))\n        if self.hexagonal:\n            self.view.setSceneRect(\n                0, -1, self.size_x - 1,\n                (self.size_y + leg_extra) * sqrt3_2 + leg_height / scale)\n        else:\n            self.view.setSceneRect(\n                -0.25, -0.25, self.size_x - 0.5,\n                self.size_y - 0.5 + leg_height / scale)\n\n    def update_output(self):\n        if self.data is None:\n            self.Outputs.selected_data.send(None)\n            self.Outputs.annotated_data.send(None)\n            return\n\n        indices = np.zeros(len(self.data), dtype=int)\n        if self.selection is not None and np.any(self.selection):\n            for y in range(self.size_y):\n                for x in range(self.size_x):\n                    rows = self.get_member_indices(x, y)\n                    indices[rows] = self.selection[x, y]\n\n        if np.any(indices):\n            sel_data = create_groups_table(self.data, indices, False, \"Group\")\n            self.Outputs.selected_data.send(sel_data)\n        else:\n            self.Outputs.selected_data.send(None)\n\n        if np.max(indices) > 1:\n            annotated = create_groups_table(self.data, indices)\n        else:\n            annotated = create_annotated_table(\n                self.data, np.flatnonzero(indices))\n        self.Outputs.annotated_data.send(annotated)\n\n    def set_color_bins(self):\n        if self.attr_color is None:\n            self.thresholds = self.bin_labels = self.colors = None\n        elif self.attr_color.is_discrete:\n            self.thresholds = self.bin_labels = None\n            self.colors = self.attr_color.palette\n        else:\n            col = self.data.get_column_view(self.attr_color)[0].astype(float)\n            if self.attr_color.is_time:\n                binning = time_binnings(col, min_bins=4)[-1]\n            else:\n                binning = decimal_binnings(col, min_bins=4)[-1]\n            self.thresholds = binning.thresholds[1:-1]\n            self.bin_labels = (binning.labels[1:-1], binning.short_labels[1:-1])\n            palette = BinnedContinuousPalette.from_palette(\n                self.attr_color.palette, binning.thresholds)\n            self.colors = palette\n\n    def create_legend(self):\n        if self.legend is not None:\n            self.scene.removeItem(self.legend)\n            self.legend = None\n        if self.attr_color is None:\n            return\n\n        if self.attr_color.is_discrete:\n            names = self.attr_color.values\n        else:\n            names = self._bin_names()\n\n        items = []\n        size = 8\n        for name, color in zip(names, self.colors.qcolors):\n            item = QGraphicsItemGroup()\n            item.addToGroup(\n                CanvasRectangle(None, -size / 2, -size / 2, size, size,\n                                Qt.gray, color))\n            item.addToGroup(CanvasText(None, name, size, 0, Qt.AlignVCenter))\n            items.append(item)\n\n        self.legend = wrap_legend_items(\n            items, hspacing=20, vspacing=16 + size,\n            max_width=self.view.width() - 25)\n        self.legend.setFlags(self.legend.ItemIgnoresTransformations)\n        self.legend.setTransform(\n            QTransform.fromTranslate(-self.legend.boundingRect().width() / 2,\n                                     0))\n        self.scene.addItem(self.legend)\n        self.set_legend_pos()\n\n    def _bin_names(self):\n        labels, short_labels = self.bin_labels\n        return \\\n            [f\"< {labels[0]}\"] \\\n            + [f\"{x} - {y}\" for x, y in zip(labels, short_labels[1:])] \\\n            + [f\"≥ {labels[-1]}\"]\n\n    def set_legend_pos(self):\n        if self.legend is None:\n            return\n        self.legend.setPos(\n            self.size_x / 2,\n            (self.size_y + 0.2 + 0.3 * self.hexagonal) * self._grid_factors[1])\n\n    def send_report(self):\n        self.report_plot()\n        if self.attr_color:\n            self.report_caption(\n                f\"Self-organizing map colored by '{self.attr_color.name}'\")",
                            "ast_type_str": "ClassDef",
                            "start_lineno": 359,
                            "end_lineno": 370,
                            "parent": {
                                "file_name": "Orange/widgets/unsupervised/owsom.py",
                                "type": "class",
                                "name": "OWSOM",
                                "content": "class OWSOM(OWWidget):\n    name = \"Self-Organizing Map\"\n    description = \"Computation of self-organizing map.\"\n    icon = \"icons/SOM.svg\"\n    keywords = [\"SOM\"]\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    settingsHandler = DomainContextHandler()\n    auto_dimension = Setting(True)\n    size_x = Setting(10)\n    size_y = Setting(10)\n    hexagonal = Setting(1)\n    initialization = Setting(0)\n\n    attr_color = ContextSetting(None)\n    size_by_instances = Setting(True)\n    pie_charts = Setting(False)\n    selection = Setting(None, schema_only=True)\n\n    graph_name = \"view\"\n\n    _grid_pen = QPen(QBrush(QColor(224, 224, 224)), 2)\n    _grid_pen.setCosmetic(True)\n\n    OptControls = namedtuple(\n        \"OptControls\",\n        (\"shape\", \"auto_dim\", \"spin_x\", \"spin_y\", \"initialization\", \"start\")\n    )\n\n    class Warning(OWWidget.Warning):\n        ignoring_disc_variables = Msg(\"SOM ignores categorical variables.\")\n        missing_colors = \\\n            Msg(\"Some data instances have undefined value of '{}'.\")\n        missing_values = \\\n            Msg(\"{} data instance{} with undefined value(s) {} not shown.\")\n        single_attribute = Msg(\"Data contains a single numeric column.\")\n\n    class Error(OWWidget.Error):\n        no_numeric_variables = Msg(\"Data contains no numeric columns.\")\n        no_defined_rows = Msg(\"All rows contain at least one undefined value.\")\n\n    def __init__(self):\n        super().__init__()\n        self.__pending_selection = self.selection\n        self._optimizer = None\n        self._optimizer_thread = None\n        self.stop_optimization = False\n\n        self.data = self.cont_x = None\n        self.cells = self.member_data = None\n        self.selection = None\n        self.colors = self.thresholds = self.bin_labels = None\n\n        box = gui.vBox(self.controlArea, box=\"SOM\")\n        shape = gui.comboBox(\n            box, self, \"\", items=(\"Hexagonal grid\", \"Square grid\"))\n        shape.setCurrentIndex(1 - self.hexagonal)\n\n        box2 = gui.indentedBox(box, 10)\n        auto_dim = gui.checkBox(\n            box2, self, \"auto_dimension\", \"Set dimensions automatically\",\n            callback=self.on_auto_dimension_changed)\n        self.manual_box = box3 = gui.hBox(box2)\n        spinargs = dict(\n            value=\"\", widget=box3, master=self, minv=5, maxv=100, step=5,\n            alignment=Qt.AlignRight)\n        spin_x = gui.spin(**spinargs)\n        spin_x.setValue(self.size_x)\n        gui.widgetLabel(box3, \"×\")\n        spin_y = gui.spin(**spinargs)\n        spin_y.setValue(self.size_y)\n        gui.rubber(box3)\n        self.manual_box.setEnabled(not self.auto_dimension)\n\n        initialization = gui.comboBox(\n            box, self, \"initialization\",\n            items=(\"Initialize with PCA\", \"Random initialization\",\n                   \"Replicable random\"))\n\n        start = gui.button(\n            box, self, \"Restart\", callback=self.restart_som_pressed,\n            sizePolicy=(QSizePolicy.MinimumExpanding, QSizePolicy.Fixed))\n\n        self.opt_controls = self.OptControls(\n            shape, auto_dim, spin_x, spin_y, initialization, start)\n\n        box = gui.vBox(self.controlArea, \"Color\")\n        gui.comboBox(\n            box, self, \"attr_color\", searchable=True,\n            callback=self.on_attr_color_change,\n            model=DomainModel(placeholder=\"(Same color)\",\n                              valid_types=DomainModel.PRIMITIVE))\n        gui.checkBox(\n            box, self, \"pie_charts\", label=\"Show pie charts\",\n            callback=self.on_pie_chart_change)\n        gui.checkBox(\n            box, self, \"size_by_instances\", label=\"Size by number of instances\",\n            callback=self.on_attr_size_change)\n\n        gui.rubber(self.controlArea)\n\n        self.scene = QGraphicsScene(self)\n\n        self.view = SomView(self.scene)\n        self.view.setMinimumWidth(400)\n        self.view.setMinimumHeight(400)\n        self.view.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)\n        self.view.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)\n        self.view.setRenderHint(QPainter.Antialiasing)\n        self.view.selection_changed.connect(self.on_selection_change)\n        self.view.selection_moved.connect(self.on_selection_move)\n        self.view.selection_mark_changed.connect(self.on_selection_mark_change)\n        self.mainArea.layout().addWidget(self.view)\n\n        self.elements = None\n        self.grid = None\n        self.grid_cells = None\n        self.legend = None\n\n    @Inputs.data\n    def set_data(self, data):\n        def prepare_data():\n            if len(cont_attrs) < len(attrs):\n                self.Warning.ignoring_disc_variables()\n            if len(cont_attrs) == 1:\n                self.Warning.single_attribute()\n            x = Table.from_table(Domain(cont_attrs), data).X\n            if sp.issparse(x):\n                self.data = data\n                self.cont_x = x.tocsr()\n            else:\n                mask = np.all(np.isfinite(x), axis=1)\n                if not np.any(mask):\n                    self.Error.no_defined_rows()\n                else:\n                    if np.all(mask):\n                        self.data = data\n                        self.cont_x = x.copy()\n                    else:\n                        self.data = data[mask]\n                        self.cont_x = x[mask]\n                    self.cont_x -= np.min(self.cont_x, axis=0)[None, :]\n                    sums = np.sum(self.cont_x, axis=0)[None, :]\n                    sums[sums == 0] = 1\n                    self.cont_x /= sums\n\n        def set_warnings():\n            missing = len(data) - len(self.data)\n            if missing == 1:\n                self.Warning.missing_values(1, \"\", \"is\")\n            elif missing > 1:\n                self.Warning.missing_values(missing, \"s\", \"are\")\n\n        self.stop_optimization_and_wait()\n\n        self.closeContext()\n        self.clear()\n        self.Error.clear()\n        self.Warning.clear()\n\n        if data is not None:\n            attrs = data.domain.attributes\n            cont_attrs = [var for var in attrs if var.is_continuous]\n            if not cont_attrs:\n                self.Error.no_numeric_variables()\n            else:\n                prepare_data()\n\n        if self.data is not None:\n            self.controls.attr_color.model().set_domain(data.domain)\n            self.attr_color = data.domain.class_var\n            set_warnings()\n\n        self.openContext(self.data)\n        self.set_color_bins()\n        self.create_legend()\n        self.recompute_dimensions()\n        self.start_som()\n\n    def clear(self):\n        self.data = self.cont_x = None\n        self.cells = self.member_data = None\n        self.attr_color = None\n        self.colors = self.thresholds = self.bin_labels = None\n        if self.elements is not None:\n            self.scene.removeItem(self.elements)\n            self.elements = None\n        self.clear_selection()\n        self.controls.attr_color.model().set_domain(None)\n        self.Warning.clear()\n        self.Error.clear()\n\n    def recompute_dimensions(self):\n        if not self.auto_dimension or self.cont_x is None:\n            return\n        dim = max(5, int(np.ceil(np.sqrt(5 * np.sqrt(self.cont_x.shape[0])))))\n        self.opt_controls.spin_x.setValue(dim)\n        self.opt_controls.spin_y.setValue(dim)\n\n    def on_auto_dimension_changed(self):\n        self.manual_box.setEnabled(not self.auto_dimension)\n        if self.auto_dimension:\n            self.recompute_dimensions()\n        else:\n            spin_x = self.opt_controls.spin_x\n            spin_y = self.opt_controls.spin_y\n            dimx = int(5 * np.round(spin_x.value() / 5))\n            dimy = int(5 * np.round(spin_y.value() / 5))\n            spin_x.setValue(dimx)\n            spin_y.setValue(dimy)\n\n    def on_attr_color_change(self):\n        self.controls.pie_charts.setEnabled(self.attr_color is not None)\n        self.set_color_bins()\n        self.create_legend()\n        self.rescale()\n        self._redraw()\n\n    def on_attr_size_change(self):\n        self._redraw()\n\n    def on_pie_chart_change(self):\n        self._redraw()\n\n    def clear_selection(self):\n        self.selection = None\n        self.redraw_selection()\n\n    def on_selection_change(self, selection, action=SomView.SelectionSet):\n        if self.data is None:  # clicks on empty canvas\n            return\n        if self.selection is None:\n            self.selection = np.zeros(self.grid_cells.T.shape, dtype=np.int16)\n        if action == SomView.SelectionSet:\n            self.selection[:] = 0\n            self.selection[selection] = 1\n        elif action == SomView.SelectionAddToGroup:\n            self.selection[selection] = max(1, np.max(self.selection))\n        elif action == SomView.SelectionNewGroup:\n            self.selection[selection] = 1 + np.max(self.selection)\n        elif action & SomView.SelectionRemove:\n            self.selection[selection] = 0\n        self.redraw_selection()\n        self.update_output()\n\n    def on_selection_move(self, event: QKeyEvent):\n        if self.selection is None or not np.any(self.selection):\n            if event.key() in (Qt.Key_Right, Qt.Key_Down):\n                x = y = 0\n            else:\n                x = self.size_x - 1\n                y = self.size_y - 1\n        else:\n            x, y = np.nonzero(self.selection)\n            if len(x) > 1:\n                return\n            if event.key() == Qt.Key_Up and y > 0:\n                y -= 1\n            if event.key() == Qt.Key_Down and y < self.size_y - 1:\n                y += 1\n            if event.key() == Qt.Key_Left and x:\n                x -= 1\n            if event.key() == Qt.Key_Right and x < self.size_x - 1:\n                x += 1\n            x -= self.hexagonal and x == self.size_x - 1 and y % 2\n\n        if self.selection is not None and self.selection[x, y]:\n            return\n        selection = np.zeros(self.grid_cells.shape, dtype=bool)\n        selection[x, y] = True\n        self.on_selection_change(selection)\n\n    def on_selection_mark_change(self, marks):\n        self.redraw_selection(marks=marks)\n\n    def redraw_selection(self, marks=None):\n        if self.grid_cells is None:\n            return\n\n        sel_pen = QPen(QBrush(QColor(128, 128, 128)), 2)\n        sel_pen.setCosmetic(True)\n        mark_pen = QPen(QBrush(QColor(128, 128, 128)), 4)\n        mark_pen.setCosmetic(True)\n        pens = [self._grid_pen, sel_pen]\n\n        mark_brush = QBrush(QColor(224, 255, 255))\n        sels = self.selection is not None and np.max(self.selection)\n        palette = LimitedDiscretePalette(number_of_colors=sels + 1)\n        brushes = [QBrush(Qt.NoBrush)] + \\\n                  [QBrush(palette[i].lighter(165)) for i in range(sels)]\n\n        for y in range(self.size_y):\n            for x in range(self.size_x - (y % 2) * self.hexagonal):\n                cell = self.grid_cells[y, x]\n                marked = marks is not None and marks[x, y]\n                sel_group = self.selection is not None and self.selection[x, y]\n                if marked:\n                    cell.setBrush(mark_brush)\n                    cell.setPen(mark_pen)\n                else:\n                    cell.setBrush(brushes[sel_group])\n                    cell.setPen(pens[bool(sel_group)])\n                cell.setZValue(marked or sel_group)\n\n    def restart_som_pressed(self):\n        if self._optimizer_thread is not None:\n            self.stop_optimization = True\n            self._optimizer.stop_optimization = True\n        else:\n            self.start_som()\n\n    def start_som(self):\n        self.read_controls()\n        self.update_layout()\n        self.clear_selection()\n        if self.cont_x is not None:\n            self.enable_controls(False)\n            self._recompute_som()\n        else:\n            self.update_output()\n\n    def read_controls(self):\n        c = self.opt_controls\n        self.hexagonal = c.shape.currentIndex() == 0\n        self.size_x = c.spin_x.value()\n        self.size_y = c.spin_y.value()\n\n    def enable_controls(self, enable):\n        c = self.opt_controls\n        c.shape.setEnabled(enable)\n        c.auto_dim.setEnabled(enable)\n        c.start.setText(\"Start\" if enable else \"Stop\")\n\n    def update_layout(self):\n        self.set_legend_pos()\n        if self.elements:  # Prevent having redrawn grid but with old elements\n            self.scene.removeItem(self.elements)\n            self.elements = None\n        self.redraw_grid()\n        self.rescale()\n\n    def _redraw(self):\n        self.Warning.missing_colors.clear()\n        if self.elements:\n            self.scene.removeItem(self.elements)\n            self.elements = None\n        self.view.set_dimensions(self.size_x, self.size_y, self.hexagonal)\n\n        if self.cells is None:\n            return\n        sizes = self.cells[:, :, 1] - self.cells[:, :, 0]\n        sizes = sizes.astype(float)\n        if not self.size_by_instances:\n            sizes[sizes != 0] = 0.8\n        else:\n            sizes *= 0.8 / np.max(sizes)\n\n        self.elements = QGraphicsItemGroup()\n        self.scene.addItem(self.elements)\n        if self.attr_color is None:\n            self._draw_same_color(sizes)\n        elif self.pie_charts:\n            self._draw_pie_charts(sizes)\n        else:\n            self._draw_colored_circles(sizes)\n\n    @property\n    def _grid_factors(self):\n        return (0.5, sqrt3_2) if self.hexagonal else (0, 1)\n\n    def _draw_same_color(self, sizes):\n        fx, fy = self._grid_factors\n        color = QColor(64, 64, 64)\n        for y in range(self.size_y):\n            for x in range(self.size_x - self.hexagonal * (y % 2)):\n                r = sizes[x, y]\n                n = len(self.get_member_indices(x, y))\n                if not r:\n                    continue\n                ellipse = ColoredCircle(r / 2, color, 0)\n                ellipse.setPos(x + (y % 2) * fx, y * fy)\n                ellipse.setToolTip(f\"{n} instances\")\n                self.elements.addToGroup(ellipse)\n\n    def _get_color_column(self):\n        color_column = \\\n            self.data.get_column_view(self.attr_color)[0].astype(float,\n                                                                 copy=False)\n        if self.attr_color.is_discrete:\n            with np.errstate(invalid=\"ignore\"):\n                int_col = color_column.astype(int)\n            int_col[np.isnan(color_column)] = len(self.colors)\n        else:\n            int_col = np.zeros(len(color_column), dtype=int)\n            # The following line is unnecessary because rows with missing\n            # numeric data are excluded. Uncomment it if you change SOM to\n            # tolerate missing values.\n            # int_col[np.isnan(color_column)] = len(self.colors)\n            for i, thresh in enumerate(self.thresholds, start=1):\n                int_col[color_column >= thresh] = i\n        return int_col\n\n    def _tooltip(self, colors, distribution):\n        if self.attr_color.is_discrete:\n            values = self.attr_color.values\n        else:\n            values = self._bin_names()\n        tot = np.sum(distribution)\n        nbhp = \"\\N{NON-BREAKING HYPHEN}\"\n        return '<table style=\"white-space: nowrap\">' + \"\".join(f\"\"\"\n            <tr>\n                <td>\n                    <font color={color.name()}>■</font>\n                    <b>{escape(val).replace(\"-\", nbhp)}</b>:\n                </td>\n                <td>\n                    {n} ({n / tot * 100:.1f}&nbsp;%)\n                </td>\n            </tr>\n            \"\"\" for color, val, n in zip(colors, values, distribution) if n) \\\n            + \"</table>\"\n\n    def _draw_pie_charts(self, sizes):\n        fx, fy = self._grid_factors\n        color_column = self._get_color_column()\n        colors = self.colors.qcolors_w_nan\n        for y in range(self.size_y):\n            for x in range(self.size_x - self.hexagonal * (y % 2)):\n                r = sizes[x, y]\n                if not r:\n                    self.grid_cells[y, x].setToolTip(\"\")\n                    continue\n                members = self.get_member_indices(x, y)\n                color_dist = np.bincount(color_column[members],\n                                         minlength=len(colors))\n                rel_color_dist = color_dist.astype(float) / len(members)\n                pie = PieChart(rel_color_dist, r / 2, colors)\n                pie.setToolTip(self._tooltip(colors, color_dist))\n                self.elements.addToGroup(pie)\n                pie.setPos(x + (y % 2) * fx, y * fy)\n\n    def _draw_colored_circles(self, sizes):\n        fx, fy = self._grid_factors\n        color_column = self._get_color_column()\n        qcolors = self.colors.qcolors_w_nan\n        for y in range(self.size_y):\n            for x in range(self.size_x - self.hexagonal * (y % 2)):\n                r = sizes[x, y]\n                if not r:\n                    continue\n                members = self.get_member_indices(x, y)\n                color_dist = color_column[members]\n                color_dist = color_dist[color_dist < len(self.colors)]\n                if len(color_dist) != len(members):\n                    self.Warning.missing_colors(self.attr_color.name)\n                bc = np.bincount(color_dist, minlength=len(self.colors))\n                color = qcolors[np.argmax(bc)]\n                ellipse = ColoredCircle(r / 2, color, np.max(bc) / len(members))\n                ellipse.setPos(x + (y % 2) * fx, y * fy)\n                ellipse.setToolTip(self._tooltip(qcolors, bc))\n                self.elements.addToGroup(ellipse)\n\n    def redraw_grid(self):\n        if self.grid is not None:\n            self.scene.removeItem(self.grid)\n        self.grid = QGraphicsItemGroup()\n        self.grid.setZValue(-200)\n        self.grid_cells = np.full((self.size_y, self.size_x), None)\n        for y in range(self.size_y):\n            for x in range(self.size_x - (y % 2) * self.hexagonal):\n                if self.hexagonal:\n                    cell = QGraphicsPathItem(_hexagon_path)\n                    cell.setPos(x + (y % 2) / 2, y * sqrt3_2)\n                else:\n                    cell = QGraphicsRectItem(x - 0.5, y - 0.5, 1, 1)\n                self.grid_cells[y, x] = cell\n                cell.setPen(self._grid_pen)\n                self.grid.addToGroup(cell)\n        self.scene.addItem(self.grid)\n\n    def get_member_indices(self, x, y):\n        i, j = self.cells[x, y]\n        return self.member_data[i:j]\n\n    def _recompute_som(self):\n        if self.cont_x is None:\n            return\n\n        som = SOM(\n            self.size_x, self.size_y,\n            hexagonal=self.hexagonal,\n            pca_init=self.initialization == 0,\n            random_seed=0 if self.initialization == 2 else None\n        )\n\n        class Optimizer(QObject):\n            update = Signal(float, np.ndarray, np.ndarray)\n            done = Signal(SOM)\n            stopped = Signal()\n            stop_optimization = False\n\n            def __init__(self, data, som):\n                super().__init__()\n                self.som = som\n                self.data = data\n\n            def callback(self, progress):\n                self.update.emit(\n                    progress,\n                    self.som.weights.copy(), self.som.ssum_weights.copy())\n                return not self.stop_optimization\n\n            def run(self):\n                try:\n                    self.som.fit(self.data, N_ITERATIONS,\n                                 callback=self.callback)\n                    # Report an exception, but still remove the thread\n                finally:\n                    self.done.emit(self.som)\n                    self.stopped.emit()\n\n        def thread_finished():\n            self._optimizer = None\n            self._optimizer_thread = None\n\n        self.progressBarInit()\n\n        self._optimizer = Optimizer(self.cont_x, som)\n        self._optimizer_thread = QThread()\n        self._optimizer_thread.setStackSize(5 * 2 ** 20)\n        self._optimizer.update.connect(self.__update)\n        self._optimizer.done.connect(self.__done)\n        self._optimizer.stopped.connect(self._optimizer_thread.quit)\n        self._optimizer.moveToThread(self._optimizer_thread)\n        self._optimizer_thread.started.connect(self._optimizer.run)\n        self._optimizer_thread.finished.connect(thread_finished)\n        self.stop_optimization = False\n        self._optimizer_thread.start()\n\n    @Slot(float, object, object)\n    def __update(self, _progress, weights, ssum_weights):\n        self.progressBarSet(_progress)\n        self._assign_instances(weights, ssum_weights)\n        self._redraw()\n\n    @Slot(object)\n    def __done(self, som):\n        self.enable_controls(True)\n        self.progressBarFinished()\n        self._assign_instances(som.weights, som.ssum_weights)\n        self._redraw()\n        # This is the first time we know what was selected (assuming that\n        # initialization is not set to random)\n        if self.__pending_selection is not None:\n            self.on_selection_change(self.__pending_selection)\n            self.__pending_selection = None\n        self.update_output()\n\n    def stop_optimization_and_wait(self):\n        if self._optimizer_thread is not None:\n            self.stop_optimization = True\n            self._optimizer.stop_optimization = True\n            self._optimizer_thread.quit()\n            self._optimizer_thread.wait()\n            self._optimizer_thread = None\n\n    def onDeleteWidget(self):\n        self.stop_optimization_and_wait()\n        self.clear()\n        super().onDeleteWidget()\n\n    def _assign_instances(self, weights, ssum_weights):\n        if self.cont_x is None:\n            return  # the widget is shutting down while signals still processed\n        assignments = SOM.winner_from_weights(\n            self.cont_x, weights, ssum_weights, self.hexagonal)\n        members = defaultdict(list)\n        for i, (x, y) in enumerate(assignments):\n            members[(x, y)].append(i)\n        members.pop(None, None)\n        self.cells = np.empty((self.size_x, self.size_y, 2), dtype=int)\n        self.member_data = np.empty(self.cont_x.shape[0], dtype=int)\n        index = 0\n        for x in range(self.size_x):\n            for y in range(self.size_y):\n                nmembers = len(members[(x, y)])\n                self.member_data[index:index + nmembers] = members[(x, y)]\n                self.cells[x, y] = [index, index + nmembers]\n                index += nmembers\n\n    def resizeEvent(self, event):\n        super().resizeEvent(event)\n        self.create_legend()  # re-wrap lines if necessary\n        self.rescale()\n\n    def rescale(self):\n        if self.legend:\n            leg_height = self.legend.boundingRect().height()\n            leg_extra = 1.5\n        else:\n            leg_height = 0\n            leg_extra = 1\n\n        vw, vh = self.view.width(), self.view.height() - leg_height\n        scale = min(vw / (self.size_x + 1),\n                    vh / ((self.size_y + leg_extra) * self._grid_factors[1]))\n        self.view.setTransform(QTransform.fromScale(scale, scale))\n        if self.hexagonal:\n            self.view.setSceneRect(\n                0, -1, self.size_x - 1,\n                (self.size_y + leg_extra) * sqrt3_2 + leg_height / scale)\n        else:\n            self.view.setSceneRect(\n                -0.25, -0.25, self.size_x - 0.5,\n                self.size_y - 0.5 + leg_height / scale)\n\n    def update_output(self):\n        if self.data is None:\n            self.Outputs.selected_data.send(None)\n            self.Outputs.annotated_data.send(None)\n            return\n\n        indices = np.zeros(len(self.data), dtype=int)\n        if self.selection is not None and np.any(self.selection):\n            for y in range(self.size_y):\n                for x in range(self.size_x):\n                    rows = self.get_member_indices(x, y)\n                    indices[rows] = self.selection[x, y]\n\n        if np.any(indices):\n            sel_data = create_groups_table(self.data, indices, False, \"Group\")\n            self.Outputs.selected_data.send(sel_data)\n        else:\n            self.Outputs.selected_data.send(None)\n\n        if np.max(indices) > 1:\n            annotated = create_groups_table(self.data, indices)\n        else:\n            annotated = create_annotated_table(\n                self.data, np.flatnonzero(indices))\n        self.Outputs.annotated_data.send(annotated)\n\n    def set_color_bins(self):\n        if self.attr_color is None:\n            self.thresholds = self.bin_labels = self.colors = None\n        elif self.attr_color.is_discrete:\n            self.thresholds = self.bin_labels = None\n            self.colors = self.attr_color.palette\n        else:\n            col = self.data.get_column_view(self.attr_color)[0].astype(float)\n            if self.attr_color.is_time:\n                binning = time_binnings(col, min_bins=4)[-1]\n            else:\n                binning = decimal_binnings(col, min_bins=4)[-1]\n            self.thresholds = binning.thresholds[1:-1]\n            self.bin_labels = (binning.labels[1:-1], binning.short_labels[1:-1])\n            palette = BinnedContinuousPalette.from_palette(\n                self.attr_color.palette, binning.thresholds)\n            self.colors = palette\n\n    def create_legend(self):\n        if self.legend is not None:\n            self.scene.removeItem(self.legend)\n            self.legend = None\n        if self.attr_color is None:\n            return\n\n        if self.attr_color.is_discrete:\n            names = self.attr_color.values\n        else:\n            names = self._bin_names()\n\n        items = []\n        size = 8\n        for name, color in zip(names, self.colors.qcolors):\n            item = QGraphicsItemGroup()\n            item.addToGroup(\n                CanvasRectangle(None, -size / 2, -size / 2, size, size,\n                                Qt.gray, color))\n            item.addToGroup(CanvasText(None, name, size, 0, Qt.AlignVCenter))\n            items.append(item)\n\n        self.legend = wrap_legend_items(\n            items, hspacing=20, vspacing=16 + size,\n            max_width=self.view.width() - 25)\n        self.legend.setFlags(self.legend.ItemIgnoresTransformations)\n        self.legend.setTransform(\n            QTransform.fromTranslate(-self.legend.boundingRect().width() / 2,\n                                     0))\n        self.scene.addItem(self.legend)\n        self.set_legend_pos()\n\n    def _bin_names(self):\n        labels, short_labels = self.bin_labels\n        return \\\n            [f\"< {labels[0]}\"] \\\n            + [f\"{x} - {y}\" for x, y in zip(labels, short_labels[1:])] \\\n            + [f\"≥ {labels[-1]}\"]\n\n    def set_legend_pos(self):\n        if self.legend is None:\n            return\n        self.legend.setPos(\n            self.size_x / 2,\n            (self.size_y + 0.2 + 0.3 * self.hexagonal) * self._grid_factors[1])\n\n    def send_report(self):\n        self.report_plot()\n        if self.attr_color:\n            self.report_caption(\n                f\"Self-organizing map colored by '{self.attr_color.name}'\")",
                                "ast_type_str": "ClassDef",
                                "start_lineno": 174,
                                "end_lineno": 890,
                                "parent": null
                            }
                        }
                    ],
                    "added_entities": [],
                    "deleted_entities": []
                },
                {
                    "descriptor": {
                        "old_range": {
                            "start": 366,
                            "length": 8
                        },
                        "new_range": {
                            "start": 373,
                            "length": 6
                        },
                        "section": "class OWSOM(OWWidget):"
                    },
                    "line_group": {
                        "all_lines": [
                            {
                                "content": "            self.elements = None",
                                "type": "context"
                            },
                            {
                                "content": "        self.clear_selection()",
                                "type": "context"
                            },
                            {
                                "content": "        self.controls.attr_color.model().set_domain(None)",
                                "type": "context"
                            },
                            {
                                "content": "        self.Warning.clear()",
                                "type": "deleted"
                            },
                            {
                                "content": "        self.Error.clear()",
                                "type": "deleted"
                            },
                            {
                                "content": "",
                                "type": "context"
                            },
                            {
                                "content": "    def recompute_dimensions(self):",
                                "type": "context"
                            },
                            {
                                "content": "        if not self.auto_dimension or self.cont_x is None:",
                                "type": "context"
                            }
                        ]
                    },
                    "modified_entities": [
                        {
                            "file_name": "Orange/widgets/unsupervised/owsom.py",
                            "type": "method",
                            "name": "OWSOM.clear",
                            "content": "class OWSOM(OWWidget):\n    name = \"Self-Organizing Map\"\n    description = \"Computation of self-organizing map.\"\n    icon = \"icons/SOM.svg\"\n    keywords = [\"SOM\"]\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    settingsHandler = DomainContextHandler()\n    auto_dimension = Setting(True)\n    size_x = Setting(10)\n    size_y = Setting(10)\n    hexagonal = Setting(1)\n    initialization = Setting(0)\n\n    attr_color = ContextSetting(None)\n    size_by_instances = Setting(True)\n    pie_charts = Setting(False)\n    selection = Setting(None, schema_only=True)\n\n    graph_name = \"view\"\n\n    _grid_pen = QPen(QBrush(QColor(224, 224, 224)), 2)\n    _grid_pen.setCosmetic(True)\n\n    OptControls = namedtuple(\n        \"OptControls\",\n        (\"shape\", \"auto_dim\", \"spin_x\", \"spin_y\", \"initialization\", \"start\")\n    )\n\n    class Warning(OWWidget.Warning):\n        ignoring_disc_variables = Msg(\"SOM ignores categorical variables.\")\n        missing_colors = \\\n            Msg(\"Some data instances have undefined value of '{}'.\")\n        missing_values = \\\n            Msg(\"{} data instance{} with undefined value(s) {} not shown.\")\n        single_attribute = Msg(\"Data contains a single numeric column.\")\n\n    class Error(OWWidget.Error):\n        no_numeric_variables = Msg(\"Data contains no numeric columns.\")\n        no_defined_rows = Msg(\"All rows contain at least one undefined value.\")\n\n    def __init__(self):\n        super().__init__()\n        self.__pending_selection = self.selection\n        self._optimizer = None\n        self._optimizer_thread = None\n        self.stop_optimization = False\n\n        self.data = self.cont_x = None\n        self.cells = self.member_data = None\n        self.selection = None\n        self.colors = self.thresholds = self.bin_labels = None\n\n        box = gui.vBox(self.controlArea, box=\"SOM\")\n        shape = gui.comboBox(\n            box, self, \"\", items=(\"Hexagonal grid\", \"Square grid\"))\n        shape.setCurrentIndex(1 - self.hexagonal)\n\n        box2 = gui.indentedBox(box, 10)\n        auto_dim = gui.checkBox(\n            box2, self, \"auto_dimension\", \"Set dimensions automatically\",\n            callback=self.on_auto_dimension_changed)\n        self.manual_box = box3 = gui.hBox(box2)\n        spinargs = dict(\n            value=\"\", widget=box3, master=self, minv=5, maxv=100, step=5,\n            alignment=Qt.AlignRight)\n        spin_x = gui.spin(**spinargs)\n        spin_x.setValue(self.size_x)\n        gui.widgetLabel(box3, \"×\")\n        spin_y = gui.spin(**spinargs)\n        spin_y.setValue(self.size_y)\n        gui.rubber(box3)\n        self.manual_box.setEnabled(not self.auto_dimension)\n\n        initialization = gui.comboBox(\n            box, self, \"initialization\",\n            items=(\"Initialize with PCA\", \"Random initialization\",\n                   \"Replicable random\"))\n\n        start = gui.button(\n            box, self, \"Restart\", callback=self.restart_som_pressed,\n            sizePolicy=(QSizePolicy.MinimumExpanding, QSizePolicy.Fixed))\n\n        self.opt_controls = self.OptControls(\n            shape, auto_dim, spin_x, spin_y, initialization, start)\n\n        box = gui.vBox(self.controlArea, \"Color\")\n        gui.comboBox(\n            box, self, \"attr_color\", searchable=True,\n            callback=self.on_attr_color_change,\n            model=DomainModel(placeholder=\"(Same color)\",\n                              valid_types=DomainModel.PRIMITIVE))\n        gui.checkBox(\n            box, self, \"pie_charts\", label=\"Show pie charts\",\n            callback=self.on_pie_chart_change)\n        gui.checkBox(\n            box, self, \"size_by_instances\", label=\"Size by number of instances\",\n            callback=self.on_attr_size_change)\n\n        gui.rubber(self.controlArea)\n\n        self.scene = QGraphicsScene(self)\n\n        self.view = SomView(self.scene)\n        self.view.setMinimumWidth(400)\n        self.view.setMinimumHeight(400)\n        self.view.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)\n        self.view.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)\n        self.view.setRenderHint(QPainter.Antialiasing)\n        self.view.selection_changed.connect(self.on_selection_change)\n        self.view.selection_moved.connect(self.on_selection_move)\n        self.view.selection_mark_changed.connect(self.on_selection_mark_change)\n        self.mainArea.layout().addWidget(self.view)\n\n        self.elements = None\n        self.grid = None\n        self.grid_cells = None\n        self.legend = None\n\n    @Inputs.data\n    def set_data(self, data):\n        def prepare_data():\n            if len(cont_attrs) < len(attrs):\n                self.Warning.ignoring_disc_variables()\n            if len(cont_attrs) == 1:\n                self.Warning.single_attribute()\n            x = Table.from_table(Domain(cont_attrs), data).X\n            if sp.issparse(x):\n                self.data = data\n                self.cont_x = x.tocsr()\n            else:\n                mask = np.all(np.isfinite(x), axis=1)\n                if not np.any(mask):\n                    self.Error.no_defined_rows()\n                else:\n                    if np.all(mask):\n                        self.data = data\n                        self.cont_x = x.copy()\n                    else:\n                        self.data = data[mask]\n                        self.cont_x = x[mask]\n                    self.cont_x -= np.min(self.cont_x, axis=0)[None, :]\n                    sums = np.sum(self.cont_x, axis=0)[None, :]\n                    sums[sums == 0] = 1\n                    self.cont_x /= sums\n\n        def set_warnings():\n            missing = len(data) - len(self.data)\n            if missing == 1:\n                self.Warning.missing_values(1, \"\", \"is\")\n            elif missing > 1:\n                self.Warning.missing_values(missing, \"s\", \"are\")\n\n        self.stop_optimization_and_wait()\n\n        self.closeContext()\n        self.clear()\n        self.Error.clear()\n        self.Warning.clear()\n\n        if data is not None:\n            attrs = data.domain.attributes\n            cont_attrs = [var for var in attrs if var.is_continuous]\n            if not cont_attrs:\n                self.Error.no_numeric_variables()\n            else:\n                prepare_data()\n\n        if self.data is not None:\n            self.controls.attr_color.model().set_domain(data.domain)\n            self.attr_color = data.domain.class_var\n            set_warnings()\n\n        self.openContext(self.data)\n        self.set_color_bins()\n        self.create_legend()\n        self.recompute_dimensions()\n        self.start_som()\n\n    def clear(self):\n        self.data = self.cont_x = None\n        self.cells = self.member_data = None\n        self.attr_color = None\n        self.colors = self.thresholds = self.bin_labels = None\n        if self.elements is not None:\n            self.scene.removeItem(self.elements)\n            self.elements = None\n        self.clear_selection()\n        self.controls.attr_color.model().set_domain(None)\n        self.Warning.clear()\n        self.Error.clear()\n\n    def recompute_dimensions(self):\n        if not self.auto_dimension or self.cont_x is None:\n            return\n        dim = max(5, int(np.ceil(np.sqrt(5 * np.sqrt(self.cont_x.shape[0])))))\n        self.opt_controls.spin_x.setValue(dim)\n        self.opt_controls.spin_y.setValue(dim)\n\n    def on_auto_dimension_changed(self):\n        self.manual_box.setEnabled(not self.auto_dimension)\n        if self.auto_dimension:\n            self.recompute_dimensions()\n        else:\n            spin_x = self.opt_controls.spin_x\n            spin_y = self.opt_controls.spin_y\n            dimx = int(5 * np.round(spin_x.value() / 5))\n            dimy = int(5 * np.round(spin_y.value() / 5))\n            spin_x.setValue(dimx)\n            spin_y.setValue(dimy)\n\n    def on_attr_color_change(self):\n        self.controls.pie_charts.setEnabled(self.attr_color is not None)\n        self.set_color_bins()\n        self.create_legend()\n        self.rescale()\n        self._redraw()\n\n    def on_attr_size_change(self):\n        self._redraw()\n\n    def on_pie_chart_change(self):\n        self._redraw()\n\n    def clear_selection(self):\n        self.selection = None\n        self.redraw_selection()\n\n    def on_selection_change(self, selection, action=SomView.SelectionSet):\n        if self.data is None:  # clicks on empty canvas\n            return\n        if self.selection is None:\n            self.selection = np.zeros(self.grid_cells.T.shape, dtype=np.int16)\n        if action == SomView.SelectionSet:\n            self.selection[:] = 0\n            self.selection[selection] = 1\n        elif action == SomView.SelectionAddToGroup:\n            self.selection[selection] = max(1, np.max(self.selection))\n        elif action == SomView.SelectionNewGroup:\n            self.selection[selection] = 1 + np.max(self.selection)\n        elif action & SomView.SelectionRemove:\n            self.selection[selection] = 0\n        self.redraw_selection()\n        self.update_output()\n\n    def on_selection_move(self, event: QKeyEvent):\n        if self.selection is None or not np.any(self.selection):\n            if event.key() in (Qt.Key_Right, Qt.Key_Down):\n                x = y = 0\n            else:\n                x = self.size_x - 1\n                y = self.size_y - 1\n        else:\n            x, y = np.nonzero(self.selection)\n            if len(x) > 1:\n                return\n            if event.key() == Qt.Key_Up and y > 0:\n                y -= 1\n            if event.key() == Qt.Key_Down and y < self.size_y - 1:\n                y += 1\n            if event.key() == Qt.Key_Left and x:\n                x -= 1\n            if event.key() == Qt.Key_Right and x < self.size_x - 1:\n                x += 1\n            x -= self.hexagonal and x == self.size_x - 1 and y % 2\n\n        if self.selection is not None and self.selection[x, y]:\n            return\n        selection = np.zeros(self.grid_cells.shape, dtype=bool)\n        selection[x, y] = True\n        self.on_selection_change(selection)\n\n    def on_selection_mark_change(self, marks):\n        self.redraw_selection(marks=marks)\n\n    def redraw_selection(self, marks=None):\n        if self.grid_cells is None:\n            return\n\n        sel_pen = QPen(QBrush(QColor(128, 128, 128)), 2)\n        sel_pen.setCosmetic(True)\n        mark_pen = QPen(QBrush(QColor(128, 128, 128)), 4)\n        mark_pen.setCosmetic(True)\n        pens = [self._grid_pen, sel_pen]\n\n        mark_brush = QBrush(QColor(224, 255, 255))\n        sels = self.selection is not None and np.max(self.selection)\n        palette = LimitedDiscretePalette(number_of_colors=sels + 1)\n        brushes = [QBrush(Qt.NoBrush)] + \\\n                  [QBrush(palette[i].lighter(165)) for i in range(sels)]\n\n        for y in range(self.size_y):\n            for x in range(self.size_x - (y % 2) * self.hexagonal):\n                cell = self.grid_cells[y, x]\n                marked = marks is not None and marks[x, y]\n                sel_group = self.selection is not None and self.selection[x, y]\n                if marked:\n                    cell.setBrush(mark_brush)\n                    cell.setPen(mark_pen)\n                else:\n                    cell.setBrush(brushes[sel_group])\n                    cell.setPen(pens[bool(sel_group)])\n                cell.setZValue(marked or sel_group)\n\n    def restart_som_pressed(self):\n        if self._optimizer_thread is not None:\n            self.stop_optimization = True\n            self._optimizer.stop_optimization = True\n        else:\n            self.start_som()\n\n    def start_som(self):\n        self.read_controls()\n        self.update_layout()\n        self.clear_selection()\n        if self.cont_x is not None:\n            self.enable_controls(False)\n            self._recompute_som()\n        else:\n            self.update_output()\n\n    def read_controls(self):\n        c = self.opt_controls\n        self.hexagonal = c.shape.currentIndex() == 0\n        self.size_x = c.spin_x.value()\n        self.size_y = c.spin_y.value()\n\n    def enable_controls(self, enable):\n        c = self.opt_controls\n        c.shape.setEnabled(enable)\n        c.auto_dim.setEnabled(enable)\n        c.start.setText(\"Start\" if enable else \"Stop\")\n\n    def update_layout(self):\n        self.set_legend_pos()\n        if self.elements:  # Prevent having redrawn grid but with old elements\n            self.scene.removeItem(self.elements)\n            self.elements = None\n        self.redraw_grid()\n        self.rescale()\n\n    def _redraw(self):\n        self.Warning.missing_colors.clear()\n        if self.elements:\n            self.scene.removeItem(self.elements)\n            self.elements = None\n        self.view.set_dimensions(self.size_x, self.size_y, self.hexagonal)\n\n        if self.cells is None:\n            return\n        sizes = self.cells[:, :, 1] - self.cells[:, :, 0]\n        sizes = sizes.astype(float)\n        if not self.size_by_instances:\n            sizes[sizes != 0] = 0.8\n        else:\n            sizes *= 0.8 / np.max(sizes)\n\n        self.elements = QGraphicsItemGroup()\n        self.scene.addItem(self.elements)\n        if self.attr_color is None:\n            self._draw_same_color(sizes)\n        elif self.pie_charts:\n            self._draw_pie_charts(sizes)\n        else:\n            self._draw_colored_circles(sizes)\n\n    @property\n    def _grid_factors(self):\n        return (0.5, sqrt3_2) if self.hexagonal else (0, 1)\n\n    def _draw_same_color(self, sizes):\n        fx, fy = self._grid_factors\n        color = QColor(64, 64, 64)\n        for y in range(self.size_y):\n            for x in range(self.size_x - self.hexagonal * (y % 2)):\n                r = sizes[x, y]\n                n = len(self.get_member_indices(x, y))\n                if not r:\n                    continue\n                ellipse = ColoredCircle(r / 2, color, 0)\n                ellipse.setPos(x + (y % 2) * fx, y * fy)\n                ellipse.setToolTip(f\"{n} instances\")\n                self.elements.addToGroup(ellipse)\n\n    def _get_color_column(self):\n        color_column = \\\n            self.data.get_column_view(self.attr_color)[0].astype(float,\n                                                                 copy=False)\n        if self.attr_color.is_discrete:\n            with np.errstate(invalid=\"ignore\"):\n                int_col = color_column.astype(int)\n            int_col[np.isnan(color_column)] = len(self.colors)\n        else:\n            int_col = np.zeros(len(color_column), dtype=int)\n            # The following line is unnecessary because rows with missing\n            # numeric data are excluded. Uncomment it if you change SOM to\n            # tolerate missing values.\n            # int_col[np.isnan(color_column)] = len(self.colors)\n            for i, thresh in enumerate(self.thresholds, start=1):\n                int_col[color_column >= thresh] = i\n        return int_col\n\n    def _tooltip(self, colors, distribution):\n        if self.attr_color.is_discrete:\n            values = self.attr_color.values\n        else:\n            values = self._bin_names()\n        tot = np.sum(distribution)\n        nbhp = \"\\N{NON-BREAKING HYPHEN}\"\n        return '<table style=\"white-space: nowrap\">' + \"\".join(f\"\"\"\n            <tr>\n                <td>\n                    <font color={color.name()}>■</font>\n                    <b>{escape(val).replace(\"-\", nbhp)}</b>:\n                </td>\n                <td>\n                    {n} ({n / tot * 100:.1f}&nbsp;%)\n                </td>\n            </tr>\n            \"\"\" for color, val, n in zip(colors, values, distribution) if n) \\\n            + \"</table>\"\n\n    def _draw_pie_charts(self, sizes):\n        fx, fy = self._grid_factors\n        color_column = self._get_color_column()\n        colors = self.colors.qcolors_w_nan\n        for y in range(self.size_y):\n            for x in range(self.size_x - self.hexagonal * (y % 2)):\n                r = sizes[x, y]\n                if not r:\n                    self.grid_cells[y, x].setToolTip(\"\")\n                    continue\n                members = self.get_member_indices(x, y)\n                color_dist = np.bincount(color_column[members],\n                                         minlength=len(colors))\n                rel_color_dist = color_dist.astype(float) / len(members)\n                pie = PieChart(rel_color_dist, r / 2, colors)\n                pie.setToolTip(self._tooltip(colors, color_dist))\n                self.elements.addToGroup(pie)\n                pie.setPos(x + (y % 2) * fx, y * fy)\n\n    def _draw_colored_circles(self, sizes):\n        fx, fy = self._grid_factors\n        color_column = self._get_color_column()\n        qcolors = self.colors.qcolors_w_nan\n        for y in range(self.size_y):\n            for x in range(self.size_x - self.hexagonal * (y % 2)):\n                r = sizes[x, y]\n                if not r:\n                    continue\n                members = self.get_member_indices(x, y)\n                color_dist = color_column[members]\n                color_dist = color_dist[color_dist < len(self.colors)]\n                if len(color_dist) != len(members):\n                    self.Warning.missing_colors(self.attr_color.name)\n                bc = np.bincount(color_dist, minlength=len(self.colors))\n                color = qcolors[np.argmax(bc)]\n                ellipse = ColoredCircle(r / 2, color, np.max(bc) / len(members))\n                ellipse.setPos(x + (y % 2) * fx, y * fy)\n                ellipse.setToolTip(self._tooltip(qcolors, bc))\n                self.elements.addToGroup(ellipse)\n\n    def redraw_grid(self):\n        if self.grid is not None:\n            self.scene.removeItem(self.grid)\n        self.grid = QGraphicsItemGroup()\n        self.grid.setZValue(-200)\n        self.grid_cells = np.full((self.size_y, self.size_x), None)\n        for y in range(self.size_y):\n            for x in range(self.size_x - (y % 2) * self.hexagonal):\n                if self.hexagonal:\n                    cell = QGraphicsPathItem(_hexagon_path)\n                    cell.setPos(x + (y % 2) / 2, y * sqrt3_2)\n                else:\n                    cell = QGraphicsRectItem(x - 0.5, y - 0.5, 1, 1)\n                self.grid_cells[y, x] = cell\n                cell.setPen(self._grid_pen)\n                self.grid.addToGroup(cell)\n        self.scene.addItem(self.grid)\n\n    def get_member_indices(self, x, y):\n        i, j = self.cells[x, y]\n        return self.member_data[i:j]\n\n    def _recompute_som(self):\n        if self.cont_x is None:\n            return\n\n        som = SOM(\n            self.size_x, self.size_y,\n            hexagonal=self.hexagonal,\n            pca_init=self.initialization == 0,\n            random_seed=0 if self.initialization == 2 else None\n        )\n\n        class Optimizer(QObject):\n            update = Signal(float, np.ndarray, np.ndarray)\n            done = Signal(SOM)\n            stopped = Signal()\n            stop_optimization = False\n\n            def __init__(self, data, som):\n                super().__init__()\n                self.som = som\n                self.data = data\n\n            def callback(self, progress):\n                self.update.emit(\n                    progress,\n                    self.som.weights.copy(), self.som.ssum_weights.copy())\n                return not self.stop_optimization\n\n            def run(self):\n                try:\n                    self.som.fit(self.data, N_ITERATIONS,\n                                 callback=self.callback)\n                    # Report an exception, but still remove the thread\n                finally:\n                    self.done.emit(self.som)\n                    self.stopped.emit()\n\n        def thread_finished():\n            self._optimizer = None\n            self._optimizer_thread = None\n\n        self.progressBarInit()\n\n        self._optimizer = Optimizer(self.cont_x, som)\n        self._optimizer_thread = QThread()\n        self._optimizer_thread.setStackSize(5 * 2 ** 20)\n        self._optimizer.update.connect(self.__update)\n        self._optimizer.done.connect(self.__done)\n        self._optimizer.stopped.connect(self._optimizer_thread.quit)\n        self._optimizer.moveToThread(self._optimizer_thread)\n        self._optimizer_thread.started.connect(self._optimizer.run)\n        self._optimizer_thread.finished.connect(thread_finished)\n        self.stop_optimization = False\n        self._optimizer_thread.start()\n\n    @Slot(float, object, object)\n    def __update(self, _progress, weights, ssum_weights):\n        self.progressBarSet(_progress)\n        self._assign_instances(weights, ssum_weights)\n        self._redraw()\n\n    @Slot(object)\n    def __done(self, som):\n        self.enable_controls(True)\n        self.progressBarFinished()\n        self._assign_instances(som.weights, som.ssum_weights)\n        self._redraw()\n        # This is the first time we know what was selected (assuming that\n        # initialization is not set to random)\n        if self.__pending_selection is not None:\n            self.on_selection_change(self.__pending_selection)\n            self.__pending_selection = None\n        self.update_output()\n\n    def stop_optimization_and_wait(self):\n        if self._optimizer_thread is not None:\n            self.stop_optimization = True\n            self._optimizer.stop_optimization = True\n            self._optimizer_thread.quit()\n            self._optimizer_thread.wait()\n            self._optimizer_thread = None\n\n    def onDeleteWidget(self):\n        self.stop_optimization_and_wait()\n        self.clear()\n        super().onDeleteWidget()\n\n    def _assign_instances(self, weights, ssum_weights):\n        if self.cont_x is None:\n            return  # the widget is shutting down while signals still processed\n        assignments = SOM.winner_from_weights(\n            self.cont_x, weights, ssum_weights, self.hexagonal)\n        members = defaultdict(list)\n        for i, (x, y) in enumerate(assignments):\n            members[(x, y)].append(i)\n        members.pop(None, None)\n        self.cells = np.empty((self.size_x, self.size_y, 2), dtype=int)\n        self.member_data = np.empty(self.cont_x.shape[0], dtype=int)\n        index = 0\n        for x in range(self.size_x):\n            for y in range(self.size_y):\n                nmembers = len(members[(x, y)])\n                self.member_data[index:index + nmembers] = members[(x, y)]\n                self.cells[x, y] = [index, index + nmembers]\n                index += nmembers\n\n    def resizeEvent(self, event):\n        super().resizeEvent(event)\n        self.create_legend()  # re-wrap lines if necessary\n        self.rescale()\n\n    def rescale(self):\n        if self.legend:\n            leg_height = self.legend.boundingRect().height()\n            leg_extra = 1.5\n        else:\n            leg_height = 0\n            leg_extra = 1\n\n        vw, vh = self.view.width(), self.view.height() - leg_height\n        scale = min(vw / (self.size_x + 1),\n                    vh / ((self.size_y + leg_extra) * self._grid_factors[1]))\n        self.view.setTransform(QTransform.fromScale(scale, scale))\n        if self.hexagonal:\n            self.view.setSceneRect(\n                0, -1, self.size_x - 1,\n                (self.size_y + leg_extra) * sqrt3_2 + leg_height / scale)\n        else:\n            self.view.setSceneRect(\n                -0.25, -0.25, self.size_x - 0.5,\n                self.size_y - 0.5 + leg_height / scale)\n\n    def update_output(self):\n        if self.data is None:\n            self.Outputs.selected_data.send(None)\n            self.Outputs.annotated_data.send(None)\n            return\n\n        indices = np.zeros(len(self.data), dtype=int)\n        if self.selection is not None and np.any(self.selection):\n            for y in range(self.size_y):\n                for x in range(self.size_x):\n                    rows = self.get_member_indices(x, y)\n                    indices[rows] = self.selection[x, y]\n\n        if np.any(indices):\n            sel_data = create_groups_table(self.data, indices, False, \"Group\")\n            self.Outputs.selected_data.send(sel_data)\n        else:\n            self.Outputs.selected_data.send(None)\n\n        if np.max(indices) > 1:\n            annotated = create_groups_table(self.data, indices)\n        else:\n            annotated = create_annotated_table(\n                self.data, np.flatnonzero(indices))\n        self.Outputs.annotated_data.send(annotated)\n\n    def set_color_bins(self):\n        if self.attr_color is None:\n            self.thresholds = self.bin_labels = self.colors = None\n        elif self.attr_color.is_discrete:\n            self.thresholds = self.bin_labels = None\n            self.colors = self.attr_color.palette\n        else:\n            col = self.data.get_column_view(self.attr_color)[0].astype(float)\n            if self.attr_color.is_time:\n                binning = time_binnings(col, min_bins=4)[-1]\n            else:\n                binning = decimal_binnings(col, min_bins=4)[-1]\n            self.thresholds = binning.thresholds[1:-1]\n            self.bin_labels = (binning.labels[1:-1], binning.short_labels[1:-1])\n            palette = BinnedContinuousPalette.from_palette(\n                self.attr_color.palette, binning.thresholds)\n            self.colors = palette\n\n    def create_legend(self):\n        if self.legend is not None:\n            self.scene.removeItem(self.legend)\n            self.legend = None\n        if self.attr_color is None:\n            return\n\n        if self.attr_color.is_discrete:\n            names = self.attr_color.values\n        else:\n            names = self._bin_names()\n\n        items = []\n        size = 8\n        for name, color in zip(names, self.colors.qcolors):\n            item = QGraphicsItemGroup()\n            item.addToGroup(\n                CanvasRectangle(None, -size / 2, -size / 2, size, size,\n                                Qt.gray, color))\n            item.addToGroup(CanvasText(None, name, size, 0, Qt.AlignVCenter))\n            items.append(item)\n\n        self.legend = wrap_legend_items(\n            items, hspacing=20, vspacing=16 + size,\n            max_width=self.view.width() - 25)\n        self.legend.setFlags(self.legend.ItemIgnoresTransformations)\n        self.legend.setTransform(\n            QTransform.fromTranslate(-self.legend.boundingRect().width() / 2,\n                                     0))\n        self.scene.addItem(self.legend)\n        self.set_legend_pos()\n\n    def _bin_names(self):\n        labels, short_labels = self.bin_labels\n        return \\\n            [f\"< {labels[0]}\"] \\\n            + [f\"{x} - {y}\" for x, y in zip(labels, short_labels[1:])] \\\n            + [f\"≥ {labels[-1]}\"]\n\n    def set_legend_pos(self):\n        if self.legend is None:\n            return\n        self.legend.setPos(\n            self.size_x / 2,\n            (self.size_y + 0.2 + 0.3 * self.hexagonal) * self._grid_factors[1])\n\n    def send_report(self):\n        self.report_plot()\n        if self.attr_color:\n            self.report_caption(\n                f\"Self-organizing map colored by '{self.attr_color.name}'\")",
                            "ast_type_str": "ClassDef",
                            "start_lineno": 359,
                            "end_lineno": 370,
                            "parent": {
                                "file_name": "Orange/widgets/unsupervised/owsom.py",
                                "type": "class",
                                "name": "OWSOM",
                                "content": "class OWSOM(OWWidget):\n    name = \"Self-Organizing Map\"\n    description = \"Computation of self-organizing map.\"\n    icon = \"icons/SOM.svg\"\n    keywords = [\"SOM\"]\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    settingsHandler = DomainContextHandler()\n    auto_dimension = Setting(True)\n    size_x = Setting(10)\n    size_y = Setting(10)\n    hexagonal = Setting(1)\n    initialization = Setting(0)\n\n    attr_color = ContextSetting(None)\n    size_by_instances = Setting(True)\n    pie_charts = Setting(False)\n    selection = Setting(None, schema_only=True)\n\n    graph_name = \"view\"\n\n    _grid_pen = QPen(QBrush(QColor(224, 224, 224)), 2)\n    _grid_pen.setCosmetic(True)\n\n    OptControls = namedtuple(\n        \"OptControls\",\n        (\"shape\", \"auto_dim\", \"spin_x\", \"spin_y\", \"initialization\", \"start\")\n    )\n\n    class Warning(OWWidget.Warning):\n        ignoring_disc_variables = Msg(\"SOM ignores categorical variables.\")\n        missing_colors = \\\n            Msg(\"Some data instances have undefined value of '{}'.\")\n        missing_values = \\\n            Msg(\"{} data instance{} with undefined value(s) {} not shown.\")\n        single_attribute = Msg(\"Data contains a single numeric column.\")\n\n    class Error(OWWidget.Error):\n        no_numeric_variables = Msg(\"Data contains no numeric columns.\")\n        no_defined_rows = Msg(\"All rows contain at least one undefined value.\")\n\n    def __init__(self):\n        super().__init__()\n        self.__pending_selection = self.selection\n        self._optimizer = None\n        self._optimizer_thread = None\n        self.stop_optimization = False\n\n        self.data = self.cont_x = None\n        self.cells = self.member_data = None\n        self.selection = None\n        self.colors = self.thresholds = self.bin_labels = None\n\n        box = gui.vBox(self.controlArea, box=\"SOM\")\n        shape = gui.comboBox(\n            box, self, \"\", items=(\"Hexagonal grid\", \"Square grid\"))\n        shape.setCurrentIndex(1 - self.hexagonal)\n\n        box2 = gui.indentedBox(box, 10)\n        auto_dim = gui.checkBox(\n            box2, self, \"auto_dimension\", \"Set dimensions automatically\",\n            callback=self.on_auto_dimension_changed)\n        self.manual_box = box3 = gui.hBox(box2)\n        spinargs = dict(\n            value=\"\", widget=box3, master=self, minv=5, maxv=100, step=5,\n            alignment=Qt.AlignRight)\n        spin_x = gui.spin(**spinargs)\n        spin_x.setValue(self.size_x)\n        gui.widgetLabel(box3, \"×\")\n        spin_y = gui.spin(**spinargs)\n        spin_y.setValue(self.size_y)\n        gui.rubber(box3)\n        self.manual_box.setEnabled(not self.auto_dimension)\n\n        initialization = gui.comboBox(\n            box, self, \"initialization\",\n            items=(\"Initialize with PCA\", \"Random initialization\",\n                   \"Replicable random\"))\n\n        start = gui.button(\n            box, self, \"Restart\", callback=self.restart_som_pressed,\n            sizePolicy=(QSizePolicy.MinimumExpanding, QSizePolicy.Fixed))\n\n        self.opt_controls = self.OptControls(\n            shape, auto_dim, spin_x, spin_y, initialization, start)\n\n        box = gui.vBox(self.controlArea, \"Color\")\n        gui.comboBox(\n            box, self, \"attr_color\", searchable=True,\n            callback=self.on_attr_color_change,\n            model=DomainModel(placeholder=\"(Same color)\",\n                              valid_types=DomainModel.PRIMITIVE))\n        gui.checkBox(\n            box, self, \"pie_charts\", label=\"Show pie charts\",\n            callback=self.on_pie_chart_change)\n        gui.checkBox(\n            box, self, \"size_by_instances\", label=\"Size by number of instances\",\n            callback=self.on_attr_size_change)\n\n        gui.rubber(self.controlArea)\n\n        self.scene = QGraphicsScene(self)\n\n        self.view = SomView(self.scene)\n        self.view.setMinimumWidth(400)\n        self.view.setMinimumHeight(400)\n        self.view.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)\n        self.view.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)\n        self.view.setRenderHint(QPainter.Antialiasing)\n        self.view.selection_changed.connect(self.on_selection_change)\n        self.view.selection_moved.connect(self.on_selection_move)\n        self.view.selection_mark_changed.connect(self.on_selection_mark_change)\n        self.mainArea.layout().addWidget(self.view)\n\n        self.elements = None\n        self.grid = None\n        self.grid_cells = None\n        self.legend = None\n\n    @Inputs.data\n    def set_data(self, data):\n        def prepare_data():\n            if len(cont_attrs) < len(attrs):\n                self.Warning.ignoring_disc_variables()\n            if len(cont_attrs) == 1:\n                self.Warning.single_attribute()\n            x = Table.from_table(Domain(cont_attrs), data).X\n            if sp.issparse(x):\n                self.data = data\n                self.cont_x = x.tocsr()\n            else:\n                mask = np.all(np.isfinite(x), axis=1)\n                if not np.any(mask):\n                    self.Error.no_defined_rows()\n                else:\n                    if np.all(mask):\n                        self.data = data\n                        self.cont_x = x.copy()\n                    else:\n                        self.data = data[mask]\n                        self.cont_x = x[mask]\n                    self.cont_x -= np.min(self.cont_x, axis=0)[None, :]\n                    sums = np.sum(self.cont_x, axis=0)[None, :]\n                    sums[sums == 0] = 1\n                    self.cont_x /= sums\n\n        def set_warnings():\n            missing = len(data) - len(self.data)\n            if missing == 1:\n                self.Warning.missing_values(1, \"\", \"is\")\n            elif missing > 1:\n                self.Warning.missing_values(missing, \"s\", \"are\")\n\n        self.stop_optimization_and_wait()\n\n        self.closeContext()\n        self.clear()\n        self.Error.clear()\n        self.Warning.clear()\n\n        if data is not None:\n            attrs = data.domain.attributes\n            cont_attrs = [var for var in attrs if var.is_continuous]\n            if not cont_attrs:\n                self.Error.no_numeric_variables()\n            else:\n                prepare_data()\n\n        if self.data is not None:\n            self.controls.attr_color.model().set_domain(data.domain)\n            self.attr_color = data.domain.class_var\n            set_warnings()\n\n        self.openContext(self.data)\n        self.set_color_bins()\n        self.create_legend()\n        self.recompute_dimensions()\n        self.start_som()\n\n    def clear(self):\n        self.data = self.cont_x = None\n        self.cells = self.member_data = None\n        self.attr_color = None\n        self.colors = self.thresholds = self.bin_labels = None\n        if self.elements is not None:\n            self.scene.removeItem(self.elements)\n            self.elements = None\n        self.clear_selection()\n        self.controls.attr_color.model().set_domain(None)\n        self.Warning.clear()\n        self.Error.clear()\n\n    def recompute_dimensions(self):\n        if not self.auto_dimension or self.cont_x is None:\n            return\n        dim = max(5, int(np.ceil(np.sqrt(5 * np.sqrt(self.cont_x.shape[0])))))\n        self.opt_controls.spin_x.setValue(dim)\n        self.opt_controls.spin_y.setValue(dim)\n\n    def on_auto_dimension_changed(self):\n        self.manual_box.setEnabled(not self.auto_dimension)\n        if self.auto_dimension:\n            self.recompute_dimensions()\n        else:\n            spin_x = self.opt_controls.spin_x\n            spin_y = self.opt_controls.spin_y\n            dimx = int(5 * np.round(spin_x.value() / 5))\n            dimy = int(5 * np.round(spin_y.value() / 5))\n            spin_x.setValue(dimx)\n            spin_y.setValue(dimy)\n\n    def on_attr_color_change(self):\n        self.controls.pie_charts.setEnabled(self.attr_color is not None)\n        self.set_color_bins()\n        self.create_legend()\n        self.rescale()\n        self._redraw()\n\n    def on_attr_size_change(self):\n        self._redraw()\n\n    def on_pie_chart_change(self):\n        self._redraw()\n\n    def clear_selection(self):\n        self.selection = None\n        self.redraw_selection()\n\n    def on_selection_change(self, selection, action=SomView.SelectionSet):\n        if self.data is None:  # clicks on empty canvas\n            return\n        if self.selection is None:\n            self.selection = np.zeros(self.grid_cells.T.shape, dtype=np.int16)\n        if action == SomView.SelectionSet:\n            self.selection[:] = 0\n            self.selection[selection] = 1\n        elif action == SomView.SelectionAddToGroup:\n            self.selection[selection] = max(1, np.max(self.selection))\n        elif action == SomView.SelectionNewGroup:\n            self.selection[selection] = 1 + np.max(self.selection)\n        elif action & SomView.SelectionRemove:\n            self.selection[selection] = 0\n        self.redraw_selection()\n        self.update_output()\n\n    def on_selection_move(self, event: QKeyEvent):\n        if self.selection is None or not np.any(self.selection):\n            if event.key() in (Qt.Key_Right, Qt.Key_Down):\n                x = y = 0\n            else:\n                x = self.size_x - 1\n                y = self.size_y - 1\n        else:\n            x, y = np.nonzero(self.selection)\n            if len(x) > 1:\n                return\n            if event.key() == Qt.Key_Up and y > 0:\n                y -= 1\n            if event.key() == Qt.Key_Down and y < self.size_y - 1:\n                y += 1\n            if event.key() == Qt.Key_Left and x:\n                x -= 1\n            if event.key() == Qt.Key_Right and x < self.size_x - 1:\n                x += 1\n            x -= self.hexagonal and x == self.size_x - 1 and y % 2\n\n        if self.selection is not None and self.selection[x, y]:\n            return\n        selection = np.zeros(self.grid_cells.shape, dtype=bool)\n        selection[x, y] = True\n        self.on_selection_change(selection)\n\n    def on_selection_mark_change(self, marks):\n        self.redraw_selection(marks=marks)\n\n    def redraw_selection(self, marks=None):\n        if self.grid_cells is None:\n            return\n\n        sel_pen = QPen(QBrush(QColor(128, 128, 128)), 2)\n        sel_pen.setCosmetic(True)\n        mark_pen = QPen(QBrush(QColor(128, 128, 128)), 4)\n        mark_pen.setCosmetic(True)\n        pens = [self._grid_pen, sel_pen]\n\n        mark_brush = QBrush(QColor(224, 255, 255))\n        sels = self.selection is not None and np.max(self.selection)\n        palette = LimitedDiscretePalette(number_of_colors=sels + 1)\n        brushes = [QBrush(Qt.NoBrush)] + \\\n                  [QBrush(palette[i].lighter(165)) for i in range(sels)]\n\n        for y in range(self.size_y):\n            for x in range(self.size_x - (y % 2) * self.hexagonal):\n                cell = self.grid_cells[y, x]\n                marked = marks is not None and marks[x, y]\n                sel_group = self.selection is not None and self.selection[x, y]\n                if marked:\n                    cell.setBrush(mark_brush)\n                    cell.setPen(mark_pen)\n                else:\n                    cell.setBrush(brushes[sel_group])\n                    cell.setPen(pens[bool(sel_group)])\n                cell.setZValue(marked or sel_group)\n\n    def restart_som_pressed(self):\n        if self._optimizer_thread is not None:\n            self.stop_optimization = True\n            self._optimizer.stop_optimization = True\n        else:\n            self.start_som()\n\n    def start_som(self):\n        self.read_controls()\n        self.update_layout()\n        self.clear_selection()\n        if self.cont_x is not None:\n            self.enable_controls(False)\n            self._recompute_som()\n        else:\n            self.update_output()\n\n    def read_controls(self):\n        c = self.opt_controls\n        self.hexagonal = c.shape.currentIndex() == 0\n        self.size_x = c.spin_x.value()\n        self.size_y = c.spin_y.value()\n\n    def enable_controls(self, enable):\n        c = self.opt_controls\n        c.shape.setEnabled(enable)\n        c.auto_dim.setEnabled(enable)\n        c.start.setText(\"Start\" if enable else \"Stop\")\n\n    def update_layout(self):\n        self.set_legend_pos()\n        if self.elements:  # Prevent having redrawn grid but with old elements\n            self.scene.removeItem(self.elements)\n            self.elements = None\n        self.redraw_grid()\n        self.rescale()\n\n    def _redraw(self):\n        self.Warning.missing_colors.clear()\n        if self.elements:\n            self.scene.removeItem(self.elements)\n            self.elements = None\n        self.view.set_dimensions(self.size_x, self.size_y, self.hexagonal)\n\n        if self.cells is None:\n            return\n        sizes = self.cells[:, :, 1] - self.cells[:, :, 0]\n        sizes = sizes.astype(float)\n        if not self.size_by_instances:\n            sizes[sizes != 0] = 0.8\n        else:\n            sizes *= 0.8 / np.max(sizes)\n\n        self.elements = QGraphicsItemGroup()\n        self.scene.addItem(self.elements)\n        if self.attr_color is None:\n            self._draw_same_color(sizes)\n        elif self.pie_charts:\n            self._draw_pie_charts(sizes)\n        else:\n            self._draw_colored_circles(sizes)\n\n    @property\n    def _grid_factors(self):\n        return (0.5, sqrt3_2) if self.hexagonal else (0, 1)\n\n    def _draw_same_color(self, sizes):\n        fx, fy = self._grid_factors\n        color = QColor(64, 64, 64)\n        for y in range(self.size_y):\n            for x in range(self.size_x - self.hexagonal * (y % 2)):\n                r = sizes[x, y]\n                n = len(self.get_member_indices(x, y))\n                if not r:\n                    continue\n                ellipse = ColoredCircle(r / 2, color, 0)\n                ellipse.setPos(x + (y % 2) * fx, y * fy)\n                ellipse.setToolTip(f\"{n} instances\")\n                self.elements.addToGroup(ellipse)\n\n    def _get_color_column(self):\n        color_column = \\\n            self.data.get_column_view(self.attr_color)[0].astype(float,\n                                                                 copy=False)\n        if self.attr_color.is_discrete:\n            with np.errstate(invalid=\"ignore\"):\n                int_col = color_column.astype(int)\n            int_col[np.isnan(color_column)] = len(self.colors)\n        else:\n            int_col = np.zeros(len(color_column), dtype=int)\n            # The following line is unnecessary because rows with missing\n            # numeric data are excluded. Uncomment it if you change SOM to\n            # tolerate missing values.\n            # int_col[np.isnan(color_column)] = len(self.colors)\n            for i, thresh in enumerate(self.thresholds, start=1):\n                int_col[color_column >= thresh] = i\n        return int_col\n\n    def _tooltip(self, colors, distribution):\n        if self.attr_color.is_discrete:\n            values = self.attr_color.values\n        else:\n            values = self._bin_names()\n        tot = np.sum(distribution)\n        nbhp = \"\\N{NON-BREAKING HYPHEN}\"\n        return '<table style=\"white-space: nowrap\">' + \"\".join(f\"\"\"\n            <tr>\n                <td>\n                    <font color={color.name()}>■</font>\n                    <b>{escape(val).replace(\"-\", nbhp)}</b>:\n                </td>\n                <td>\n                    {n} ({n / tot * 100:.1f}&nbsp;%)\n                </td>\n            </tr>\n            \"\"\" for color, val, n in zip(colors, values, distribution) if n) \\\n            + \"</table>\"\n\n    def _draw_pie_charts(self, sizes):\n        fx, fy = self._grid_factors\n        color_column = self._get_color_column()\n        colors = self.colors.qcolors_w_nan\n        for y in range(self.size_y):\n            for x in range(self.size_x - self.hexagonal * (y % 2)):\n                r = sizes[x, y]\n                if not r:\n                    self.grid_cells[y, x].setToolTip(\"\")\n                    continue\n                members = self.get_member_indices(x, y)\n                color_dist = np.bincount(color_column[members],\n                                         minlength=len(colors))\n                rel_color_dist = color_dist.astype(float) / len(members)\n                pie = PieChart(rel_color_dist, r / 2, colors)\n                pie.setToolTip(self._tooltip(colors, color_dist))\n                self.elements.addToGroup(pie)\n                pie.setPos(x + (y % 2) * fx, y * fy)\n\n    def _draw_colored_circles(self, sizes):\n        fx, fy = self._grid_factors\n        color_column = self._get_color_column()\n        qcolors = self.colors.qcolors_w_nan\n        for y in range(self.size_y):\n            for x in range(self.size_x - self.hexagonal * (y % 2)):\n                r = sizes[x, y]\n                if not r:\n                    continue\n                members = self.get_member_indices(x, y)\n                color_dist = color_column[members]\n                color_dist = color_dist[color_dist < len(self.colors)]\n                if len(color_dist) != len(members):\n                    self.Warning.missing_colors(self.attr_color.name)\n                bc = np.bincount(color_dist, minlength=len(self.colors))\n                color = qcolors[np.argmax(bc)]\n                ellipse = ColoredCircle(r / 2, color, np.max(bc) / len(members))\n                ellipse.setPos(x + (y % 2) * fx, y * fy)\n                ellipse.setToolTip(self._tooltip(qcolors, bc))\n                self.elements.addToGroup(ellipse)\n\n    def redraw_grid(self):\n        if self.grid is not None:\n            self.scene.removeItem(self.grid)\n        self.grid = QGraphicsItemGroup()\n        self.grid.setZValue(-200)\n        self.grid_cells = np.full((self.size_y, self.size_x), None)\n        for y in range(self.size_y):\n            for x in range(self.size_x - (y % 2) * self.hexagonal):\n                if self.hexagonal:\n                    cell = QGraphicsPathItem(_hexagon_path)\n                    cell.setPos(x + (y % 2) / 2, y * sqrt3_2)\n                else:\n                    cell = QGraphicsRectItem(x - 0.5, y - 0.5, 1, 1)\n                self.grid_cells[y, x] = cell\n                cell.setPen(self._grid_pen)\n                self.grid.addToGroup(cell)\n        self.scene.addItem(self.grid)\n\n    def get_member_indices(self, x, y):\n        i, j = self.cells[x, y]\n        return self.member_data[i:j]\n\n    def _recompute_som(self):\n        if self.cont_x is None:\n            return\n\n        som = SOM(\n            self.size_x, self.size_y,\n            hexagonal=self.hexagonal,\n            pca_init=self.initialization == 0,\n            random_seed=0 if self.initialization == 2 else None\n        )\n\n        class Optimizer(QObject):\n            update = Signal(float, np.ndarray, np.ndarray)\n            done = Signal(SOM)\n            stopped = Signal()\n            stop_optimization = False\n\n            def __init__(self, data, som):\n                super().__init__()\n                self.som = som\n                self.data = data\n\n            def callback(self, progress):\n                self.update.emit(\n                    progress,\n                    self.som.weights.copy(), self.som.ssum_weights.copy())\n                return not self.stop_optimization\n\n            def run(self):\n                try:\n                    self.som.fit(self.data, N_ITERATIONS,\n                                 callback=self.callback)\n                    # Report an exception, but still remove the thread\n                finally:\n                    self.done.emit(self.som)\n                    self.stopped.emit()\n\n        def thread_finished():\n            self._optimizer = None\n            self._optimizer_thread = None\n\n        self.progressBarInit()\n\n        self._optimizer = Optimizer(self.cont_x, som)\n        self._optimizer_thread = QThread()\n        self._optimizer_thread.setStackSize(5 * 2 ** 20)\n        self._optimizer.update.connect(self.__update)\n        self._optimizer.done.connect(self.__done)\n        self._optimizer.stopped.connect(self._optimizer_thread.quit)\n        self._optimizer.moveToThread(self._optimizer_thread)\n        self._optimizer_thread.started.connect(self._optimizer.run)\n        self._optimizer_thread.finished.connect(thread_finished)\n        self.stop_optimization = False\n        self._optimizer_thread.start()\n\n    @Slot(float, object, object)\n    def __update(self, _progress, weights, ssum_weights):\n        self.progressBarSet(_progress)\n        self._assign_instances(weights, ssum_weights)\n        self._redraw()\n\n    @Slot(object)\n    def __done(self, som):\n        self.enable_controls(True)\n        self.progressBarFinished()\n        self._assign_instances(som.weights, som.ssum_weights)\n        self._redraw()\n        # This is the first time we know what was selected (assuming that\n        # initialization is not set to random)\n        if self.__pending_selection is not None:\n            self.on_selection_change(self.__pending_selection)\n            self.__pending_selection = None\n        self.update_output()\n\n    def stop_optimization_and_wait(self):\n        if self._optimizer_thread is not None:\n            self.stop_optimization = True\n            self._optimizer.stop_optimization = True\n            self._optimizer_thread.quit()\n            self._optimizer_thread.wait()\n            self._optimizer_thread = None\n\n    def onDeleteWidget(self):\n        self.stop_optimization_and_wait()\n        self.clear()\n        super().onDeleteWidget()\n\n    def _assign_instances(self, weights, ssum_weights):\n        if self.cont_x is None:\n            return  # the widget is shutting down while signals still processed\n        assignments = SOM.winner_from_weights(\n            self.cont_x, weights, ssum_weights, self.hexagonal)\n        members = defaultdict(list)\n        for i, (x, y) in enumerate(assignments):\n            members[(x, y)].append(i)\n        members.pop(None, None)\n        self.cells = np.empty((self.size_x, self.size_y, 2), dtype=int)\n        self.member_data = np.empty(self.cont_x.shape[0], dtype=int)\n        index = 0\n        for x in range(self.size_x):\n            for y in range(self.size_y):\n                nmembers = len(members[(x, y)])\n                self.member_data[index:index + nmembers] = members[(x, y)]\n                self.cells[x, y] = [index, index + nmembers]\n                index += nmembers\n\n    def resizeEvent(self, event):\n        super().resizeEvent(event)\n        self.create_legend()  # re-wrap lines if necessary\n        self.rescale()\n\n    def rescale(self):\n        if self.legend:\n            leg_height = self.legend.boundingRect().height()\n            leg_extra = 1.5\n        else:\n            leg_height = 0\n            leg_extra = 1\n\n        vw, vh = self.view.width(), self.view.height() - leg_height\n        scale = min(vw / (self.size_x + 1),\n                    vh / ((self.size_y + leg_extra) * self._grid_factors[1]))\n        self.view.setTransform(QTransform.fromScale(scale, scale))\n        if self.hexagonal:\n            self.view.setSceneRect(\n                0, -1, self.size_x - 1,\n                (self.size_y + leg_extra) * sqrt3_2 + leg_height / scale)\n        else:\n            self.view.setSceneRect(\n                -0.25, -0.25, self.size_x - 0.5,\n                self.size_y - 0.5 + leg_height / scale)\n\n    def update_output(self):\n        if self.data is None:\n            self.Outputs.selected_data.send(None)\n            self.Outputs.annotated_data.send(None)\n            return\n\n        indices = np.zeros(len(self.data), dtype=int)\n        if self.selection is not None and np.any(self.selection):\n            for y in range(self.size_y):\n                for x in range(self.size_x):\n                    rows = self.get_member_indices(x, y)\n                    indices[rows] = self.selection[x, y]\n\n        if np.any(indices):\n            sel_data = create_groups_table(self.data, indices, False, \"Group\")\n            self.Outputs.selected_data.send(sel_data)\n        else:\n            self.Outputs.selected_data.send(None)\n\n        if np.max(indices) > 1:\n            annotated = create_groups_table(self.data, indices)\n        else:\n            annotated = create_annotated_table(\n                self.data, np.flatnonzero(indices))\n        self.Outputs.annotated_data.send(annotated)\n\n    def set_color_bins(self):\n        if self.attr_color is None:\n            self.thresholds = self.bin_labels = self.colors = None\n        elif self.attr_color.is_discrete:\n            self.thresholds = self.bin_labels = None\n            self.colors = self.attr_color.palette\n        else:\n            col = self.data.get_column_view(self.attr_color)[0].astype(float)\n            if self.attr_color.is_time:\n                binning = time_binnings(col, min_bins=4)[-1]\n            else:\n                binning = decimal_binnings(col, min_bins=4)[-1]\n            self.thresholds = binning.thresholds[1:-1]\n            self.bin_labels = (binning.labels[1:-1], binning.short_labels[1:-1])\n            palette = BinnedContinuousPalette.from_palette(\n                self.attr_color.palette, binning.thresholds)\n            self.colors = palette\n\n    def create_legend(self):\n        if self.legend is not None:\n            self.scene.removeItem(self.legend)\n            self.legend = None\n        if self.attr_color is None:\n            return\n\n        if self.attr_color.is_discrete:\n            names = self.attr_color.values\n        else:\n            names = self._bin_names()\n\n        items = []\n        size = 8\n        for name, color in zip(names, self.colors.qcolors):\n            item = QGraphicsItemGroup()\n            item.addToGroup(\n                CanvasRectangle(None, -size / 2, -size / 2, size, size,\n                                Qt.gray, color))\n            item.addToGroup(CanvasText(None, name, size, 0, Qt.AlignVCenter))\n            items.append(item)\n\n        self.legend = wrap_legend_items(\n            items, hspacing=20, vspacing=16 + size,\n            max_width=self.view.width() - 25)\n        self.legend.setFlags(self.legend.ItemIgnoresTransformations)\n        self.legend.setTransform(\n            QTransform.fromTranslate(-self.legend.boundingRect().width() / 2,\n                                     0))\n        self.scene.addItem(self.legend)\n        self.set_legend_pos()\n\n    def _bin_names(self):\n        labels, short_labels = self.bin_labels\n        return \\\n            [f\"< {labels[0]}\"] \\\n            + [f\"{x} - {y}\" for x, y in zip(labels, short_labels[1:])] \\\n            + [f\"≥ {labels[-1]}\"]\n\n    def set_legend_pos(self):\n        if self.legend is None:\n            return\n        self.legend.setPos(\n            self.size_x / 2,\n            (self.size_y + 0.2 + 0.3 * self.hexagonal) * self._grid_factors[1])\n\n    def send_report(self):\n        self.report_plot()\n        if self.attr_color:\n            self.report_caption(\n                f\"Self-organizing map colored by '{self.attr_color.name}'\")",
                                "ast_type_str": "ClassDef",
                                "start_lineno": 174,
                                "end_lineno": 890,
                                "parent": null
                            }
                        },
                        {
                            "file_name": "Orange/widgets/unsupervised/owsom.py",
                            "type": "class",
                            "name": "OWSOM",
                            "content": "class OWSOM(OWWidget):\n    name = \"Self-Organizing Map\"\n    description = \"Computation of self-organizing map.\"\n    icon = \"icons/SOM.svg\"\n    keywords = [\"SOM\"]\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    settingsHandler = DomainContextHandler()\n    auto_dimension = Setting(True)\n    size_x = Setting(10)\n    size_y = Setting(10)\n    hexagonal = Setting(1)\n    initialization = Setting(0)\n\n    attr_color = ContextSetting(None)\n    size_by_instances = Setting(True)\n    pie_charts = Setting(False)\n    selection = Setting(None, schema_only=True)\n\n    graph_name = \"view\"\n\n    _grid_pen = QPen(QBrush(QColor(224, 224, 224)), 2)\n    _grid_pen.setCosmetic(True)\n\n    OptControls = namedtuple(\n        \"OptControls\",\n        (\"shape\", \"auto_dim\", \"spin_x\", \"spin_y\", \"initialization\", \"start\")\n    )\n\n    class Warning(OWWidget.Warning):\n        ignoring_disc_variables = Msg(\"SOM ignores categorical variables.\")\n        missing_colors = \\\n            Msg(\"Some data instances have undefined value of '{}'.\")\n        missing_values = \\\n            Msg(\"{} data instance{} with undefined value(s) {} not shown.\")\n        single_attribute = Msg(\"Data contains a single numeric column.\")\n\n    class Error(OWWidget.Error):\n        no_numeric_variables = Msg(\"Data contains no numeric columns.\")\n        no_defined_rows = Msg(\"All rows contain at least one undefined value.\")\n\n    def __init__(self):\n        super().__init__()\n        self.__pending_selection = self.selection\n        self._optimizer = None\n        self._optimizer_thread = None\n        self.stop_optimization = False\n\n        self.data = self.cont_x = None\n        self.cells = self.member_data = None\n        self.selection = None\n        self.colors = self.thresholds = self.bin_labels = None\n\n        box = gui.vBox(self.controlArea, box=\"SOM\")\n        shape = gui.comboBox(\n            box, self, \"\", items=(\"Hexagonal grid\", \"Square grid\"))\n        shape.setCurrentIndex(1 - self.hexagonal)\n\n        box2 = gui.indentedBox(box, 10)\n        auto_dim = gui.checkBox(\n            box2, self, \"auto_dimension\", \"Set dimensions automatically\",\n            callback=self.on_auto_dimension_changed)\n        self.manual_box = box3 = gui.hBox(box2)\n        spinargs = dict(\n            value=\"\", widget=box3, master=self, minv=5, maxv=100, step=5,\n            alignment=Qt.AlignRight)\n        spin_x = gui.spin(**spinargs)\n        spin_x.setValue(self.size_x)\n        gui.widgetLabel(box3, \"×\")\n        spin_y = gui.spin(**spinargs)\n        spin_y.setValue(self.size_y)\n        gui.rubber(box3)\n        self.manual_box.setEnabled(not self.auto_dimension)\n\n        initialization = gui.comboBox(\n            box, self, \"initialization\",\n            items=(\"Initialize with PCA\", \"Random initialization\",\n                   \"Replicable random\"))\n\n        start = gui.button(\n            box, self, \"Restart\", callback=self.restart_som_pressed,\n            sizePolicy=(QSizePolicy.MinimumExpanding, QSizePolicy.Fixed))\n\n        self.opt_controls = self.OptControls(\n            shape, auto_dim, spin_x, spin_y, initialization, start)\n\n        box = gui.vBox(self.controlArea, \"Color\")\n        gui.comboBox(\n            box, self, \"attr_color\", searchable=True,\n            callback=self.on_attr_color_change,\n            model=DomainModel(placeholder=\"(Same color)\",\n                              valid_types=DomainModel.PRIMITIVE))\n        gui.checkBox(\n            box, self, \"pie_charts\", label=\"Show pie charts\",\n            callback=self.on_pie_chart_change)\n        gui.checkBox(\n            box, self, \"size_by_instances\", label=\"Size by number of instances\",\n            callback=self.on_attr_size_change)\n\n        gui.rubber(self.controlArea)\n\n        self.scene = QGraphicsScene(self)\n\n        self.view = SomView(self.scene)\n        self.view.setMinimumWidth(400)\n        self.view.setMinimumHeight(400)\n        self.view.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)\n        self.view.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)\n        self.view.setRenderHint(QPainter.Antialiasing)\n        self.view.selection_changed.connect(self.on_selection_change)\n        self.view.selection_moved.connect(self.on_selection_move)\n        self.view.selection_mark_changed.connect(self.on_selection_mark_change)\n        self.mainArea.layout().addWidget(self.view)\n\n        self.elements = None\n        self.grid = None\n        self.grid_cells = None\n        self.legend = None\n\n    @Inputs.data\n    def set_data(self, data):\n        def prepare_data():\n            if len(cont_attrs) < len(attrs):\n                self.Warning.ignoring_disc_variables()\n            if len(cont_attrs) == 1:\n                self.Warning.single_attribute()\n            x = Table.from_table(Domain(cont_attrs), data).X\n            if sp.issparse(x):\n                self.data = data\n                self.cont_x = x.tocsr()\n            else:\n                mask = np.all(np.isfinite(x), axis=1)\n                if not np.any(mask):\n                    self.Error.no_defined_rows()\n                else:\n                    if np.all(mask):\n                        self.data = data\n                        self.cont_x = x.copy()\n                    else:\n                        self.data = data[mask]\n                        self.cont_x = x[mask]\n                    self.cont_x -= np.min(self.cont_x, axis=0)[None, :]\n                    sums = np.sum(self.cont_x, axis=0)[None, :]\n                    sums[sums == 0] = 1\n                    self.cont_x /= sums\n\n        def set_warnings():\n            missing = len(data) - len(self.data)\n            if missing == 1:\n                self.Warning.missing_values(1, \"\", \"is\")\n            elif missing > 1:\n                self.Warning.missing_values(missing, \"s\", \"are\")\n\n        self.stop_optimization_and_wait()\n\n        self.closeContext()\n        self.clear()\n        self.Error.clear()\n        self.Warning.clear()\n\n        if data is not None:\n            attrs = data.domain.attributes\n            cont_attrs = [var for var in attrs if var.is_continuous]\n            if not cont_attrs:\n                self.Error.no_numeric_variables()\n            else:\n                prepare_data()\n\n        if self.data is not None:\n            self.controls.attr_color.model().set_domain(data.domain)\n            self.attr_color = data.domain.class_var\n            set_warnings()\n\n        self.openContext(self.data)\n        self.set_color_bins()\n        self.create_legend()\n        self.recompute_dimensions()\n        self.start_som()\n\n    def clear(self):\n        self.data = self.cont_x = None\n        self.cells = self.member_data = None\n        self.attr_color = None\n        self.colors = self.thresholds = self.bin_labels = None\n        if self.elements is not None:\n            self.scene.removeItem(self.elements)\n            self.elements = None\n        self.clear_selection()\n        self.controls.attr_color.model().set_domain(None)\n        self.Warning.clear()\n        self.Error.clear()\n\n    def recompute_dimensions(self):\n        if not self.auto_dimension or self.cont_x is None:\n            return\n        dim = max(5, int(np.ceil(np.sqrt(5 * np.sqrt(self.cont_x.shape[0])))))\n        self.opt_controls.spin_x.setValue(dim)\n        self.opt_controls.spin_y.setValue(dim)\n\n    def on_auto_dimension_changed(self):\n        self.manual_box.setEnabled(not self.auto_dimension)\n        if self.auto_dimension:\n            self.recompute_dimensions()\n        else:\n            spin_x = self.opt_controls.spin_x\n            spin_y = self.opt_controls.spin_y\n            dimx = int(5 * np.round(spin_x.value() / 5))\n            dimy = int(5 * np.round(spin_y.value() / 5))\n            spin_x.setValue(dimx)\n            spin_y.setValue(dimy)\n\n    def on_attr_color_change(self):\n        self.controls.pie_charts.setEnabled(self.attr_color is not None)\n        self.set_color_bins()\n        self.create_legend()\n        self.rescale()\n        self._redraw()\n\n    def on_attr_size_change(self):\n        self._redraw()\n\n    def on_pie_chart_change(self):\n        self._redraw()\n\n    def clear_selection(self):\n        self.selection = None\n        self.redraw_selection()\n\n    def on_selection_change(self, selection, action=SomView.SelectionSet):\n        if self.data is None:  # clicks on empty canvas\n            return\n        if self.selection is None:\n            self.selection = np.zeros(self.grid_cells.T.shape, dtype=np.int16)\n        if action == SomView.SelectionSet:\n            self.selection[:] = 0\n            self.selection[selection] = 1\n        elif action == SomView.SelectionAddToGroup:\n            self.selection[selection] = max(1, np.max(self.selection))\n        elif action == SomView.SelectionNewGroup:\n            self.selection[selection] = 1 + np.max(self.selection)\n        elif action & SomView.SelectionRemove:\n            self.selection[selection] = 0\n        self.redraw_selection()\n        self.update_output()\n\n    def on_selection_move(self, event: QKeyEvent):\n        if self.selection is None or not np.any(self.selection):\n            if event.key() in (Qt.Key_Right, Qt.Key_Down):\n                x = y = 0\n            else:\n                x = self.size_x - 1\n                y = self.size_y - 1\n        else:\n            x, y = np.nonzero(self.selection)\n            if len(x) > 1:\n                return\n            if event.key() == Qt.Key_Up and y > 0:\n                y -= 1\n            if event.key() == Qt.Key_Down and y < self.size_y - 1:\n                y += 1\n            if event.key() == Qt.Key_Left and x:\n                x -= 1\n            if event.key() == Qt.Key_Right and x < self.size_x - 1:\n                x += 1\n            x -= self.hexagonal and x == self.size_x - 1 and y % 2\n\n        if self.selection is not None and self.selection[x, y]:\n            return\n        selection = np.zeros(self.grid_cells.shape, dtype=bool)\n        selection[x, y] = True\n        self.on_selection_change(selection)\n\n    def on_selection_mark_change(self, marks):\n        self.redraw_selection(marks=marks)\n\n    def redraw_selection(self, marks=None):\n        if self.grid_cells is None:\n            return\n\n        sel_pen = QPen(QBrush(QColor(128, 128, 128)), 2)\n        sel_pen.setCosmetic(True)\n        mark_pen = QPen(QBrush(QColor(128, 128, 128)), 4)\n        mark_pen.setCosmetic(True)\n        pens = [self._grid_pen, sel_pen]\n\n        mark_brush = QBrush(QColor(224, 255, 255))\n        sels = self.selection is not None and np.max(self.selection)\n        palette = LimitedDiscretePalette(number_of_colors=sels + 1)\n        brushes = [QBrush(Qt.NoBrush)] + \\\n                  [QBrush(palette[i].lighter(165)) for i in range(sels)]\n\n        for y in range(self.size_y):\n            for x in range(self.size_x - (y % 2) * self.hexagonal):\n                cell = self.grid_cells[y, x]\n                marked = marks is not None and marks[x, y]\n                sel_group = self.selection is not None and self.selection[x, y]\n                if marked:\n                    cell.setBrush(mark_brush)\n                    cell.setPen(mark_pen)\n                else:\n                    cell.setBrush(brushes[sel_group])\n                    cell.setPen(pens[bool(sel_group)])\n                cell.setZValue(marked or sel_group)\n\n    def restart_som_pressed(self):\n        if self._optimizer_thread is not None:\n            self.stop_optimization = True\n            self._optimizer.stop_optimization = True\n        else:\n            self.start_som()\n\n    def start_som(self):\n        self.read_controls()\n        self.update_layout()\n        self.clear_selection()\n        if self.cont_x is not None:\n            self.enable_controls(False)\n            self._recompute_som()\n        else:\n            self.update_output()\n\n    def read_controls(self):\n        c = self.opt_controls\n        self.hexagonal = c.shape.currentIndex() == 0\n        self.size_x = c.spin_x.value()\n        self.size_y = c.spin_y.value()\n\n    def enable_controls(self, enable):\n        c = self.opt_controls\n        c.shape.setEnabled(enable)\n        c.auto_dim.setEnabled(enable)\n        c.start.setText(\"Start\" if enable else \"Stop\")\n\n    def update_layout(self):\n        self.set_legend_pos()\n        if self.elements:  # Prevent having redrawn grid but with old elements\n            self.scene.removeItem(self.elements)\n            self.elements = None\n        self.redraw_grid()\n        self.rescale()\n\n    def _redraw(self):\n        self.Warning.missing_colors.clear()\n        if self.elements:\n            self.scene.removeItem(self.elements)\n            self.elements = None\n        self.view.set_dimensions(self.size_x, self.size_y, self.hexagonal)\n\n        if self.cells is None:\n            return\n        sizes = self.cells[:, :, 1] - self.cells[:, :, 0]\n        sizes = sizes.astype(float)\n        if not self.size_by_instances:\n            sizes[sizes != 0] = 0.8\n        else:\n            sizes *= 0.8 / np.max(sizes)\n\n        self.elements = QGraphicsItemGroup()\n        self.scene.addItem(self.elements)\n        if self.attr_color is None:\n            self._draw_same_color(sizes)\n        elif self.pie_charts:\n            self._draw_pie_charts(sizes)\n        else:\n            self._draw_colored_circles(sizes)\n\n    @property\n    def _grid_factors(self):\n        return (0.5, sqrt3_2) if self.hexagonal else (0, 1)\n\n    def _draw_same_color(self, sizes):\n        fx, fy = self._grid_factors\n        color = QColor(64, 64, 64)\n        for y in range(self.size_y):\n            for x in range(self.size_x - self.hexagonal * (y % 2)):\n                r = sizes[x, y]\n                n = len(self.get_member_indices(x, y))\n                if not r:\n                    continue\n                ellipse = ColoredCircle(r / 2, color, 0)\n                ellipse.setPos(x + (y % 2) * fx, y * fy)\n                ellipse.setToolTip(f\"{n} instances\")\n                self.elements.addToGroup(ellipse)\n\n    def _get_color_column(self):\n        color_column = \\\n            self.data.get_column_view(self.attr_color)[0].astype(float,\n                                                                 copy=False)\n        if self.attr_color.is_discrete:\n            with np.errstate(invalid=\"ignore\"):\n                int_col = color_column.astype(int)\n            int_col[np.isnan(color_column)] = len(self.colors)\n        else:\n            int_col = np.zeros(len(color_column), dtype=int)\n            # The following line is unnecessary because rows with missing\n            # numeric data are excluded. Uncomment it if you change SOM to\n            # tolerate missing values.\n            # int_col[np.isnan(color_column)] = len(self.colors)\n            for i, thresh in enumerate(self.thresholds, start=1):\n                int_col[color_column >= thresh] = i\n        return int_col\n\n    def _tooltip(self, colors, distribution):\n        if self.attr_color.is_discrete:\n            values = self.attr_color.values\n        else:\n            values = self._bin_names()\n        tot = np.sum(distribution)\n        nbhp = \"\\N{NON-BREAKING HYPHEN}\"\n        return '<table style=\"white-space: nowrap\">' + \"\".join(f\"\"\"\n            <tr>\n                <td>\n                    <font color={color.name()}>■</font>\n                    <b>{escape(val).replace(\"-\", nbhp)}</b>:\n                </td>\n                <td>\n                    {n} ({n / tot * 100:.1f}&nbsp;%)\n                </td>\n            </tr>\n            \"\"\" for color, val, n in zip(colors, values, distribution) if n) \\\n            + \"</table>\"\n\n    def _draw_pie_charts(self, sizes):\n        fx, fy = self._grid_factors\n        color_column = self._get_color_column()\n        colors = self.colors.qcolors_w_nan\n        for y in range(self.size_y):\n            for x in range(self.size_x - self.hexagonal * (y % 2)):\n                r = sizes[x, y]\n                if not r:\n                    self.grid_cells[y, x].setToolTip(\"\")\n                    continue\n                members = self.get_member_indices(x, y)\n                color_dist = np.bincount(color_column[members],\n                                         minlength=len(colors))\n                rel_color_dist = color_dist.astype(float) / len(members)\n                pie = PieChart(rel_color_dist, r / 2, colors)\n                pie.setToolTip(self._tooltip(colors, color_dist))\n                self.elements.addToGroup(pie)\n                pie.setPos(x + (y % 2) * fx, y * fy)\n\n    def _draw_colored_circles(self, sizes):\n        fx, fy = self._grid_factors\n        color_column = self._get_color_column()\n        qcolors = self.colors.qcolors_w_nan\n        for y in range(self.size_y):\n            for x in range(self.size_x - self.hexagonal * (y % 2)):\n                r = sizes[x, y]\n                if not r:\n                    continue\n                members = self.get_member_indices(x, y)\n                color_dist = color_column[members]\n                color_dist = color_dist[color_dist < len(self.colors)]\n                if len(color_dist) != len(members):\n                    self.Warning.missing_colors(self.attr_color.name)\n                bc = np.bincount(color_dist, minlength=len(self.colors))\n                color = qcolors[np.argmax(bc)]\n                ellipse = ColoredCircle(r / 2, color, np.max(bc) / len(members))\n                ellipse.setPos(x + (y % 2) * fx, y * fy)\n                ellipse.setToolTip(self._tooltip(qcolors, bc))\n                self.elements.addToGroup(ellipse)\n\n    def redraw_grid(self):\n        if self.grid is not None:\n            self.scene.removeItem(self.grid)\n        self.grid = QGraphicsItemGroup()\n        self.grid.setZValue(-200)\n        self.grid_cells = np.full((self.size_y, self.size_x), None)\n        for y in range(self.size_y):\n            for x in range(self.size_x - (y % 2) * self.hexagonal):\n                if self.hexagonal:\n                    cell = QGraphicsPathItem(_hexagon_path)\n                    cell.setPos(x + (y % 2) / 2, y * sqrt3_2)\n                else:\n                    cell = QGraphicsRectItem(x - 0.5, y - 0.5, 1, 1)\n                self.grid_cells[y, x] = cell\n                cell.setPen(self._grid_pen)\n                self.grid.addToGroup(cell)\n        self.scene.addItem(self.grid)\n\n    def get_member_indices(self, x, y):\n        i, j = self.cells[x, y]\n        return self.member_data[i:j]\n\n    def _recompute_som(self):\n        if self.cont_x is None:\n            return\n\n        som = SOM(\n            self.size_x, self.size_y,\n            hexagonal=self.hexagonal,\n            pca_init=self.initialization == 0,\n            random_seed=0 if self.initialization == 2 else None\n        )\n\n        class Optimizer(QObject):\n            update = Signal(float, np.ndarray, np.ndarray)\n            done = Signal(SOM)\n            stopped = Signal()\n            stop_optimization = False\n\n            def __init__(self, data, som):\n                super().__init__()\n                self.som = som\n                self.data = data\n\n            def callback(self, progress):\n                self.update.emit(\n                    progress,\n                    self.som.weights.copy(), self.som.ssum_weights.copy())\n                return not self.stop_optimization\n\n            def run(self):\n                try:\n                    self.som.fit(self.data, N_ITERATIONS,\n                                 callback=self.callback)\n                    # Report an exception, but still remove the thread\n                finally:\n                    self.done.emit(self.som)\n                    self.stopped.emit()\n\n        def thread_finished():\n            self._optimizer = None\n            self._optimizer_thread = None\n\n        self.progressBarInit()\n\n        self._optimizer = Optimizer(self.cont_x, som)\n        self._optimizer_thread = QThread()\n        self._optimizer_thread.setStackSize(5 * 2 ** 20)\n        self._optimizer.update.connect(self.__update)\n        self._optimizer.done.connect(self.__done)\n        self._optimizer.stopped.connect(self._optimizer_thread.quit)\n        self._optimizer.moveToThread(self._optimizer_thread)\n        self._optimizer_thread.started.connect(self._optimizer.run)\n        self._optimizer_thread.finished.connect(thread_finished)\n        self.stop_optimization = False\n        self._optimizer_thread.start()\n\n    @Slot(float, object, object)\n    def __update(self, _progress, weights, ssum_weights):\n        self.progressBarSet(_progress)\n        self._assign_instances(weights, ssum_weights)\n        self._redraw()\n\n    @Slot(object)\n    def __done(self, som):\n        self.enable_controls(True)\n        self.progressBarFinished()\n        self._assign_instances(som.weights, som.ssum_weights)\n        self._redraw()\n        # This is the first time we know what was selected (assuming that\n        # initialization is not set to random)\n        if self.__pending_selection is not None:\n            self.on_selection_change(self.__pending_selection)\n            self.__pending_selection = None\n        self.update_output()\n\n    def stop_optimization_and_wait(self):\n        if self._optimizer_thread is not None:\n            self.stop_optimization = True\n            self._optimizer.stop_optimization = True\n            self._optimizer_thread.quit()\n            self._optimizer_thread.wait()\n            self._optimizer_thread = None\n\n    def onDeleteWidget(self):\n        self.stop_optimization_and_wait()\n        self.clear()\n        super().onDeleteWidget()\n\n    def _assign_instances(self, weights, ssum_weights):\n        if self.cont_x is None:\n            return  # the widget is shutting down while signals still processed\n        assignments = SOM.winner_from_weights(\n            self.cont_x, weights, ssum_weights, self.hexagonal)\n        members = defaultdict(list)\n        for i, (x, y) in enumerate(assignments):\n            members[(x, y)].append(i)\n        members.pop(None, None)\n        self.cells = np.empty((self.size_x, self.size_y, 2), dtype=int)\n        self.member_data = np.empty(self.cont_x.shape[0], dtype=int)\n        index = 0\n        for x in range(self.size_x):\n            for y in range(self.size_y):\n                nmembers = len(members[(x, y)])\n                self.member_data[index:index + nmembers] = members[(x, y)]\n                self.cells[x, y] = [index, index + nmembers]\n                index += nmembers\n\n    def resizeEvent(self, event):\n        super().resizeEvent(event)\n        self.create_legend()  # re-wrap lines if necessary\n        self.rescale()\n\n    def rescale(self):\n        if self.legend:\n            leg_height = self.legend.boundingRect().height()\n            leg_extra = 1.5\n        else:\n            leg_height = 0\n            leg_extra = 1\n\n        vw, vh = self.view.width(), self.view.height() - leg_height\n        scale = min(vw / (self.size_x + 1),\n                    vh / ((self.size_y + leg_extra) * self._grid_factors[1]))\n        self.view.setTransform(QTransform.fromScale(scale, scale))\n        if self.hexagonal:\n            self.view.setSceneRect(\n                0, -1, self.size_x - 1,\n                (self.size_y + leg_extra) * sqrt3_2 + leg_height / scale)\n        else:\n            self.view.setSceneRect(\n                -0.25, -0.25, self.size_x - 0.5,\n                self.size_y - 0.5 + leg_height / scale)\n\n    def update_output(self):\n        if self.data is None:\n            self.Outputs.selected_data.send(None)\n            self.Outputs.annotated_data.send(None)\n            return\n\n        indices = np.zeros(len(self.data), dtype=int)\n        if self.selection is not None and np.any(self.selection):\n            for y in range(self.size_y):\n                for x in range(self.size_x):\n                    rows = self.get_member_indices(x, y)\n                    indices[rows] = self.selection[x, y]\n\n        if np.any(indices):\n            sel_data = create_groups_table(self.data, indices, False, \"Group\")\n            self.Outputs.selected_data.send(sel_data)\n        else:\n            self.Outputs.selected_data.send(None)\n\n        if np.max(indices) > 1:\n            annotated = create_groups_table(self.data, indices)\n        else:\n            annotated = create_annotated_table(\n                self.data, np.flatnonzero(indices))\n        self.Outputs.annotated_data.send(annotated)\n\n    def set_color_bins(self):\n        if self.attr_color is None:\n            self.thresholds = self.bin_labels = self.colors = None\n        elif self.attr_color.is_discrete:\n            self.thresholds = self.bin_labels = None\n            self.colors = self.attr_color.palette\n        else:\n            col = self.data.get_column_view(self.attr_color)[0].astype(float)\n            if self.attr_color.is_time:\n                binning = time_binnings(col, min_bins=4)[-1]\n            else:\n                binning = decimal_binnings(col, min_bins=4)[-1]\n            self.thresholds = binning.thresholds[1:-1]\n            self.bin_labels = (binning.labels[1:-1], binning.short_labels[1:-1])\n            palette = BinnedContinuousPalette.from_palette(\n                self.attr_color.palette, binning.thresholds)\n            self.colors = palette\n\n    def create_legend(self):\n        if self.legend is not None:\n            self.scene.removeItem(self.legend)\n            self.legend = None\n        if self.attr_color is None:\n            return\n\n        if self.attr_color.is_discrete:\n            names = self.attr_color.values\n        else:\n            names = self._bin_names()\n\n        items = []\n        size = 8\n        for name, color in zip(names, self.colors.qcolors):\n            item = QGraphicsItemGroup()\n            item.addToGroup(\n                CanvasRectangle(None, -size / 2, -size / 2, size, size,\n                                Qt.gray, color))\n            item.addToGroup(CanvasText(None, name, size, 0, Qt.AlignVCenter))\n            items.append(item)\n\n        self.legend = wrap_legend_items(\n            items, hspacing=20, vspacing=16 + size,\n            max_width=self.view.width() - 25)\n        self.legend.setFlags(self.legend.ItemIgnoresTransformations)\n        self.legend.setTransform(\n            QTransform.fromTranslate(-self.legend.boundingRect().width() / 2,\n                                     0))\n        self.scene.addItem(self.legend)\n        self.set_legend_pos()\n\n    def _bin_names(self):\n        labels, short_labels = self.bin_labels\n        return \\\n            [f\"< {labels[0]}\"] \\\n            + [f\"{x} - {y}\" for x, y in zip(labels, short_labels[1:])] \\\n            + [f\"≥ {labels[-1]}\"]\n\n    def set_legend_pos(self):\n        if self.legend is None:\n            return\n        self.legend.setPos(\n            self.size_x / 2,\n            (self.size_y + 0.2 + 0.3 * self.hexagonal) * self._grid_factors[1])\n\n    def send_report(self):\n        self.report_plot()\n        if self.attr_color:\n            self.report_caption(\n                f\"Self-organizing map colored by '{self.attr_color.name}'\")",
                            "ast_type_str": "ClassDef",
                            "start_lineno": 174,
                            "end_lineno": 890,
                            "parent": null
                        }
                    ],
                    "added_entities": [],
                    "deleted_entities": []
                }
            ]
        },
        {
            "old_file_content": "# pylint: disable=protected-access\n\nimport unittest\nfrom unittest.mock import Mock, patch\n\nimport numpy as np\nimport scipy.sparse as sp\n\nfrom Orange.data import Table, Domain\nfrom Orange.widgets.tests.base import WidgetTest\nfrom Orange.widgets.utils.annotated_data import ANNOTATED_DATA_FEATURE_NAME\nfrom Orange.widgets.unsupervised.owsom import OWSOM, SomView, SOM\n\n\ndef _patch_recompute_som(meth):\n    def winners_from_weights(cont_x, *_1, **_2):\n        n = cont_x.shape[0]\n        w = np.zeros((n, 2), dtype=int)\n        w[n // 5:] = [0, 1]\n        w[n // 3:] = [1, 2]\n        return w\n\n    def recompute(self):\n        if not self.data:\n            return\n        self._assign_instances(None, None)\n        self._redraw()\n        self.update_output()\n\n    def patched(*args, **kwargs):\n        with patch.object(SOM, \"winner_from_weights\", new=winners_from_weights), \\\n                patch.object(OWSOM, \"_recompute_som\", new=recompute):\n            meth(*args, **kwargs)\n\n    return patched\n\n\nclass TestOWSOM(WidgetTest):\n    def setUp(self):\n        self.widget = self.create_widget(OWSOM)  #: OWSOM\n        self.iris = Table(\"iris\")\n\n    def tearDown(self):\n        self.widget.onDeleteWidget()\n        super().tearDown()\n\n    def test_requires_continuous(self):\n        widget = self.widget\n        heart = Table(\"heart_disease\")\n\n        self.send_signal(widget.Inputs.data, Table(\"zoo\"))\n        self.assertTrue(widget.Error.no_numeric_variables.is_shown())\n        self.assertFalse(widget.Warning.ignoring_disc_variables.is_shown())\n        self.assertIsNone(widget.data)\n        self.assertIsNone(widget.cont_x)\n\n        self.send_signal(widget.Inputs.data, heart)\n        self.assertFalse(widget.Error.no_numeric_variables.is_shown())\n        self.assertTrue(widget.Warning.ignoring_disc_variables.is_shown())\n        self.assertEqual(\n            widget.cont_x.shape[1],\n            sum(attr.is_continuous for attr in heart.domain.attributes))\n\n        self.send_signal(widget.Inputs.data, self.iris)\n        self.assertFalse(widget.Error.no_numeric_variables.is_shown())\n        self.assertFalse(widget.Warning.ignoring_disc_variables.is_shown())\n        self.assertEqual(widget.cont_x.shape, (150, 4))\n\n    def test_single_attribute(self):\n        widget = self.widget\n        self.send_signal(widget.Inputs.data, self.iris)\n        self.assertFalse(widget.Warning.single_attribute.is_shown())\n        iris = self.iris[:, 0]\n        self.send_signal(widget.Inputs.data, iris)\n        self.assertTrue(widget.Warning.single_attribute.is_shown())\n\n    def test_missing_all_data(self):\n        widget = self.widget\n\n        self.send_signal(widget.Inputs.data, Table(\"heart_disease\"))\n        self.assertTrue(widget.Warning.ignoring_disc_variables.is_shown())\n\n        with self.iris.unlocked():\n            for i in range(150):\n                self.iris.X[i, i % 4] = np.nan\n\n        self.send_signal(widget.Inputs.data, self.iris)\n        self.assertTrue(widget.Error.no_defined_rows.is_shown())\n        self.assertFalse(widget.Warning.ignoring_disc_variables.is_shown())\n        self.assertIsNone(widget.data)\n        self.assertIsNone(widget.cont_x)\n\n        self.send_signal(widget.Inputs.data, None)\n        self.assertFalse(widget.Warning.ignoring_disc_variables.is_shown())\n\n    def test_missing_some_data(self):\n        widget = self.widget\n        with self.iris.unlocked():\n            self.iris.X[:50, 0] = np.nan\n\n        self.send_signal(widget.Inputs.data, self.iris)\n        self.assertFalse(widget.Error.no_defined_rows.is_shown())\n        self.assertTrue(widget.Warning.missing_values.is_shown())\n        np.testing.assert_almost_equal(\n            widget.data.Y.flatten(), [1] * 50 + [2] * 50)\n        self.assertEqual(widget.cont_x.shape, (100, 4))\n\n        self.send_signal(widget.Inputs.data, None)\n        self.assertFalse(widget.Warning.missing_values.is_shown())\n\n    def test_missing_one_row_data(self):\n        widget = self.widget\n        with self.iris.unlocked():\n            self.iris.X[5, 0] = np.nan\n\n        self.send_signal(widget.Inputs.data, self.iris)\n        self.assertFalse(widget.Error.no_defined_rows.is_shown())\n        self.assertTrue(widget.Warning.missing_values.is_shown())\n\n        self.send_signal(widget.Inputs.data, None)\n        self.assertFalse(widget.Warning.missing_values.is_shown())\n\n    @_patch_recompute_som\n    def test_sparse_data(self):\n        widget = self.widget\n        with self.iris.unlocked():\n            self.iris.X = sp.csc_matrix(self.iris.X)\n\n        # Table.from_table can decide to return dense data\n        with patch.object(Table, \"from_table\", lambda _, x: x):\n            # update_output would fail because of above patch\n            widget.update_output = Mock()\n            self.send_signal(widget.Inputs.data, self.iris)\n        self.assertIs(widget.data, self.iris)\n        self.assertTrue(sp.isspmatrix_csr(widget.cont_x))\n        self.assertEqual(widget.cont_x.shape, (150, 4))\n\n    def test_auto_compute_dimensions(self):\n        widget = self.widget\n        self.send_signal(widget.Inputs.data, self.iris)\n        spin_x = widget.opt_controls.spin_x\n        spin_y = widget.opt_controls.spin_y\n\n        spin_x.setValue(100)\n        spin_y.setValue(100)\n        widget.auto_dimension = True\n        widget.controls.auto_dimension.toggled.emit(True)\n        self.assertLess(spin_x.value(), 100)\n        self.assertLess(spin_y.value(), 100)\n\n        spin_x.setValue(9)\n        spin_y.setValue(7)\n        widget.auto_dimension = False\n        widget.controls.auto_dimension.toggled.emit(False)\n        self.assertEqual(spin_x.value(), 10)\n        self.assertEqual(spin_y.value(), 5)\n\n    @_patch_recompute_som\n    def test_redraw_grid(self):\n        widget = self.widget\n\n        widget.auto_dimension = False\n\n        widget.opt_controls.spin_x.setValue(8)\n        widget.opt_controls.spin_y.setValue(5)\n        widget.opt_controls.shape.setCurrentIndex(1)\n        self.send_signal(widget.Inputs.data, self.iris)\n        self.assertEqual(widget.grid_cells.shape, (5, 8))\n        self.assertEqual(\n            sum(c is not None for c in widget.grid_cells.ravel()), 40)\n\n        widget.opt_controls.shape.setCurrentIndex(0)\n        widget.opt_controls.start.clicked.emit()\n        self.assertEqual(widget.grid_cells.shape, (5, 8))\n        self.assertEqual(\n            sum(c is not None for c in widget.grid_cells.ravel()), 38)\n        self.assertIsNone(widget.grid_cells[1, 7])\n        self.assertIsNone(widget.grid_cells[3, 7])\n\n        widget.opt_controls.shape.setCurrentIndex(1)\n        widget.opt_controls.start.clicked.emit()\n        self.assertEqual(widget.grid_cells.shape, (5, 8))\n        self.assertEqual(\n            sum(c is not None for c in widget.grid_cells.ravel()), 40)\n\n    def test_attr_color_change(self):\n        widget = self.widget\n        widget._redraw = Mock()\n\n        heart = Table(\"heart_disease\")\n        self.send_signal(widget.Inputs.data, heart)\n\n        widget._redraw.reset_mock()\n        combo = widget.controls.attr_color\n        combo.setCurrentIndex(0)\n        combo.activated[int].emit(0)\n        self.assertFalse(widget.controls.pie_charts.isEnabled())\n        self.assertIsNone(widget.colors)\n        self.assertIsNone(widget.thresholds)\n        widget._redraw.assert_called()\n\n        widget._redraw.reset_mock()\n        combo = widget.controls.attr_color\n        gender = heart.domain[\"gender\"]\n        ind_gen = combo.model().indexOf(gender)\n        combo.setCurrentIndex(ind_gen)\n        combo.activated[int].emit(ind_gen)\n        self.assertTrue(widget.controls.pie_charts.isEnabled())\n        np.testing.assert_equal(widget.colors.palette, gender.colors)\n        self.assertIsNone(widget.thresholds)\n        widget._redraw.assert_called()\n\n        widget._redraw.reset_mock()\n        combo = widget.controls.attr_color\n        age = heart.domain[\"age\"]\n        ind_age = combo.model().indexOf(age)\n        combo.setCurrentIndex(ind_age)\n        combo.activated[int].emit(ind_age)\n        self.assertTrue(widget.controls.pie_charts.isEnabled())\n        self.assertIsNotNone(widget.thresholds)\n        widget._redraw.assert_called()\n\n    @_patch_recompute_som\n    def test_cell_sizes(self):\n        widget = self.widget\n        widget._draw_same_color = widget._draw_pie_charts = \\\n            widget._draw_colored_circles = draw = Mock()\n\n        widget.size_by_instances = False\n        self.send_signal(widget.Inputs.data, self.iris)\n\n        widget.size_by_instances = True\n        widget.pie_charts = False\n        widget.controls.size_by_instances.toggled.emit(True)\n        sizes = draw.call_args[0][0]  # pylint: disable=unsubscriptable-object\n        self.assertAlmostEqual(sizes[0, 0], 0.8 * (30 / 100))\n        self.assertAlmostEqual(sizes[0, 1], 0.8 * (20 / 100))\n        self.assertAlmostEqual(sizes[1, 2], 0.8)\n        sizes[0, 0] = sizes[0, 1] = sizes[1, 2] = 0\n        self.assertTrue(np.all(sizes == 0))\n\n        widget.size_by_instances = False\n        widget.controls.size_by_instances.toggled.emit(False)\n        sizes = draw.call_args[0][0]  # pylint: disable=unsubscriptable-object\n        self.assertAlmostEqual(sizes[0, 0], 0.8)\n        self.assertAlmostEqual(sizes[0, 1], 0.8)\n        self.assertAlmostEqual(sizes[0, 1], 0.8)\n        sizes[0, 0] = sizes[0, 1] = sizes[1, 2] = 0\n        self.assertTrue(np.all(sizes == 0))\n\n    @_patch_recompute_som\n    def test_same_color_same_size(self):\n        widget = self.widget\n\n        widget.size_by_instances = False\n\n        self.send_signal(widget.Inputs.data, self.iris)\n        widget.attr_color = None\n        widget.controls.attr_color.activated[int].emit(0)\n        a = widget.elements.childItems()[0]\n        for e, t in zip(widget.elements.childItems(),\n                        [(0, 0, 0.4),\n                         (0.5, np.sqrt(3) / 2, 0.4),\n                         (1, np.sqrt(3), 0.4)]):\n            np.testing.assert_almost_equal((e.x(), e.y(), e.r), t)\n            self.assertEqual(e.color.getRgb(), a.color.getRgb())\n\n        widget.opt_controls.shape.setCurrentIndex(1)\n        widget.opt_controls.start.clicked.emit()\n        a = widget.elements.childItems()[0]\n        for e, t in zip(widget.elements.childItems(),\n                        [(0, 0, 0.4), (0, 1, 0.4), (1, 2, 0.4)]):\n            np.testing.assert_almost_equal((e.x(), e.y(), e.r), t)\n            self.assertEqual(e.color.getRgb(), a.color.getRgb())\n\n    @_patch_recompute_som\n    def test_diff_color_same_size(self):\n        widget = self.widget\n\n        widget.size_by_instances = False\n\n        widget.opt_controls.shape.setCurrentIndex(0)\n        self.send_signal(widget.Inputs.data, self.iris)\n        a, b, c = widget.elements.childItems()\n        for e, t in zip(widget.elements.childItems(),\n                        [(0, 0, 0.4),\n                         (0.5, np.sqrt(3) / 2, 0.4),\n                         (1, np.sqrt(3), 0.4)]):\n            np.testing.assert_almost_equal((e.x(), e.y(), e.r), t)\n        self.assertEqual(a.color.getRgb(), b.color.getRgb())\n        self.assertNotEqual(a.color.getRgb(), c.color.getRgb())\n\n        widget.opt_controls.shape.setCurrentIndex(1)\n        widget.opt_controls.start.clicked.emit()\n        a, b, c = widget.elements.childItems()\n        for e, t in zip(widget.elements.childItems(),\n                        [(0, 0, 0.4), (0, 1, 0.4), (1, 2, 0.4)]):\n            np.testing.assert_almost_equal((e.x(), e.y(), e.r), t)\n        self.assertEqual(a.color.getRgb(), b.color.getRgb())\n        self.assertNotEqual(a.color.getRgb(), c.color.getRgb())\n\n    @_patch_recompute_som\n    def test_same_color_diff_size(self):\n        widget = self.widget\n\n        widget.size_by_instances = True\n        widget.opt_controls.shape.setCurrentIndex(0)\n\n        self.send_signal(widget.Inputs.data, self.iris)\n        widget.attr_color = None\n        widget.controls.attr_color.activated[int].emit(0)\n        a = widget.elements.childItems()[0]\n        for e, t in zip(widget.elements.childItems(),\n                        [(0, 0, 0.4 * (30 / 100)),\n                         (0.5, np.sqrt(3) / 2, 0.4 * (20 / 100)),\n                         (1, np.sqrt(3), 0.4)]):\n            np.testing.assert_almost_equal((e.x(), e.y(), e.r), t)\n            self.assertEqual(e.color.getRgb(), a.color.getRgb())\n\n        widget.opt_controls.shape.setCurrentIndex(1)\n        widget.opt_controls.start.clicked.emit()\n        a = widget.elements.childItems()[0]\n        for e, t in zip(widget.elements.childItems(),\n                        [(0, 0, 0.4 * 30 / 100),\n                         (0, 1, 0.4 * 20 / 100),\n                         (1, 2, 0.4)]):\n            np.testing.assert_almost_equal((e.x(), e.y(), e.r), t)\n            self.assertEqual(e.color.getRgb(), a.color.getRgb())\n\n    @_patch_recompute_som\n    def test_diff_color_diff_size(self):\n        widget = self.widget\n\n        widget.size_by_instances = True\n\n        widget.opt_controls.shape.setCurrentIndex(0)\n        self.send_signal(widget.Inputs.data, self.iris)\n        a, b, c = widget.elements.childItems()\n        for e, t in zip(widget.elements.childItems(),\n                        [(0, 0, 0.4 * 30 / 100),\n                         (0.5, np.sqrt(3) / 2, 0.4 * 20 / 100),\n                         (1, np.sqrt(3), 0.4)]):\n            np.testing.assert_almost_equal((e.x(), e.y(), e.r), t)\n        self.assertEqual(a.color.getRgb(), b.color.getRgb())\n        self.assertNotEqual(a.color.getRgb(), c.color.getRgb())\n\n        widget.opt_controls.shape.setCurrentIndex(1)\n        widget.opt_controls.start.clicked.emit()\n        a, b, c = widget.elements.childItems()\n        for e, t in zip(widget.elements.childItems(),\n                        [(0, 0, 0.4 * 30 / 100),\n                         (0, 1, 0.4 * 20 / 100),\n                         (1, 2, 0.4)]):\n            np.testing.assert_almost_equal((e.x(), e.y(), e.r), t)\n        self.assertEqual(a.color.getRgb(), b.color.getRgb())\n        self.assertNotEqual(a.color.getRgb(), c.color.getRgb())\n\n    @_patch_recompute_som\n    def test_pie_charts(self):\n        widget = self.widget\n\n        widget.size_by_instances = True\n\n        widget.opt_controls.shape.setCurrentIndex(0)\n        self.send_signal(widget.Inputs.data, self.iris)\n\n        widget.pie_charts = True\n        widget.controls.pie_charts.toggled.emit(True)\n\n        a, b, c = widget.elements.childItems()\n        for e, (x, y, r) in zip(widget.elements.childItems(),\n                                [(0, 0, 0.8 * 30 / 100),\n                                 (0.5, np.sqrt(3) / 2, 0.8 * 20 / 100),\n                                 (1, np.sqrt(3), 0.8)]):\n            self.assertEqual(e.x(), x)\n            self.assertEqual(e.y(), y)\n            self.assertEqual(e.r, r / 2)\n            self.assertEqual(len(e.colors), len(widget.colors) + 1)\n        np.testing.assert_equal(a.dist, [1, 0, 0, 0])\n        np.testing.assert_equal(b.dist, [1, 0, 0, 0])\n        np.testing.assert_equal(c.dist, [0, 0.5, 0.5, 0])\n\n        widget.opt_controls.shape.setCurrentIndex(1)\n        widget.opt_controls.start.clicked.emit()\n        for e, (x, y, r) in zip(widget.elements.childItems(),\n                                [(0, 0, 0.8 * 30 / 100),\n                                 (0, 1, 0.8 * 20 / 100),\n                                 (1, 2, 0.8)]):\n            self.assertEqual(e.x(), x)\n            self.assertEqual(e.y(), y)\n            self.assertEqual(e.r, r / 2)\n\n        with self.iris.unlocked():\n            self.iris.Y[:15] = np.nan\n        self.send_signal(widget.Inputs.data, self.iris)\n        a = widget.elements.childItems()[0]\n        np.testing.assert_equal(a.dist, [0.5, 0, 0, 0.5])\n\n    def test_get_color_column(self):\n        widget = self.widget\n\n        table = Table(\"heart_disease\")\n        domain = table.domain\n        new_domain = Domain(\n            domain.attributes[3:], domain.class_var, domain.attributes[:3])\n        new_table = table.transform(new_domain).copy()\n        with new_table.unlocked(new_table.metas):\n            new_table.metas = new_table.metas.astype(object)\n        self.send_signal(widget.Inputs.data, new_table)\n\n        # discrete attribute\n        widget.attr_color = domain[\"rest ECG\"]\n        np.testing.assert_equal(\n            widget._get_color_column(),\n            widget.data.get_column_view(\"rest ECG\")[0].astype(int))\n\n        # discrete meta\n        widget.attr_color = domain[\"gender\"]\n        np.testing.assert_equal(\n            widget._get_color_column(),\n            widget.data.get_column_view(\"gender\")[0].astype(int))\n\n\n        # numeric attribute\n        widget.thresholds = np.array([120, 150])\n        widget.attr_color = domain[\"max HR\"]\n        for c, d in zip(widget._get_color_column(),\n                        widget.data.get_column_view(\"max HR\")[0]):\n            if d < 120:\n                self.assertEqual(c, 0)\n            if 120 <= d < 150:\n                self.assertEqual(c, 1)\n            if d >= 150:\n                self.assertEqual(c, 2)\n\n        # numeric meta\n        widget.thresholds = np.array([50, 60])\n        widget.attr_color = domain[\"age\"]\n        for c, d in zip(widget._get_color_column(),\n                        widget.data.get_column_view(\"age\")[0]):\n            if d < 50:\n                self.assertEqual(c, 0)\n            if 50 <= d < 60:\n                self.assertEqual(c, 1)\n            if d >= 60:\n                self.assertEqual(c, 2)\n\n        # discrete meta with missing values\n        widget.attr_color = domain[\"gender\"]\n        with widget.data.unlocked():\n            col = widget.data.get_column_view(\"gender\")[0]\n            col[:5] = np.nan\n        col = col.copy()\n        col[:5] = 2\n        np.testing.assert_equal(widget._get_color_column(), col)\n\n    @_patch_recompute_som\n    def test_colored_circles_with_missing_values(self):\n        with self.iris.unlocked():\n            self.iris.get_column_view(\"iris\")[0][:5] = np.nan\n        self.send_signal(self.widget.Inputs.data, self.iris)\n        self.assertTrue(self.widget.Warning.missing_colors.is_shown())\n\n        self.send_signal(self.widget.Inputs.data, None)\n        self.assertFalse(self.widget.Warning.missing_colors.is_shown())\n\n    def test_send_report(self):\n        self.widget.send_report()\n\n        self.send_signal(self.widget.Inputs.data, self.iris)\n        self.widget.send_report()\n\n    @_patch_recompute_som\n    def test_on_selection_change(self):\n        def selm(*cells):\n            m = np.zeros((widget.size_x, widget.size_y), dtype=bool)\n            for x, y in cells:\n                m[x, y] = True\n            return m\n\n        widget = self.widget\n        self.send_signal(self.widget.Inputs.data, self.iris)\n\n        widget.redraw_selection = Mock()\n        widget.update_output = Mock()\n\n        m = selm((0, 0)).astype(int)\n        widget.on_selection_change(selm((0, 0)))\n        np.testing.assert_equal(widget.selection, m)\n        widget.redraw_selection.assert_called_once()\n        widget.update_output.assert_called_once()\n\n        m = selm((0, 1)).astype(int)\n        widget.on_selection_change(selm((0, 1)))\n        np.testing.assert_equal(widget.selection, m)\n\n        m[0, 0] = 1\n        widget.on_selection_change(selm((0, 0)), SomView.SelectionAddToGroup)\n        np.testing.assert_equal(widget.selection, m)\n\n        m[0, 0] = 0\n        widget.on_selection_change(selm((0, 0)), SomView.SelectionRemove)\n        np.testing.assert_equal(widget.selection, m)\n\n        m[0, 0] = 2\n        widget.on_selection_change(selm((0, 0)), SomView.SelectionNewGroup)\n        np.testing.assert_equal(widget.selection, m)\n\n    @_patch_recompute_som\n    def test_on_selection_change_on_empty(self):\n        \"\"\"Test clicks on empty scene, when no data\"\"\"\n        widget = self.widget\n        widget.on_selection_change([])\n\n    @_patch_recompute_som\n    def test_output(self):\n        widget = self.widget\n        self.send_signal(self.widget.Inputs.data, self.iris)\n\n        self.assertIsNone(self.get_output(widget.Outputs.selected_data))\n        out = self.get_output(widget.Outputs.annotated_data)\n        self.assertEqual(len(out), 150)\n        self.assertTrue(\n            np.all(out.get_column_view(ANNOTATED_DATA_FEATURE_NAME)[0] == 0))\n\n        m = np.zeros((widget.size_x, widget.size_y), dtype=bool)\n        m[0, 0] = True\n        widget.on_selection_change(m)\n        out = self.get_output(widget.Outputs.selected_data)\n        np.testing.assert_equal(out.ids, self.iris.ids[:30])\n\n        out = self.get_output(widget.Outputs.annotated_data)\n        np.testing.assert_equal(\n            out.get_column_view(ANNOTATED_DATA_FEATURE_NAME)[0],\n            [1] * 30 + [0] * 120)\n\n        m[0, 0] = False\n        m[0, 1] = True\n        widget.on_selection_change(m, SomView.SelectionNewGroup)\n        out = self.get_output(widget.Outputs.selected_data)\n        np.testing.assert_equal(out.ids, self.iris.ids[:50])\n\n        out = self.get_output(widget.Outputs.annotated_data)\n        np.testing.assert_equal(\n            out.get_column_view(ANNOTATED_DATA_FEATURE_NAME)[0],\n            [0] * 30 + [1] * 20 + [2] * 100)\n\n        self.send_signal(self.widget.Inputs.data, None)\n        self.assertIsNone(self.get_output(widget.Outputs.selected_data))\n        self.assertIsNone(self.get_output(widget.Outputs.annotated_data))\n\n\nif __name__ == \"__main__\":\n    unittest.main()\n",
            "new_file_content": "# pylint: disable=protected-access\n\nimport unittest\nfrom unittest.mock import Mock, patch\n\nimport numpy as np\nimport scipy.sparse as sp\n\nfrom Orange.data import Table, Domain\nfrom Orange.widgets.tests.base import WidgetTest\nfrom Orange.widgets.utils.annotated_data import ANNOTATED_DATA_FEATURE_NAME\nfrom Orange.widgets.unsupervised.owsom import OWSOM, SomView, SOM\n\n\ndef _patch_recompute_som(meth):\n    def winners_from_weights(cont_x, *_1, **_2):\n        n = cont_x.shape[0]\n        w = np.zeros((n, 2), dtype=int)\n        w[n // 5:] = [0, 1]\n        w[n // 3:] = [1, 2]\n        return w\n\n    def recompute(self):\n        if not self.data:\n            return\n        self._assign_instances(None, None)\n        self._redraw()\n        self.update_output()\n\n    def patched(*args, **kwargs):\n        with patch.object(SOM, \"winner_from_weights\", new=winners_from_weights), \\\n                patch.object(OWSOM, \"_recompute_som\", new=recompute):\n            meth(*args, **kwargs)\n\n    return patched\n\n\nclass TestOWSOM(WidgetTest):\n    def setUp(self):\n        self.widget = self.create_widget(OWSOM)  #: OWSOM\n        self.iris = Table(\"iris\")\n\n    def tearDown(self):\n        self.widget.onDeleteWidget()\n        super().tearDown()\n\n    def test_requires_continuous(self):\n        widget = self.widget\n        heart = Table(\"heart_disease\")\n\n        self.send_signal(widget.Inputs.data, Table(\"zoo\"))\n        self.assertTrue(widget.Error.no_numeric_variables.is_shown())\n        self.assertFalse(widget.Warning.ignoring_disc_variables.is_shown())\n        self.assertIsNone(widget.data)\n        self.assertIsNone(widget.cont_x)\n\n        self.send_signal(widget.Inputs.data, heart)\n        self.assertFalse(widget.Error.no_numeric_variables.is_shown())\n        self.assertTrue(widget.Warning.ignoring_disc_variables.is_shown())\n        self.assertEqual(\n            widget.cont_x.shape[1],\n            sum(attr.is_continuous for attr in heart.domain.attributes))\n\n        self.send_signal(widget.Inputs.data, self.iris)\n        self.assertFalse(widget.Error.no_numeric_variables.is_shown())\n        self.assertFalse(widget.Warning.ignoring_disc_variables.is_shown())\n        self.assertEqual(widget.cont_x.shape, (150, 4))\n\n    def test_single_attribute(self):\n        widget = self.widget\n        self.send_signal(widget.Inputs.data, self.iris)\n        self.assertFalse(widget.Warning.single_attribute.is_shown())\n        iris = self.iris[:, 0]\n        self.send_signal(widget.Inputs.data, iris)\n        self.assertTrue(widget.Warning.single_attribute.is_shown())\n\n    def test_missing_all_data(self):\n        widget = self.widget\n\n        self.send_signal(widget.Inputs.data, Table(\"heart_disease\"))\n        self.assertTrue(widget.Warning.ignoring_disc_variables.is_shown())\n\n        with self.iris.unlocked():\n            for i in range(150):\n                self.iris.X[i, i % 4] = np.nan\n\n        self.send_signal(widget.Inputs.data, self.iris)\n        self.assertTrue(widget.Error.no_defined_rows.is_shown())\n        self.assertFalse(widget.Warning.ignoring_disc_variables.is_shown())\n        self.assertIsNone(widget.data)\n        self.assertIsNone(widget.cont_x)\n\n        self.send_signal(widget.Inputs.data, None)\n        self.assertFalse(widget.Warning.ignoring_disc_variables.is_shown())\n\n    def test_missing_some_data(self):\n        widget = self.widget\n        with self.iris.unlocked():\n            self.iris.X[:50, 0] = np.nan\n\n        self.send_signal(widget.Inputs.data, self.iris)\n        self.assertFalse(widget.Error.no_defined_rows.is_shown())\n        self.assertTrue(widget.Warning.missing_values.is_shown())\n        np.testing.assert_almost_equal(\n            widget.data.Y.flatten(), [1] * 50 + [2] * 50)\n        self.assertEqual(widget.cont_x.shape, (100, 4))\n\n        self.send_signal(widget.Inputs.data, None)\n        self.assertFalse(widget.Warning.missing_values.is_shown())\n\n    def test_missing_one_row_data(self):\n        widget = self.widget\n        with self.iris.unlocked():\n            self.iris.X[5, 0] = np.nan\n\n        self.send_signal(widget.Inputs.data, self.iris)\n        self.assertFalse(widget.Error.no_defined_rows.is_shown())\n        self.assertTrue(widget.Warning.missing_values.is_shown())\n\n        self.send_signal(widget.Inputs.data, None)\n        self.assertFalse(widget.Warning.missing_values.is_shown())\n\n    @_patch_recompute_som\n    def test_sparse_data(self):\n        widget = self.widget\n        with self.iris.unlocked():\n            self.iris.X = sp.csc_matrix(self.iris.X)\n\n        # Table.from_table can decide to return dense data\n        with patch.object(Table, \"from_table\", lambda _, x: x):\n            # update_output would fail because of above patch\n            widget.update_output = Mock()\n            self.send_signal(widget.Inputs.data, self.iris)\n        self.assertIs(widget.data, self.iris)\n        self.assertTrue(sp.isspmatrix_csr(widget.cont_x))\n        self.assertEqual(widget.cont_x.shape, (150, 4))\n\n    def test_auto_compute_dimensions(self):\n        widget = self.widget\n        self.send_signal(widget.Inputs.data, self.iris)\n        spin_x = widget.opt_controls.spin_x\n        spin_y = widget.opt_controls.spin_y\n\n        spin_x.setValue(100)\n        spin_y.setValue(100)\n        widget.auto_dimension = True\n        widget.controls.auto_dimension.toggled.emit(True)\n        self.assertLess(spin_x.value(), 100)\n        self.assertLess(spin_y.value(), 100)\n\n        spin_x.setValue(9)\n        spin_y.setValue(7)\n        widget.auto_dimension = False\n        widget.controls.auto_dimension.toggled.emit(False)\n        self.assertEqual(spin_x.value(), 10)\n        self.assertEqual(spin_y.value(), 5)\n\n    @_patch_recompute_som\n    def test_redraw_grid(self):\n        widget = self.widget\n\n        widget.auto_dimension = False\n\n        widget.opt_controls.spin_x.setValue(8)\n        widget.opt_controls.spin_y.setValue(5)\n        widget.opt_controls.shape.setCurrentIndex(1)\n        self.send_signal(widget.Inputs.data, self.iris)\n        self.assertEqual(widget.grid_cells.shape, (5, 8))\n        self.assertEqual(\n            sum(c is not None for c in widget.grid_cells.ravel()), 40)\n\n        widget.opt_controls.shape.setCurrentIndex(0)\n        widget.opt_controls.start.clicked.emit()\n        self.assertEqual(widget.grid_cells.shape, (5, 8))\n        self.assertEqual(\n            sum(c is not None for c in widget.grid_cells.ravel()), 38)\n        self.assertIsNone(widget.grid_cells[1, 7])\n        self.assertIsNone(widget.grid_cells[3, 7])\n\n        widget.opt_controls.shape.setCurrentIndex(1)\n        widget.opt_controls.start.clicked.emit()\n        self.assertEqual(widget.grid_cells.shape, (5, 8))\n        self.assertEqual(\n            sum(c is not None for c in widget.grid_cells.ravel()), 40)\n\n    def test_attr_color_change(self):\n        widget = self.widget\n        widget._redraw = Mock()\n\n        heart = Table(\"heart_disease\")\n        self.send_signal(widget.Inputs.data, heart)\n\n        widget._redraw.reset_mock()\n        combo = widget.controls.attr_color\n        combo.setCurrentIndex(0)\n        combo.activated[int].emit(0)\n        self.assertFalse(widget.controls.pie_charts.isEnabled())\n        self.assertIsNone(widget.colors)\n        self.assertIsNone(widget.thresholds)\n        widget._redraw.assert_called()\n\n        widget._redraw.reset_mock()\n        combo = widget.controls.attr_color\n        gender = heart.domain[\"gender\"]\n        ind_gen = combo.model().indexOf(gender)\n        combo.setCurrentIndex(ind_gen)\n        combo.activated[int].emit(ind_gen)\n        self.assertTrue(widget.controls.pie_charts.isEnabled())\n        np.testing.assert_equal(widget.colors.palette, gender.colors)\n        self.assertIsNone(widget.thresholds)\n        widget._redraw.assert_called()\n\n        widget._redraw.reset_mock()\n        combo = widget.controls.attr_color\n        age = heart.domain[\"age\"]\n        ind_age = combo.model().indexOf(age)\n        combo.setCurrentIndex(ind_age)\n        combo.activated[int].emit(ind_age)\n        self.assertTrue(widget.controls.pie_charts.isEnabled())\n        self.assertIsNotNone(widget.thresholds)\n        widget._redraw.assert_called()\n\n    @_patch_recompute_som\n    def test_cell_sizes(self):\n        widget = self.widget\n        widget._draw_same_color = widget._draw_pie_charts = \\\n            widget._draw_colored_circles = draw = Mock()\n\n        widget.size_by_instances = False\n        self.send_signal(widget.Inputs.data, self.iris)\n\n        widget.size_by_instances = True\n        widget.pie_charts = False\n        widget.controls.size_by_instances.toggled.emit(True)\n        sizes = draw.call_args[0][0]  # pylint: disable=unsubscriptable-object\n        self.assertAlmostEqual(sizes[0, 0], 0.8 * (30 / 100))\n        self.assertAlmostEqual(sizes[0, 1], 0.8 * (20 / 100))\n        self.assertAlmostEqual(sizes[1, 2], 0.8)\n        sizes[0, 0] = sizes[0, 1] = sizes[1, 2] = 0\n        self.assertTrue(np.all(sizes == 0))\n\n        widget.size_by_instances = False\n        widget.controls.size_by_instances.toggled.emit(False)\n        sizes = draw.call_args[0][0]  # pylint: disable=unsubscriptable-object\n        self.assertAlmostEqual(sizes[0, 0], 0.8)\n        self.assertAlmostEqual(sizes[0, 1], 0.8)\n        self.assertAlmostEqual(sizes[0, 1], 0.8)\n        sizes[0, 0] = sizes[0, 1] = sizes[1, 2] = 0\n        self.assertTrue(np.all(sizes == 0))\n\n    @_patch_recompute_som\n    def test_same_color_same_size(self):\n        widget = self.widget\n\n        widget.size_by_instances = False\n\n        self.send_signal(widget.Inputs.data, self.iris)\n        widget.attr_color = None\n        widget.controls.attr_color.activated[int].emit(0)\n        a = widget.elements.childItems()[0]\n        for e, t in zip(widget.elements.childItems(),\n                        [(0, 0, 0.4),\n                         (0.5, np.sqrt(3) / 2, 0.4),\n                         (1, np.sqrt(3), 0.4)]):\n            np.testing.assert_almost_equal((e.x(), e.y(), e.r), t)\n            self.assertEqual(e.color.getRgb(), a.color.getRgb())\n\n        widget.opt_controls.shape.setCurrentIndex(1)\n        widget.opt_controls.start.clicked.emit()\n        a = widget.elements.childItems()[0]\n        for e, t in zip(widget.elements.childItems(),\n                        [(0, 0, 0.4), (0, 1, 0.4), (1, 2, 0.4)]):\n            np.testing.assert_almost_equal((e.x(), e.y(), e.r), t)\n            self.assertEqual(e.color.getRgb(), a.color.getRgb())\n\n    @_patch_recompute_som\n    def test_diff_color_same_size(self):\n        widget = self.widget\n\n        widget.size_by_instances = False\n\n        widget.opt_controls.shape.setCurrentIndex(0)\n        self.send_signal(widget.Inputs.data, self.iris)\n        a, b, c = widget.elements.childItems()\n        for e, t in zip(widget.elements.childItems(),\n                        [(0, 0, 0.4),\n                         (0.5, np.sqrt(3) / 2, 0.4),\n                         (1, np.sqrt(3), 0.4)]):\n            np.testing.assert_almost_equal((e.x(), e.y(), e.r), t)\n        self.assertEqual(a.color.getRgb(), b.color.getRgb())\n        self.assertNotEqual(a.color.getRgb(), c.color.getRgb())\n\n        widget.opt_controls.shape.setCurrentIndex(1)\n        widget.opt_controls.start.clicked.emit()\n        a, b, c = widget.elements.childItems()\n        for e, t in zip(widget.elements.childItems(),\n                        [(0, 0, 0.4), (0, 1, 0.4), (1, 2, 0.4)]):\n            np.testing.assert_almost_equal((e.x(), e.y(), e.r), t)\n        self.assertEqual(a.color.getRgb(), b.color.getRgb())\n        self.assertNotEqual(a.color.getRgb(), c.color.getRgb())\n\n    @_patch_recompute_som\n    def test_same_color_diff_size(self):\n        widget = self.widget\n\n        widget.size_by_instances = True\n        widget.opt_controls.shape.setCurrentIndex(0)\n\n        self.send_signal(widget.Inputs.data, self.iris)\n        widget.attr_color = None\n        widget.controls.attr_color.activated[int].emit(0)\n        a = widget.elements.childItems()[0]\n        for e, t in zip(widget.elements.childItems(),\n                        [(0, 0, 0.4 * (30 / 100)),\n                         (0.5, np.sqrt(3) / 2, 0.4 * (20 / 100)),\n                         (1, np.sqrt(3), 0.4)]):\n            np.testing.assert_almost_equal((e.x(), e.y(), e.r), t)\n            self.assertEqual(e.color.getRgb(), a.color.getRgb())\n\n        widget.opt_controls.shape.setCurrentIndex(1)\n        widget.opt_controls.start.clicked.emit()\n        a = widget.elements.childItems()[0]\n        for e, t in zip(widget.elements.childItems(),\n                        [(0, 0, 0.4 * 30 / 100),\n                         (0, 1, 0.4 * 20 / 100),\n                         (1, 2, 0.4)]):\n            np.testing.assert_almost_equal((e.x(), e.y(), e.r), t)\n            self.assertEqual(e.color.getRgb(), a.color.getRgb())\n\n    @_patch_recompute_som\n    def test_diff_color_diff_size(self):\n        widget = self.widget\n\n        widget.size_by_instances = True\n\n        widget.opt_controls.shape.setCurrentIndex(0)\n        self.send_signal(widget.Inputs.data, self.iris)\n        a, b, c = widget.elements.childItems()\n        for e, t in zip(widget.elements.childItems(),\n                        [(0, 0, 0.4 * 30 / 100),\n                         (0.5, np.sqrt(3) / 2, 0.4 * 20 / 100),\n                         (1, np.sqrt(3), 0.4)]):\n            np.testing.assert_almost_equal((e.x(), e.y(), e.r), t)\n        self.assertEqual(a.color.getRgb(), b.color.getRgb())\n        self.assertNotEqual(a.color.getRgb(), c.color.getRgb())\n\n        widget.opt_controls.shape.setCurrentIndex(1)\n        widget.opt_controls.start.clicked.emit()\n        a, b, c = widget.elements.childItems()\n        for e, t in zip(widget.elements.childItems(),\n                        [(0, 0, 0.4 * 30 / 100),\n                         (0, 1, 0.4 * 20 / 100),\n                         (1, 2, 0.4)]):\n            np.testing.assert_almost_equal((e.x(), e.y(), e.r), t)\n        self.assertEqual(a.color.getRgb(), b.color.getRgb())\n        self.assertNotEqual(a.color.getRgb(), c.color.getRgb())\n\n    @_patch_recompute_som\n    def test_pie_charts(self):\n        widget = self.widget\n\n        widget.size_by_instances = True\n\n        widget.opt_controls.shape.setCurrentIndex(0)\n        self.send_signal(widget.Inputs.data, self.iris)\n\n        widget.pie_charts = True\n        widget.controls.pie_charts.toggled.emit(True)\n\n        a, b, c = widget.elements.childItems()\n        for e, (x, y, r) in zip(widget.elements.childItems(),\n                                [(0, 0, 0.8 * 30 / 100),\n                                 (0.5, np.sqrt(3) / 2, 0.8 * 20 / 100),\n                                 (1, np.sqrt(3), 0.8)]):\n            self.assertEqual(e.x(), x)\n            self.assertEqual(e.y(), y)\n            self.assertEqual(e.r, r / 2)\n            self.assertEqual(len(e.colors), len(widget.colors) + 1)\n        np.testing.assert_equal(a.dist, [1, 0, 0, 0])\n        np.testing.assert_equal(b.dist, [1, 0, 0, 0])\n        np.testing.assert_equal(c.dist, [0, 0.5, 0.5, 0])\n\n        widget.opt_controls.shape.setCurrentIndex(1)\n        widget.opt_controls.start.clicked.emit()\n        for e, (x, y, r) in zip(widget.elements.childItems(),\n                                [(0, 0, 0.8 * 30 / 100),\n                                 (0, 1, 0.8 * 20 / 100),\n                                 (1, 2, 0.8)]):\n            self.assertEqual(e.x(), x)\n            self.assertEqual(e.y(), y)\n            self.assertEqual(e.r, r / 2)\n\n        with self.iris.unlocked():\n            self.iris.Y[:15] = np.nan\n        self.send_signal(widget.Inputs.data, self.iris)\n        a = widget.elements.childItems()[0]\n        np.testing.assert_equal(a.dist, [0.5, 0, 0, 0.5])\n\n    def test_get_color_column(self):\n        widget = self.widget\n\n        table = Table(\"heart_disease\")\n        domain = table.domain\n        new_domain = Domain(\n            domain.attributes[3:], domain.class_var, domain.attributes[:3])\n        new_table = table.transform(new_domain).copy()\n        with new_table.unlocked(new_table.metas):\n            new_table.metas = new_table.metas.astype(object)\n        self.send_signal(widget.Inputs.data, new_table)\n\n        # discrete attribute\n        widget.attr_color = domain[\"rest ECG\"]\n        np.testing.assert_equal(\n            widget._get_color_column(),\n            widget.data.get_column_view(\"rest ECG\")[0].astype(int))\n\n        # discrete meta\n        widget.attr_color = domain[\"gender\"]\n        np.testing.assert_equal(\n            widget._get_color_column(),\n            widget.data.get_column_view(\"gender\")[0].astype(int))\n\n\n        # numeric attribute\n        widget.thresholds = np.array([120, 150])\n        widget.attr_color = domain[\"max HR\"]\n        for c, d in zip(widget._get_color_column(),\n                        widget.data.get_column_view(\"max HR\")[0]):\n            if d < 120:\n                self.assertEqual(c, 0)\n            if 120 <= d < 150:\n                self.assertEqual(c, 1)\n            if d >= 150:\n                self.assertEqual(c, 2)\n\n        # numeric meta\n        widget.thresholds = np.array([50, 60])\n        widget.attr_color = domain[\"age\"]\n        for c, d in zip(widget._get_color_column(),\n                        widget.data.get_column_view(\"age\")[0]):\n            if d < 50:\n                self.assertEqual(c, 0)\n            if 50 <= d < 60:\n                self.assertEqual(c, 1)\n            if d >= 60:\n                self.assertEqual(c, 2)\n\n        # discrete meta with missing values\n        widget.attr_color = domain[\"gender\"]\n        with widget.data.unlocked():\n            col = widget.data.get_column_view(\"gender\")[0]\n            col[:5] = np.nan\n        col = col.copy()\n        col[:5] = 2\n        np.testing.assert_equal(widget._get_color_column(), col)\n\n    @_patch_recompute_som\n    def test_colored_circles_with_missing_values(self):\n        with self.iris.unlocked():\n            self.iris.get_column_view(\"iris\")[0][:5] = np.nan\n        self.send_signal(self.widget.Inputs.data, self.iris)\n        self.assertTrue(self.widget.Warning.missing_colors.is_shown())\n\n        self.send_signal(self.widget.Inputs.data, None)\n        self.assertFalse(self.widget.Warning.missing_colors.is_shown())\n\n    def test_send_report(self):\n        self.widget.send_report()\n\n        self.send_signal(self.widget.Inputs.data, self.iris)\n        self.widget.send_report()\n\n    @_patch_recompute_som\n    def test_on_selection_change(self):\n        def selm(*cells):\n            m = np.zeros((widget.size_x, widget.size_y), dtype=bool)\n            for x, y in cells:\n                m[x, y] = True\n            return m\n\n        widget = self.widget\n        self.send_signal(self.widget.Inputs.data, self.iris)\n\n        widget.redraw_selection = Mock()\n        widget.update_output = Mock()\n\n        m = selm((0, 0)).astype(int)\n        widget.on_selection_change(selm((0, 0)))\n        np.testing.assert_equal(widget.selection, m)\n        widget.redraw_selection.assert_called_once()\n        widget.update_output.assert_called_once()\n\n        m = selm((0, 1)).astype(int)\n        widget.on_selection_change(selm((0, 1)))\n        np.testing.assert_equal(widget.selection, m)\n\n        m[0, 0] = 1\n        widget.on_selection_change(selm((0, 0)), SomView.SelectionAddToGroup)\n        np.testing.assert_equal(widget.selection, m)\n\n        m[0, 0] = 0\n        widget.on_selection_change(selm((0, 0)), SomView.SelectionRemove)\n        np.testing.assert_equal(widget.selection, m)\n\n        m[0, 0] = 2\n        widget.on_selection_change(selm((0, 0)), SomView.SelectionNewGroup)\n        np.testing.assert_equal(widget.selection, m)\n\n    @_patch_recompute_som\n    def test_on_selection_change_on_empty(self):\n        \"\"\"Test clicks on empty scene, when no data\"\"\"\n        widget = self.widget\n        widget.on_selection_change([])\n\n    @_patch_recompute_som\n    def test_output(self):\n        widget = self.widget\n        self.send_signal(self.widget.Inputs.data, self.iris)\n\n        self.assertIsNone(self.get_output(widget.Outputs.selected_data))\n        out = self.get_output(widget.Outputs.annotated_data)\n        self.assertEqual(len(out), 150)\n        self.assertTrue(\n            np.all(out.get_column_view(ANNOTATED_DATA_FEATURE_NAME)[0] == 0))\n\n        m = np.zeros((widget.size_x, widget.size_y), dtype=bool)\n        m[0, 0] = True\n        widget.on_selection_change(m)\n        out = self.get_output(widget.Outputs.selected_data)\n        np.testing.assert_equal(out.ids, self.iris.ids[:30])\n\n        out = self.get_output(widget.Outputs.annotated_data)\n        np.testing.assert_equal(\n            out.get_column_view(ANNOTATED_DATA_FEATURE_NAME)[0],\n            [1] * 30 + [0] * 120)\n\n        m[0, 0] = False\n        m[0, 1] = True\n        widget.on_selection_change(m, SomView.SelectionNewGroup)\n        out = self.get_output(widget.Outputs.selected_data)\n        np.testing.assert_equal(out.ids, self.iris.ids[:50])\n\n        out = self.get_output(widget.Outputs.annotated_data)\n        np.testing.assert_equal(\n            out.get_column_view(ANNOTATED_DATA_FEATURE_NAME)[0],\n            [0] * 30 + [1] * 20 + [2] * 100)\n\n        self.send_signal(self.widget.Inputs.data, None)\n        self.assertIsNone(self.get_output(widget.Outputs.selected_data))\n        self.assertIsNone(self.get_output(widget.Outputs.annotated_data))\n\n    def test_invalidated(self):\n        heart = Table(\"heart_disease\")\n        self.widget._recompute_som = Mock()\n\n        # New data - replot\n        self.send_signal(self.widget.Inputs.data, heart)\n        self.widget._recompute_som.assert_called_once()\n\n        # Same data - no replot\n        self.widget._recompute_som.reset_mock()\n        self.send_signal(self.widget.Inputs.data, heart)\n        self.widget._recompute_som.assert_not_called()\n\n        # Same data.X - no replot\n        domain = heart.domain\n        domain = Domain(domain.attributes, metas=domain.class_vars)\n        heart_with_metas = self.iris.transform(domain)\n        self.widget._recompute_som.reset_mock()\n        self.send_signal(self.widget.Inputs.data, heart_with_metas)\n        self.widget._recompute_som.assert_not_called()\n\n        # Different data, same set of cont. vars - no replot\n        attrs = [a for a in heart.domain.attributes if a.is_continuous]\n        domain = Domain(attrs)\n        heart_with_cont_features = self.iris.transform(domain)\n        self.widget._recompute_som.reset_mock()\n        self.send_signal(self.widget.Inputs.data, heart_with_cont_features)\n        self.widget._recompute_som.assert_not_called()\n\n        # Different data.X - replot\n        domain = Domain(heart.domain.attributes[:5])\n        heart_with_less_features = heart.transform(domain)\n        self.widget._recompute_som.reset_mock()\n        self.send_signal(self.widget.Inputs.data, heart_with_less_features)\n        self.widget._recompute_som.assert_called_once()\n\n\nif __name__ == \"__main__\":\n    unittest.main()\n",
            "header": {
                "file": {
                    "path": "Orange/widgets/unsupervised/tests/test_owsom.py"
                },
                "misc_line": null
            },
            "index_line": {
                "old_commit_hash": "7d6630b38",
                "new_commit_hash": "d37e83eac",
                "mode": "100644"
            },
            "is_binary_file": false,
            "binary_line": null,
            "minus_file": {
                "path": "a/Orange/widgets/unsupervised/tests/test_owsom.py"
            },
            "plus_file": {
                "path": "b/Orange/widgets/unsupervised/tests/test_owsom.py"
            },
            "hunks": [
                {
                    "descriptor": {
                        "old_range": {
                            "start": 549,
                            "length": 6
                        },
                        "new_range": {
                            "start": 549,
                            "length": 42
                        },
                        "section": "class TestOWSOM(WidgetTest):"
                    },
                    "line_group": {
                        "all_lines": [
                            {
                                "content": "        self.assertIsNone(self.get_output(widget.Outputs.selected_data))",
                                "type": "context"
                            },
                            {
                                "content": "        self.assertIsNone(self.get_output(widget.Outputs.annotated_data))",
                                "type": "context"
                            },
                            {
                                "content": "",
                                "type": "context"
                            },
                            {
                                "content": "    def test_invalidated(self):",
                                "type": "added"
                            },
                            {
                                "content": "        heart = Table(\"heart_disease\")",
                                "type": "added"
                            },
                            {
                                "content": "        self.widget._recompute_som = Mock()",
                                "type": "added"
                            },
                            {
                                "content": "",
                                "type": "added"
                            },
                            {
                                "content": "        # New data - replot",
                                "type": "added"
                            },
                            {
                                "content": "        self.send_signal(self.widget.Inputs.data, heart)",
                                "type": "added"
                            },
                            {
                                "content": "        self.widget._recompute_som.assert_called_once()",
                                "type": "added"
                            },
                            {
                                "content": "",
                                "type": "added"
                            },
                            {
                                "content": "        # Same data - no replot",
                                "type": "added"
                            },
                            {
                                "content": "        self.widget._recompute_som.reset_mock()",
                                "type": "added"
                            },
                            {
                                "content": "        self.send_signal(self.widget.Inputs.data, heart)",
                                "type": "added"
                            },
                            {
                                "content": "        self.widget._recompute_som.assert_not_called()",
                                "type": "added"
                            },
                            {
                                "content": "",
                                "type": "added"
                            },
                            {
                                "content": "        # Same data.X - no replot",
                                "type": "added"
                            },
                            {
                                "content": "        domain = heart.domain",
                                "type": "added"
                            },
                            {
                                "content": "        domain = Domain(domain.attributes, metas=domain.class_vars)",
                                "type": "added"
                            },
                            {
                                "content": "        heart_with_metas = self.iris.transform(domain)",
                                "type": "added"
                            },
                            {
                                "content": "        self.widget._recompute_som.reset_mock()",
                                "type": "added"
                            },
                            {
                                "content": "        self.send_signal(self.widget.Inputs.data, heart_with_metas)",
                                "type": "added"
                            },
                            {
                                "content": "        self.widget._recompute_som.assert_not_called()",
                                "type": "added"
                            },
                            {
                                "content": "",
                                "type": "added"
                            },
                            {
                                "content": "        # Different data, same set of cont. vars - no replot",
                                "type": "added"
                            },
                            {
                                "content": "        attrs = [a for a in heart.domain.attributes if a.is_continuous]",
                                "type": "added"
                            },
                            {
                                "content": "        domain = Domain(attrs)",
                                "type": "added"
                            },
                            {
                                "content": "        heart_with_cont_features = self.iris.transform(domain)",
                                "type": "added"
                            },
                            {
                                "content": "        self.widget._recompute_som.reset_mock()",
                                "type": "added"
                            },
                            {
                                "content": "        self.send_signal(self.widget.Inputs.data, heart_with_cont_features)",
                                "type": "added"
                            },
                            {
                                "content": "        self.widget._recompute_som.assert_not_called()",
                                "type": "added"
                            },
                            {
                                "content": "",
                                "type": "added"
                            },
                            {
                                "content": "        # Different data.X - replot",
                                "type": "added"
                            },
                            {
                                "content": "        domain = Domain(heart.domain.attributes[:5])",
                                "type": "added"
                            },
                            {
                                "content": "        heart_with_less_features = heart.transform(domain)",
                                "type": "added"
                            },
                            {
                                "content": "        self.widget._recompute_som.reset_mock()",
                                "type": "added"
                            },
                            {
                                "content": "        self.send_signal(self.widget.Inputs.data, heart_with_less_features)",
                                "type": "added"
                            },
                            {
                                "content": "        self.widget._recompute_som.assert_called_once()",
                                "type": "added"
                            },
                            {
                                "content": "",
                                "type": "added"
                            },
                            {
                                "content": "",
                                "type": "context"
                            },
                            {
                                "content": "if __name__ == \"__main__\":",
                                "type": "context"
                            },
                            {
                                "content": "    unittest.main()",
                                "type": "context"
                            },
                            {
                                "content": "",
                                "type": "context"
                            }
                        ]
                    },
                    "modified_entities": [
                        {
                            "file_name": "Orange/widgets/unsupervised/tests/test_owsom.py",
                            "type": "class",
                            "name": "TestOWSOM",
                            "content": "class TestOWSOM(WidgetTest):\n    def setUp(self):\n        self.widget = self.create_widget(OWSOM)  #: OWSOM\n        self.iris = Table(\"iris\")\n\n    def tearDown(self):\n        self.widget.onDeleteWidget()\n        super().tearDown()\n\n    def test_requires_continuous(self):\n        widget = self.widget\n        heart = Table(\"heart_disease\")\n\n        self.send_signal(widget.Inputs.data, Table(\"zoo\"))\n        self.assertTrue(widget.Error.no_numeric_variables.is_shown())\n        self.assertFalse(widget.Warning.ignoring_disc_variables.is_shown())\n        self.assertIsNone(widget.data)\n        self.assertIsNone(widget.cont_x)\n\n        self.send_signal(widget.Inputs.data, heart)\n        self.assertFalse(widget.Error.no_numeric_variables.is_shown())\n        self.assertTrue(widget.Warning.ignoring_disc_variables.is_shown())\n        self.assertEqual(\n            widget.cont_x.shape[1],\n            sum(attr.is_continuous for attr in heart.domain.attributes))\n\n        self.send_signal(widget.Inputs.data, self.iris)\n        self.assertFalse(widget.Error.no_numeric_variables.is_shown())\n        self.assertFalse(widget.Warning.ignoring_disc_variables.is_shown())\n        self.assertEqual(widget.cont_x.shape, (150, 4))\n\n    def test_single_attribute(self):\n        widget = self.widget\n        self.send_signal(widget.Inputs.data, self.iris)\n        self.assertFalse(widget.Warning.single_attribute.is_shown())\n        iris = self.iris[:, 0]\n        self.send_signal(widget.Inputs.data, iris)\n        self.assertTrue(widget.Warning.single_attribute.is_shown())\n\n    def test_missing_all_data(self):\n        widget = self.widget\n\n        self.send_signal(widget.Inputs.data, Table(\"heart_disease\"))\n        self.assertTrue(widget.Warning.ignoring_disc_variables.is_shown())\n\n        with self.iris.unlocked():\n            for i in range(150):\n                self.iris.X[i, i % 4] = np.nan\n\n        self.send_signal(widget.Inputs.data, self.iris)\n        self.assertTrue(widget.Error.no_defined_rows.is_shown())\n        self.assertFalse(widget.Warning.ignoring_disc_variables.is_shown())\n        self.assertIsNone(widget.data)\n        self.assertIsNone(widget.cont_x)\n\n        self.send_signal(widget.Inputs.data, None)\n        self.assertFalse(widget.Warning.ignoring_disc_variables.is_shown())\n\n    def test_missing_some_data(self):\n        widget = self.widget\n        with self.iris.unlocked():\n            self.iris.X[:50, 0] = np.nan\n\n        self.send_signal(widget.Inputs.data, self.iris)\n        self.assertFalse(widget.Error.no_defined_rows.is_shown())\n        self.assertTrue(widget.Warning.missing_values.is_shown())\n        np.testing.assert_almost_equal(\n            widget.data.Y.flatten(), [1] * 50 + [2] * 50)\n        self.assertEqual(widget.cont_x.shape, (100, 4))\n\n        self.send_signal(widget.Inputs.data, None)\n        self.assertFalse(widget.Warning.missing_values.is_shown())\n\n    def test_missing_one_row_data(self):\n        widget = self.widget\n        with self.iris.unlocked():\n            self.iris.X[5, 0] = np.nan\n\n        self.send_signal(widget.Inputs.data, self.iris)\n        self.assertFalse(widget.Error.no_defined_rows.is_shown())\n        self.assertTrue(widget.Warning.missing_values.is_shown())\n\n        self.send_signal(widget.Inputs.data, None)\n        self.assertFalse(widget.Warning.missing_values.is_shown())\n\n    @_patch_recompute_som\n    def test_sparse_data(self):\n        widget = self.widget\n        with self.iris.unlocked():\n            self.iris.X = sp.csc_matrix(self.iris.X)\n\n        # Table.from_table can decide to return dense data\n        with patch.object(Table, \"from_table\", lambda _, x: x):\n            # update_output would fail because of above patch\n            widget.update_output = Mock()\n            self.send_signal(widget.Inputs.data, self.iris)\n        self.assertIs(widget.data, self.iris)\n        self.assertTrue(sp.isspmatrix_csr(widget.cont_x))\n        self.assertEqual(widget.cont_x.shape, (150, 4))\n\n    def test_auto_compute_dimensions(self):\n        widget = self.widget\n        self.send_signal(widget.Inputs.data, self.iris)\n        spin_x = widget.opt_controls.spin_x\n        spin_y = widget.opt_controls.spin_y\n\n        spin_x.setValue(100)\n        spin_y.setValue(100)\n        widget.auto_dimension = True\n        widget.controls.auto_dimension.toggled.emit(True)\n        self.assertLess(spin_x.value(), 100)\n        self.assertLess(spin_y.value(), 100)\n\n        spin_x.setValue(9)\n        spin_y.setValue(7)\n        widget.auto_dimension = False\n        widget.controls.auto_dimension.toggled.emit(False)\n        self.assertEqual(spin_x.value(), 10)\n        self.assertEqual(spin_y.value(), 5)\n\n    @_patch_recompute_som\n    def test_redraw_grid(self):\n        widget = self.widget\n\n        widget.auto_dimension = False\n\n        widget.opt_controls.spin_x.setValue(8)\n        widget.opt_controls.spin_y.setValue(5)\n        widget.opt_controls.shape.setCurrentIndex(1)\n        self.send_signal(widget.Inputs.data, self.iris)\n        self.assertEqual(widget.grid_cells.shape, (5, 8))\n        self.assertEqual(\n            sum(c is not None for c in widget.grid_cells.ravel()), 40)\n\n        widget.opt_controls.shape.setCurrentIndex(0)\n        widget.opt_controls.start.clicked.emit()\n        self.assertEqual(widget.grid_cells.shape, (5, 8))\n        self.assertEqual(\n            sum(c is not None for c in widget.grid_cells.ravel()), 38)\n        self.assertIsNone(widget.grid_cells[1, 7])\n        self.assertIsNone(widget.grid_cells[3, 7])\n\n        widget.opt_controls.shape.setCurrentIndex(1)\n        widget.opt_controls.start.clicked.emit()\n        self.assertEqual(widget.grid_cells.shape, (5, 8))\n        self.assertEqual(\n            sum(c is not None for c in widget.grid_cells.ravel()), 40)\n\n    def test_attr_color_change(self):\n        widget = self.widget\n        widget._redraw = Mock()\n\n        heart = Table(\"heart_disease\")\n        self.send_signal(widget.Inputs.data, heart)\n\n        widget._redraw.reset_mock()\n        combo = widget.controls.attr_color\n        combo.setCurrentIndex(0)\n        combo.activated[int].emit(0)\n        self.assertFalse(widget.controls.pie_charts.isEnabled())\n        self.assertIsNone(widget.colors)\n        self.assertIsNone(widget.thresholds)\n        widget._redraw.assert_called()\n\n        widget._redraw.reset_mock()\n        combo = widget.controls.attr_color\n        gender = heart.domain[\"gender\"]\n        ind_gen = combo.model().indexOf(gender)\n        combo.setCurrentIndex(ind_gen)\n        combo.activated[int].emit(ind_gen)\n        self.assertTrue(widget.controls.pie_charts.isEnabled())\n        np.testing.assert_equal(widget.colors.palette, gender.colors)\n        self.assertIsNone(widget.thresholds)\n        widget._redraw.assert_called()\n\n        widget._redraw.reset_mock()\n        combo = widget.controls.attr_color\n        age = heart.domain[\"age\"]\n        ind_age = combo.model().indexOf(age)\n        combo.setCurrentIndex(ind_age)\n        combo.activated[int].emit(ind_age)\n        self.assertTrue(widget.controls.pie_charts.isEnabled())\n        self.assertIsNotNone(widget.thresholds)\n        widget._redraw.assert_called()\n\n    @_patch_recompute_som\n    def test_cell_sizes(self):\n        widget = self.widget\n        widget._draw_same_color = widget._draw_pie_charts = \\\n            widget._draw_colored_circles = draw = Mock()\n\n        widget.size_by_instances = False\n        self.send_signal(widget.Inputs.data, self.iris)\n\n        widget.size_by_instances = True\n        widget.pie_charts = False\n        widget.controls.size_by_instances.toggled.emit(True)\n        sizes = draw.call_args[0][0]  # pylint: disable=unsubscriptable-object\n        self.assertAlmostEqual(sizes[0, 0], 0.8 * (30 / 100))\n        self.assertAlmostEqual(sizes[0, 1], 0.8 * (20 / 100))\n        self.assertAlmostEqual(sizes[1, 2], 0.8)\n        sizes[0, 0] = sizes[0, 1] = sizes[1, 2] = 0\n        self.assertTrue(np.all(sizes == 0))\n\n        widget.size_by_instances = False\n        widget.controls.size_by_instances.toggled.emit(False)\n        sizes = draw.call_args[0][0]  # pylint: disable=unsubscriptable-object\n        self.assertAlmostEqual(sizes[0, 0], 0.8)\n        self.assertAlmostEqual(sizes[0, 1], 0.8)\n        self.assertAlmostEqual(sizes[0, 1], 0.8)\n        sizes[0, 0] = sizes[0, 1] = sizes[1, 2] = 0\n        self.assertTrue(np.all(sizes == 0))\n\n    @_patch_recompute_som\n    def test_same_color_same_size(self):\n        widget = self.widget\n\n        widget.size_by_instances = False\n\n        self.send_signal(widget.Inputs.data, self.iris)\n        widget.attr_color = None\n        widget.controls.attr_color.activated[int].emit(0)\n        a = widget.elements.childItems()[0]\n        for e, t in zip(widget.elements.childItems(),\n                        [(0, 0, 0.4),\n                         (0.5, np.sqrt(3) / 2, 0.4),\n                         (1, np.sqrt(3), 0.4)]):\n            np.testing.assert_almost_equal((e.x(), e.y(), e.r), t)\n            self.assertEqual(e.color.getRgb(), a.color.getRgb())\n\n        widget.opt_controls.shape.setCurrentIndex(1)\n        widget.opt_controls.start.clicked.emit()\n        a = widget.elements.childItems()[0]\n        for e, t in zip(widget.elements.childItems(),\n                        [(0, 0, 0.4), (0, 1, 0.4), (1, 2, 0.4)]):\n            np.testing.assert_almost_equal((e.x(), e.y(), e.r), t)\n            self.assertEqual(e.color.getRgb(), a.color.getRgb())\n\n    @_patch_recompute_som\n    def test_diff_color_same_size(self):\n        widget = self.widget\n\n        widget.size_by_instances = False\n\n        widget.opt_controls.shape.setCurrentIndex(0)\n        self.send_signal(widget.Inputs.data, self.iris)\n        a, b, c = widget.elements.childItems()\n        for e, t in zip(widget.elements.childItems(),\n                        [(0, 0, 0.4),\n                         (0.5, np.sqrt(3) / 2, 0.4),\n                         (1, np.sqrt(3), 0.4)]):\n            np.testing.assert_almost_equal((e.x(), e.y(), e.r), t)\n        self.assertEqual(a.color.getRgb(), b.color.getRgb())\n        self.assertNotEqual(a.color.getRgb(), c.color.getRgb())\n\n        widget.opt_controls.shape.setCurrentIndex(1)\n        widget.opt_controls.start.clicked.emit()\n        a, b, c = widget.elements.childItems()\n        for e, t in zip(widget.elements.childItems(),\n                        [(0, 0, 0.4), (0, 1, 0.4), (1, 2, 0.4)]):\n            np.testing.assert_almost_equal((e.x(), e.y(), e.r), t)\n        self.assertEqual(a.color.getRgb(), b.color.getRgb())\n        self.assertNotEqual(a.color.getRgb(), c.color.getRgb())\n\n    @_patch_recompute_som\n    def test_same_color_diff_size(self):\n        widget = self.widget\n\n        widget.size_by_instances = True\n        widget.opt_controls.shape.setCurrentIndex(0)\n\n        self.send_signal(widget.Inputs.data, self.iris)\n        widget.attr_color = None\n        widget.controls.attr_color.activated[int].emit(0)\n        a = widget.elements.childItems()[0]\n        for e, t in zip(widget.elements.childItems(),\n                        [(0, 0, 0.4 * (30 / 100)),\n                         (0.5, np.sqrt(3) / 2, 0.4 * (20 / 100)),\n                         (1, np.sqrt(3), 0.4)]):\n            np.testing.assert_almost_equal((e.x(), e.y(), e.r), t)\n            self.assertEqual(e.color.getRgb(), a.color.getRgb())\n\n        widget.opt_controls.shape.setCurrentIndex(1)\n        widget.opt_controls.start.clicked.emit()\n        a = widget.elements.childItems()[0]\n        for e, t in zip(widget.elements.childItems(),\n                        [(0, 0, 0.4 * 30 / 100),\n                         (0, 1, 0.4 * 20 / 100),\n                         (1, 2, 0.4)]):\n            np.testing.assert_almost_equal((e.x(), e.y(), e.r), t)\n            self.assertEqual(e.color.getRgb(), a.color.getRgb())\n\n    @_patch_recompute_som\n    def test_diff_color_diff_size(self):\n        widget = self.widget\n\n        widget.size_by_instances = True\n\n        widget.opt_controls.shape.setCurrentIndex(0)\n        self.send_signal(widget.Inputs.data, self.iris)\n        a, b, c = widget.elements.childItems()\n        for e, t in zip(widget.elements.childItems(),\n                        [(0, 0, 0.4 * 30 / 100),\n                         (0.5, np.sqrt(3) / 2, 0.4 * 20 / 100),\n                         (1, np.sqrt(3), 0.4)]):\n            np.testing.assert_almost_equal((e.x(), e.y(), e.r), t)\n        self.assertEqual(a.color.getRgb(), b.color.getRgb())\n        self.assertNotEqual(a.color.getRgb(), c.color.getRgb())\n\n        widget.opt_controls.shape.setCurrentIndex(1)\n        widget.opt_controls.start.clicked.emit()\n        a, b, c = widget.elements.childItems()\n        for e, t in zip(widget.elements.childItems(),\n                        [(0, 0, 0.4 * 30 / 100),\n                         (0, 1, 0.4 * 20 / 100),\n                         (1, 2, 0.4)]):\n            np.testing.assert_almost_equal((e.x(), e.y(), e.r), t)\n        self.assertEqual(a.color.getRgb(), b.color.getRgb())\n        self.assertNotEqual(a.color.getRgb(), c.color.getRgb())\n\n    @_patch_recompute_som\n    def test_pie_charts(self):\n        widget = self.widget\n\n        widget.size_by_instances = True\n\n        widget.opt_controls.shape.setCurrentIndex(0)\n        self.send_signal(widget.Inputs.data, self.iris)\n\n        widget.pie_charts = True\n        widget.controls.pie_charts.toggled.emit(True)\n\n        a, b, c = widget.elements.childItems()\n        for e, (x, y, r) in zip(widget.elements.childItems(),\n                                [(0, 0, 0.8 * 30 / 100),\n                                 (0.5, np.sqrt(3) / 2, 0.8 * 20 / 100),\n                                 (1, np.sqrt(3), 0.8)]):\n            self.assertEqual(e.x(), x)\n            self.assertEqual(e.y(), y)\n            self.assertEqual(e.r, r / 2)\n            self.assertEqual(len(e.colors), len(widget.colors) + 1)\n        np.testing.assert_equal(a.dist, [1, 0, 0, 0])\n        np.testing.assert_equal(b.dist, [1, 0, 0, 0])\n        np.testing.assert_equal(c.dist, [0, 0.5, 0.5, 0])\n\n        widget.opt_controls.shape.setCurrentIndex(1)\n        widget.opt_controls.start.clicked.emit()\n        for e, (x, y, r) in zip(widget.elements.childItems(),\n                                [(0, 0, 0.8 * 30 / 100),\n                                 (0, 1, 0.8 * 20 / 100),\n                                 (1, 2, 0.8)]):\n            self.assertEqual(e.x(), x)\n            self.assertEqual(e.y(), y)\n            self.assertEqual(e.r, r / 2)\n\n        with self.iris.unlocked():\n            self.iris.Y[:15] = np.nan\n        self.send_signal(widget.Inputs.data, self.iris)\n        a = widget.elements.childItems()[0]\n        np.testing.assert_equal(a.dist, [0.5, 0, 0, 0.5])\n\n    def test_get_color_column(self):\n        widget = self.widget\n\n        table = Table(\"heart_disease\")\n        domain = table.domain\n        new_domain = Domain(\n            domain.attributes[3:], domain.class_var, domain.attributes[:3])\n        new_table = table.transform(new_domain).copy()\n        with new_table.unlocked(new_table.metas):\n            new_table.metas = new_table.metas.astype(object)\n        self.send_signal(widget.Inputs.data, new_table)\n\n        # discrete attribute\n        widget.attr_color = domain[\"rest ECG\"]\n        np.testing.assert_equal(\n            widget._get_color_column(),\n            widget.data.get_column_view(\"rest ECG\")[0].astype(int))\n\n        # discrete meta\n        widget.attr_color = domain[\"gender\"]\n        np.testing.assert_equal(\n            widget._get_color_column(),\n            widget.data.get_column_view(\"gender\")[0].astype(int))\n\n\n        # numeric attribute\n        widget.thresholds = np.array([120, 150])\n        widget.attr_color = domain[\"max HR\"]\n        for c, d in zip(widget._get_color_column(),\n                        widget.data.get_column_view(\"max HR\")[0]):\n            if d < 120:\n                self.assertEqual(c, 0)\n            if 120 <= d < 150:\n                self.assertEqual(c, 1)\n            if d >= 150:\n                self.assertEqual(c, 2)\n\n        # numeric meta\n        widget.thresholds = np.array([50, 60])\n        widget.attr_color = domain[\"age\"]\n        for c, d in zip(widget._get_color_column(),\n                        widget.data.get_column_view(\"age\")[0]):\n            if d < 50:\n                self.assertEqual(c, 0)\n            if 50 <= d < 60:\n                self.assertEqual(c, 1)\n            if d >= 60:\n                self.assertEqual(c, 2)\n\n        # discrete meta with missing values\n        widget.attr_color = domain[\"gender\"]\n        with widget.data.unlocked():\n            col = widget.data.get_column_view(\"gender\")[0]\n            col[:5] = np.nan\n        col = col.copy()\n        col[:5] = 2\n        np.testing.assert_equal(widget._get_color_column(), col)\n\n    @_patch_recompute_som\n    def test_colored_circles_with_missing_values(self):\n        with self.iris.unlocked():\n            self.iris.get_column_view(\"iris\")[0][:5] = np.nan\n        self.send_signal(self.widget.Inputs.data, self.iris)\n        self.assertTrue(self.widget.Warning.missing_colors.is_shown())\n\n        self.send_signal(self.widget.Inputs.data, None)\n        self.assertFalse(self.widget.Warning.missing_colors.is_shown())\n\n    def test_send_report(self):\n        self.widget.send_report()\n\n        self.send_signal(self.widget.Inputs.data, self.iris)\n        self.widget.send_report()\n\n    @_patch_recompute_som\n    def test_on_selection_change(self):\n        def selm(*cells):\n            m = np.zeros((widget.size_x, widget.size_y), dtype=bool)\n            for x, y in cells:\n                m[x, y] = True\n            return m\n\n        widget = self.widget\n        self.send_signal(self.widget.Inputs.data, self.iris)\n\n        widget.redraw_selection = Mock()\n        widget.update_output = Mock()\n\n        m = selm((0, 0)).astype(int)\n        widget.on_selection_change(selm((0, 0)))\n        np.testing.assert_equal(widget.selection, m)\n        widget.redraw_selection.assert_called_once()\n        widget.update_output.assert_called_once()\n\n        m = selm((0, 1)).astype(int)\n        widget.on_selection_change(selm((0, 1)))\n        np.testing.assert_equal(widget.selection, m)\n\n        m[0, 0] = 1\n        widget.on_selection_change(selm((0, 0)), SomView.SelectionAddToGroup)\n        np.testing.assert_equal(widget.selection, m)\n\n        m[0, 0] = 0\n        widget.on_selection_change(selm((0, 0)), SomView.SelectionRemove)\n        np.testing.assert_equal(widget.selection, m)\n\n        m[0, 0] = 2\n        widget.on_selection_change(selm((0, 0)), SomView.SelectionNewGroup)\n        np.testing.assert_equal(widget.selection, m)\n\n    @_patch_recompute_som\n    def test_on_selection_change_on_empty(self):\n        \"\"\"Test clicks on empty scene, when no data\"\"\"\n        widget = self.widget\n        widget.on_selection_change([])\n\n    @_patch_recompute_som\n    def test_output(self):\n        widget = self.widget\n        self.send_signal(self.widget.Inputs.data, self.iris)\n\n        self.assertIsNone(self.get_output(widget.Outputs.selected_data))\n        out = self.get_output(widget.Outputs.annotated_data)\n        self.assertEqual(len(out), 150)\n        self.assertTrue(\n            np.all(out.get_column_view(ANNOTATED_DATA_FEATURE_NAME)[0] == 0))\n\n        m = np.zeros((widget.size_x, widget.size_y), dtype=bool)\n        m[0, 0] = True\n        widget.on_selection_change(m)\n        out = self.get_output(widget.Outputs.selected_data)\n        np.testing.assert_equal(out.ids, self.iris.ids[:30])\n\n        out = self.get_output(widget.Outputs.annotated_data)\n        np.testing.assert_equal(\n            out.get_column_view(ANNOTATED_DATA_FEATURE_NAME)[0],\n            [1] * 30 + [0] * 120)\n\n        m[0, 0] = False\n        m[0, 1] = True\n        widget.on_selection_change(m, SomView.SelectionNewGroup)\n        out = self.get_output(widget.Outputs.selected_data)\n        np.testing.assert_equal(out.ids, self.iris.ids[:50])\n\n        out = self.get_output(widget.Outputs.annotated_data)\n        np.testing.assert_equal(\n            out.get_column_view(ANNOTATED_DATA_FEATURE_NAME)[0],\n            [0] * 30 + [1] * 20 + [2] * 100)\n\n        self.send_signal(self.widget.Inputs.data, None)\n        self.assertIsNone(self.get_output(widget.Outputs.selected_data))\n        self.assertIsNone(self.get_output(widget.Outputs.annotated_data))\n\n    def test_invalidated(self):\n        heart = Table(\"heart_disease\")\n        self.widget._recompute_som = Mock()\n\n        # New data - replot\n        self.send_signal(self.widget.Inputs.data, heart)\n        self.widget._recompute_som.assert_called_once()\n\n        # Same data - no replot\n        self.widget._recompute_som.reset_mock()\n        self.send_signal(self.widget.Inputs.data, heart)\n        self.widget._recompute_som.assert_not_called()\n\n        # Same data.X - no replot\n        domain = heart.domain\n        domain = Domain(domain.attributes, metas=domain.class_vars)\n        heart_with_metas = self.iris.transform(domain)\n        self.widget._recompute_som.reset_mock()\n        self.send_signal(self.widget.Inputs.data, heart_with_metas)\n        self.widget._recompute_som.assert_not_called()\n\n        # Different data, same set of cont. vars - no replot\n        attrs = [a for a in heart.domain.attributes if a.is_continuous]\n        domain = Domain(attrs)\n        heart_with_cont_features = self.iris.transform(domain)\n        self.widget._recompute_som.reset_mock()\n        self.send_signal(self.widget.Inputs.data, heart_with_cont_features)\n        self.widget._recompute_som.assert_not_called()\n\n        # Different data.X - replot\n        domain = Domain(heart.domain.attributes[:5])\n        heart_with_less_features = heart.transform(domain)\n        self.widget._recompute_som.reset_mock()\n        self.send_signal(self.widget.Inputs.data, heart_with_less_features)\n        self.widget._recompute_som.assert_called_once()",
                            "ast_type_str": "ClassDef",
                            "start_lineno": 38,
                            "end_lineno": 586,
                            "parent": null
                        }
                    ],
                    "added_entities": [
                        {
                            "file_name": "Orange/widgets/unsupervised/tests/test_owsom.py",
                            "type": "method",
                            "name": "TestOWSOM.test_invalidated",
                            "content": "class TestOWSOM(WidgetTest):\n    def setUp(self):\n        self.widget = self.create_widget(OWSOM)  #: OWSOM\n        self.iris = Table(\"iris\")\n\n    def tearDown(self):\n        self.widget.onDeleteWidget()\n        super().tearDown()\n\n    def test_requires_continuous(self):\n        widget = self.widget\n        heart = Table(\"heart_disease\")\n\n        self.send_signal(widget.Inputs.data, Table(\"zoo\"))\n        self.assertTrue(widget.Error.no_numeric_variables.is_shown())\n        self.assertFalse(widget.Warning.ignoring_disc_variables.is_shown())\n        self.assertIsNone(widget.data)\n        self.assertIsNone(widget.cont_x)\n\n        self.send_signal(widget.Inputs.data, heart)\n        self.assertFalse(widget.Error.no_numeric_variables.is_shown())\n        self.assertTrue(widget.Warning.ignoring_disc_variables.is_shown())\n        self.assertEqual(\n            widget.cont_x.shape[1],\n            sum(attr.is_continuous for attr in heart.domain.attributes))\n\n        self.send_signal(widget.Inputs.data, self.iris)\n        self.assertFalse(widget.Error.no_numeric_variables.is_shown())\n        self.assertFalse(widget.Warning.ignoring_disc_variables.is_shown())\n        self.assertEqual(widget.cont_x.shape, (150, 4))\n\n    def test_single_attribute(self):\n        widget = self.widget\n        self.send_signal(widget.Inputs.data, self.iris)\n        self.assertFalse(widget.Warning.single_attribute.is_shown())\n        iris = self.iris[:, 0]\n        self.send_signal(widget.Inputs.data, iris)\n        self.assertTrue(widget.Warning.single_attribute.is_shown())\n\n    def test_missing_all_data(self):\n        widget = self.widget\n\n        self.send_signal(widget.Inputs.data, Table(\"heart_disease\"))\n        self.assertTrue(widget.Warning.ignoring_disc_variables.is_shown())\n\n        with self.iris.unlocked():\n            for i in range(150):\n                self.iris.X[i, i % 4] = np.nan\n\n        self.send_signal(widget.Inputs.data, self.iris)\n        self.assertTrue(widget.Error.no_defined_rows.is_shown())\n        self.assertFalse(widget.Warning.ignoring_disc_variables.is_shown())\n        self.assertIsNone(widget.data)\n        self.assertIsNone(widget.cont_x)\n\n        self.send_signal(widget.Inputs.data, None)\n        self.assertFalse(widget.Warning.ignoring_disc_variables.is_shown())\n\n    def test_missing_some_data(self):\n        widget = self.widget\n        with self.iris.unlocked():\n            self.iris.X[:50, 0] = np.nan\n\n        self.send_signal(widget.Inputs.data, self.iris)\n        self.assertFalse(widget.Error.no_defined_rows.is_shown())\n        self.assertTrue(widget.Warning.missing_values.is_shown())\n        np.testing.assert_almost_equal(\n            widget.data.Y.flatten(), [1] * 50 + [2] * 50)\n        self.assertEqual(widget.cont_x.shape, (100, 4))\n\n        self.send_signal(widget.Inputs.data, None)\n        self.assertFalse(widget.Warning.missing_values.is_shown())\n\n    def test_missing_one_row_data(self):\n        widget = self.widget\n        with self.iris.unlocked():\n            self.iris.X[5, 0] = np.nan\n\n        self.send_signal(widget.Inputs.data, self.iris)\n        self.assertFalse(widget.Error.no_defined_rows.is_shown())\n        self.assertTrue(widget.Warning.missing_values.is_shown())\n\n        self.send_signal(widget.Inputs.data, None)\n        self.assertFalse(widget.Warning.missing_values.is_shown())\n\n    @_patch_recompute_som\n    def test_sparse_data(self):\n        widget = self.widget\n        with self.iris.unlocked():\n            self.iris.X = sp.csc_matrix(self.iris.X)\n\n        # Table.from_table can decide to return dense data\n        with patch.object(Table, \"from_table\", lambda _, x: x):\n            # update_output would fail because of above patch\n            widget.update_output = Mock()\n            self.send_signal(widget.Inputs.data, self.iris)\n        self.assertIs(widget.data, self.iris)\n        self.assertTrue(sp.isspmatrix_csr(widget.cont_x))\n        self.assertEqual(widget.cont_x.shape, (150, 4))\n\n    def test_auto_compute_dimensions(self):\n        widget = self.widget\n        self.send_signal(widget.Inputs.data, self.iris)\n        spin_x = widget.opt_controls.spin_x\n        spin_y = widget.opt_controls.spin_y\n\n        spin_x.setValue(100)\n        spin_y.setValue(100)\n        widget.auto_dimension = True\n        widget.controls.auto_dimension.toggled.emit(True)\n        self.assertLess(spin_x.value(), 100)\n        self.assertLess(spin_y.value(), 100)\n\n        spin_x.setValue(9)\n        spin_y.setValue(7)\n        widget.auto_dimension = False\n        widget.controls.auto_dimension.toggled.emit(False)\n        self.assertEqual(spin_x.value(), 10)\n        self.assertEqual(spin_y.value(), 5)\n\n    @_patch_recompute_som\n    def test_redraw_grid(self):\n        widget = self.widget\n\n        widget.auto_dimension = False\n\n        widget.opt_controls.spin_x.setValue(8)\n        widget.opt_controls.spin_y.setValue(5)\n        widget.opt_controls.shape.setCurrentIndex(1)\n        self.send_signal(widget.Inputs.data, self.iris)\n        self.assertEqual(widget.grid_cells.shape, (5, 8))\n        self.assertEqual(\n            sum(c is not None for c in widget.grid_cells.ravel()), 40)\n\n        widget.opt_controls.shape.setCurrentIndex(0)\n        widget.opt_controls.start.clicked.emit()\n        self.assertEqual(widget.grid_cells.shape, (5, 8))\n        self.assertEqual(\n            sum(c is not None for c in widget.grid_cells.ravel()), 38)\n        self.assertIsNone(widget.grid_cells[1, 7])\n        self.assertIsNone(widget.grid_cells[3, 7])\n\n        widget.opt_controls.shape.setCurrentIndex(1)\n        widget.opt_controls.start.clicked.emit()\n        self.assertEqual(widget.grid_cells.shape, (5, 8))\n        self.assertEqual(\n            sum(c is not None for c in widget.grid_cells.ravel()), 40)\n\n    def test_attr_color_change(self):\n        widget = self.widget\n        widget._redraw = Mock()\n\n        heart = Table(\"heart_disease\")\n        self.send_signal(widget.Inputs.data, heart)\n\n        widget._redraw.reset_mock()\n        combo = widget.controls.attr_color\n        combo.setCurrentIndex(0)\n        combo.activated[int].emit(0)\n        self.assertFalse(widget.controls.pie_charts.isEnabled())\n        self.assertIsNone(widget.colors)\n        self.assertIsNone(widget.thresholds)\n        widget._redraw.assert_called()\n\n        widget._redraw.reset_mock()\n        combo = widget.controls.attr_color\n        gender = heart.domain[\"gender\"]\n        ind_gen = combo.model().indexOf(gender)\n        combo.setCurrentIndex(ind_gen)\n        combo.activated[int].emit(ind_gen)\n        self.assertTrue(widget.controls.pie_charts.isEnabled())\n        np.testing.assert_equal(widget.colors.palette, gender.colors)\n        self.assertIsNone(widget.thresholds)\n        widget._redraw.assert_called()\n\n        widget._redraw.reset_mock()\n        combo = widget.controls.attr_color\n        age = heart.domain[\"age\"]\n        ind_age = combo.model().indexOf(age)\n        combo.setCurrentIndex(ind_age)\n        combo.activated[int].emit(ind_age)\n        self.assertTrue(widget.controls.pie_charts.isEnabled())\n        self.assertIsNotNone(widget.thresholds)\n        widget._redraw.assert_called()\n\n    @_patch_recompute_som\n    def test_cell_sizes(self):\n        widget = self.widget\n        widget._draw_same_color = widget._draw_pie_charts = \\\n            widget._draw_colored_circles = draw = Mock()\n\n        widget.size_by_instances = False\n        self.send_signal(widget.Inputs.data, self.iris)\n\n        widget.size_by_instances = True\n        widget.pie_charts = False\n        widget.controls.size_by_instances.toggled.emit(True)\n        sizes = draw.call_args[0][0]  # pylint: disable=unsubscriptable-object\n        self.assertAlmostEqual(sizes[0, 0], 0.8 * (30 / 100))\n        self.assertAlmostEqual(sizes[0, 1], 0.8 * (20 / 100))\n        self.assertAlmostEqual(sizes[1, 2], 0.8)\n        sizes[0, 0] = sizes[0, 1] = sizes[1, 2] = 0\n        self.assertTrue(np.all(sizes == 0))\n\n        widget.size_by_instances = False\n        widget.controls.size_by_instances.toggled.emit(False)\n        sizes = draw.call_args[0][0]  # pylint: disable=unsubscriptable-object\n        self.assertAlmostEqual(sizes[0, 0], 0.8)\n        self.assertAlmostEqual(sizes[0, 1], 0.8)\n        self.assertAlmostEqual(sizes[0, 1], 0.8)\n        sizes[0, 0] = sizes[0, 1] = sizes[1, 2] = 0\n        self.assertTrue(np.all(sizes == 0))\n\n    @_patch_recompute_som\n    def test_same_color_same_size(self):\n        widget = self.widget\n\n        widget.size_by_instances = False\n\n        self.send_signal(widget.Inputs.data, self.iris)\n        widget.attr_color = None\n        widget.controls.attr_color.activated[int].emit(0)\n        a = widget.elements.childItems()[0]\n        for e, t in zip(widget.elements.childItems(),\n                        [(0, 0, 0.4),\n                         (0.5, np.sqrt(3) / 2, 0.4),\n                         (1, np.sqrt(3), 0.4)]):\n            np.testing.assert_almost_equal((e.x(), e.y(), e.r), t)\n            self.assertEqual(e.color.getRgb(), a.color.getRgb())\n\n        widget.opt_controls.shape.setCurrentIndex(1)\n        widget.opt_controls.start.clicked.emit()\n        a = widget.elements.childItems()[0]\n        for e, t in zip(widget.elements.childItems(),\n                        [(0, 0, 0.4), (0, 1, 0.4), (1, 2, 0.4)]):\n            np.testing.assert_almost_equal((e.x(), e.y(), e.r), t)\n            self.assertEqual(e.color.getRgb(), a.color.getRgb())\n\n    @_patch_recompute_som\n    def test_diff_color_same_size(self):\n        widget = self.widget\n\n        widget.size_by_instances = False\n\n        widget.opt_controls.shape.setCurrentIndex(0)\n        self.send_signal(widget.Inputs.data, self.iris)\n        a, b, c = widget.elements.childItems()\n        for e, t in zip(widget.elements.childItems(),\n                        [(0, 0, 0.4),\n                         (0.5, np.sqrt(3) / 2, 0.4),\n                         (1, np.sqrt(3), 0.4)]):\n            np.testing.assert_almost_equal((e.x(), e.y(), e.r), t)\n        self.assertEqual(a.color.getRgb(), b.color.getRgb())\n        self.assertNotEqual(a.color.getRgb(), c.color.getRgb())\n\n        widget.opt_controls.shape.setCurrentIndex(1)\n        widget.opt_controls.start.clicked.emit()\n        a, b, c = widget.elements.childItems()\n        for e, t in zip(widget.elements.childItems(),\n                        [(0, 0, 0.4), (0, 1, 0.4), (1, 2, 0.4)]):\n            np.testing.assert_almost_equal((e.x(), e.y(), e.r), t)\n        self.assertEqual(a.color.getRgb(), b.color.getRgb())\n        self.assertNotEqual(a.color.getRgb(), c.color.getRgb())\n\n    @_patch_recompute_som\n    def test_same_color_diff_size(self):\n        widget = self.widget\n\n        widget.size_by_instances = True\n        widget.opt_controls.shape.setCurrentIndex(0)\n\n        self.send_signal(widget.Inputs.data, self.iris)\n        widget.attr_color = None\n        widget.controls.attr_color.activated[int].emit(0)\n        a = widget.elements.childItems()[0]\n        for e, t in zip(widget.elements.childItems(),\n                        [(0, 0, 0.4 * (30 / 100)),\n                         (0.5, np.sqrt(3) / 2, 0.4 * (20 / 100)),\n                         (1, np.sqrt(3), 0.4)]):\n            np.testing.assert_almost_equal((e.x(), e.y(), e.r), t)\n            self.assertEqual(e.color.getRgb(), a.color.getRgb())\n\n        widget.opt_controls.shape.setCurrentIndex(1)\n        widget.opt_controls.start.clicked.emit()\n        a = widget.elements.childItems()[0]\n        for e, t in zip(widget.elements.childItems(),\n                        [(0, 0, 0.4 * 30 / 100),\n                         (0, 1, 0.4 * 20 / 100),\n                         (1, 2, 0.4)]):\n            np.testing.assert_almost_equal((e.x(), e.y(), e.r), t)\n            self.assertEqual(e.color.getRgb(), a.color.getRgb())\n\n    @_patch_recompute_som\n    def test_diff_color_diff_size(self):\n        widget = self.widget\n\n        widget.size_by_instances = True\n\n        widget.opt_controls.shape.setCurrentIndex(0)\n        self.send_signal(widget.Inputs.data, self.iris)\n        a, b, c = widget.elements.childItems()\n        for e, t in zip(widget.elements.childItems(),\n                        [(0, 0, 0.4 * 30 / 100),\n                         (0.5, np.sqrt(3) / 2, 0.4 * 20 / 100),\n                         (1, np.sqrt(3), 0.4)]):\n            np.testing.assert_almost_equal((e.x(), e.y(), e.r), t)\n        self.assertEqual(a.color.getRgb(), b.color.getRgb())\n        self.assertNotEqual(a.color.getRgb(), c.color.getRgb())\n\n        widget.opt_controls.shape.setCurrentIndex(1)\n        widget.opt_controls.start.clicked.emit()\n        a, b, c = widget.elements.childItems()\n        for e, t in zip(widget.elements.childItems(),\n                        [(0, 0, 0.4 * 30 / 100),\n                         (0, 1, 0.4 * 20 / 100),\n                         (1, 2, 0.4)]):\n            np.testing.assert_almost_equal((e.x(), e.y(), e.r), t)\n        self.assertEqual(a.color.getRgb(), b.color.getRgb())\n        self.assertNotEqual(a.color.getRgb(), c.color.getRgb())\n\n    @_patch_recompute_som\n    def test_pie_charts(self):\n        widget = self.widget\n\n        widget.size_by_instances = True\n\n        widget.opt_controls.shape.setCurrentIndex(0)\n        self.send_signal(widget.Inputs.data, self.iris)\n\n        widget.pie_charts = True\n        widget.controls.pie_charts.toggled.emit(True)\n\n        a, b, c = widget.elements.childItems()\n        for e, (x, y, r) in zip(widget.elements.childItems(),\n                                [(0, 0, 0.8 * 30 / 100),\n                                 (0.5, np.sqrt(3) / 2, 0.8 * 20 / 100),\n                                 (1, np.sqrt(3), 0.8)]):\n            self.assertEqual(e.x(), x)\n            self.assertEqual(e.y(), y)\n            self.assertEqual(e.r, r / 2)\n            self.assertEqual(len(e.colors), len(widget.colors) + 1)\n        np.testing.assert_equal(a.dist, [1, 0, 0, 0])\n        np.testing.assert_equal(b.dist, [1, 0, 0, 0])\n        np.testing.assert_equal(c.dist, [0, 0.5, 0.5, 0])\n\n        widget.opt_controls.shape.setCurrentIndex(1)\n        widget.opt_controls.start.clicked.emit()\n        for e, (x, y, r) in zip(widget.elements.childItems(),\n                                [(0, 0, 0.8 * 30 / 100),\n                                 (0, 1, 0.8 * 20 / 100),\n                                 (1, 2, 0.8)]):\n            self.assertEqual(e.x(), x)\n            self.assertEqual(e.y(), y)\n            self.assertEqual(e.r, r / 2)\n\n        with self.iris.unlocked():\n            self.iris.Y[:15] = np.nan\n        self.send_signal(widget.Inputs.data, self.iris)\n        a = widget.elements.childItems()[0]\n        np.testing.assert_equal(a.dist, [0.5, 0, 0, 0.5])\n\n    def test_get_color_column(self):\n        widget = self.widget\n\n        table = Table(\"heart_disease\")\n        domain = table.domain\n        new_domain = Domain(\n            domain.attributes[3:], domain.class_var, domain.attributes[:3])\n        new_table = table.transform(new_domain).copy()\n        with new_table.unlocked(new_table.metas):\n            new_table.metas = new_table.metas.astype(object)\n        self.send_signal(widget.Inputs.data, new_table)\n\n        # discrete attribute\n        widget.attr_color = domain[\"rest ECG\"]\n        np.testing.assert_equal(\n            widget._get_color_column(),\n            widget.data.get_column_view(\"rest ECG\")[0].astype(int))\n\n        # discrete meta\n        widget.attr_color = domain[\"gender\"]\n        np.testing.assert_equal(\n            widget._get_color_column(),\n            widget.data.get_column_view(\"gender\")[0].astype(int))\n\n\n        # numeric attribute\n        widget.thresholds = np.array([120, 150])\n        widget.attr_color = domain[\"max HR\"]\n        for c, d in zip(widget._get_color_column(),\n                        widget.data.get_column_view(\"max HR\")[0]):\n            if d < 120:\n                self.assertEqual(c, 0)\n            if 120 <= d < 150:\n                self.assertEqual(c, 1)\n            if d >= 150:\n                self.assertEqual(c, 2)\n\n        # numeric meta\n        widget.thresholds = np.array([50, 60])\n        widget.attr_color = domain[\"age\"]\n        for c, d in zip(widget._get_color_column(),\n                        widget.data.get_column_view(\"age\")[0]):\n            if d < 50:\n                self.assertEqual(c, 0)\n            if 50 <= d < 60:\n                self.assertEqual(c, 1)\n            if d >= 60:\n                self.assertEqual(c, 2)\n\n        # discrete meta with missing values\n        widget.attr_color = domain[\"gender\"]\n        with widget.data.unlocked():\n            col = widget.data.get_column_view(\"gender\")[0]\n            col[:5] = np.nan\n        col = col.copy()\n        col[:5] = 2\n        np.testing.assert_equal(widget._get_color_column(), col)\n\n    @_patch_recompute_som\n    def test_colored_circles_with_missing_values(self):\n        with self.iris.unlocked():\n            self.iris.get_column_view(\"iris\")[0][:5] = np.nan\n        self.send_signal(self.widget.Inputs.data, self.iris)\n        self.assertTrue(self.widget.Warning.missing_colors.is_shown())\n\n        self.send_signal(self.widget.Inputs.data, None)\n        self.assertFalse(self.widget.Warning.missing_colors.is_shown())\n\n    def test_send_report(self):\n        self.widget.send_report()\n\n        self.send_signal(self.widget.Inputs.data, self.iris)\n        self.widget.send_report()\n\n    @_patch_recompute_som\n    def test_on_selection_change(self):\n        def selm(*cells):\n            m = np.zeros((widget.size_x, widget.size_y), dtype=bool)\n            for x, y in cells:\n                m[x, y] = True\n            return m\n\n        widget = self.widget\n        self.send_signal(self.widget.Inputs.data, self.iris)\n\n        widget.redraw_selection = Mock()\n        widget.update_output = Mock()\n\n        m = selm((0, 0)).astype(int)\n        widget.on_selection_change(selm((0, 0)))\n        np.testing.assert_equal(widget.selection, m)\n        widget.redraw_selection.assert_called_once()\n        widget.update_output.assert_called_once()\n\n        m = selm((0, 1)).astype(int)\n        widget.on_selection_change(selm((0, 1)))\n        np.testing.assert_equal(widget.selection, m)\n\n        m[0, 0] = 1\n        widget.on_selection_change(selm((0, 0)), SomView.SelectionAddToGroup)\n        np.testing.assert_equal(widget.selection, m)\n\n        m[0, 0] = 0\n        widget.on_selection_change(selm((0, 0)), SomView.SelectionRemove)\n        np.testing.assert_equal(widget.selection, m)\n\n        m[0, 0] = 2\n        widget.on_selection_change(selm((0, 0)), SomView.SelectionNewGroup)\n        np.testing.assert_equal(widget.selection, m)\n\n    @_patch_recompute_som\n    def test_on_selection_change_on_empty(self):\n        \"\"\"Test clicks on empty scene, when no data\"\"\"\n        widget = self.widget\n        widget.on_selection_change([])\n\n    @_patch_recompute_som\n    def test_output(self):\n        widget = self.widget\n        self.send_signal(self.widget.Inputs.data, self.iris)\n\n        self.assertIsNone(self.get_output(widget.Outputs.selected_data))\n        out = self.get_output(widget.Outputs.annotated_data)\n        self.assertEqual(len(out), 150)\n        self.assertTrue(\n            np.all(out.get_column_view(ANNOTATED_DATA_FEATURE_NAME)[0] == 0))\n\n        m = np.zeros((widget.size_x, widget.size_y), dtype=bool)\n        m[0, 0] = True\n        widget.on_selection_change(m)\n        out = self.get_output(widget.Outputs.selected_data)\n        np.testing.assert_equal(out.ids, self.iris.ids[:30])\n\n        out = self.get_output(widget.Outputs.annotated_data)\n        np.testing.assert_equal(\n            out.get_column_view(ANNOTATED_DATA_FEATURE_NAME)[0],\n            [1] * 30 + [0] * 120)\n\n        m[0, 0] = False\n        m[0, 1] = True\n        widget.on_selection_change(m, SomView.SelectionNewGroup)\n        out = self.get_output(widget.Outputs.selected_data)\n        np.testing.assert_equal(out.ids, self.iris.ids[:50])\n\n        out = self.get_output(widget.Outputs.annotated_data)\n        np.testing.assert_equal(\n            out.get_column_view(ANNOTATED_DATA_FEATURE_NAME)[0],\n            [0] * 30 + [1] * 20 + [2] * 100)\n\n        self.send_signal(self.widget.Inputs.data, None)\n        self.assertIsNone(self.get_output(widget.Outputs.selected_data))\n        self.assertIsNone(self.get_output(widget.Outputs.annotated_data))\n\n    def test_invalidated(self):\n        heart = Table(\"heart_disease\")\n        self.widget._recompute_som = Mock()\n\n        # New data - replot\n        self.send_signal(self.widget.Inputs.data, heart)\n        self.widget._recompute_som.assert_called_once()\n\n        # Same data - no replot\n        self.widget._recompute_som.reset_mock()\n        self.send_signal(self.widget.Inputs.data, heart)\n        self.widget._recompute_som.assert_not_called()\n\n        # Same data.X - no replot\n        domain = heart.domain\n        domain = Domain(domain.attributes, metas=domain.class_vars)\n        heart_with_metas = self.iris.transform(domain)\n        self.widget._recompute_som.reset_mock()\n        self.send_signal(self.widget.Inputs.data, heart_with_metas)\n        self.widget._recompute_som.assert_not_called()\n\n        # Different data, same set of cont. vars - no replot\n        attrs = [a for a in heart.domain.attributes if a.is_continuous]\n        domain = Domain(attrs)\n        heart_with_cont_features = self.iris.transform(domain)\n        self.widget._recompute_som.reset_mock()\n        self.send_signal(self.widget.Inputs.data, heart_with_cont_features)\n        self.widget._recompute_som.assert_not_called()\n\n        # Different data.X - replot\n        domain = Domain(heart.domain.attributes[:5])\n        heart_with_less_features = heart.transform(domain)\n        self.widget._recompute_som.reset_mock()\n        self.send_signal(self.widget.Inputs.data, heart_with_less_features)\n        self.widget._recompute_som.assert_called_once()",
                            "ast_type_str": "ClassDef",
                            "start_lineno": 552,
                            "end_lineno": 586,
                            "parent": {
                                "file_name": "Orange/widgets/unsupervised/tests/test_owsom.py",
                                "type": "class",
                                "name": "TestOWSOM",
                                "content": "class TestOWSOM(WidgetTest):\n    def setUp(self):\n        self.widget = self.create_widget(OWSOM)  #: OWSOM\n        self.iris = Table(\"iris\")\n\n    def tearDown(self):\n        self.widget.onDeleteWidget()\n        super().tearDown()\n\n    def test_requires_continuous(self):\n        widget = self.widget\n        heart = Table(\"heart_disease\")\n\n        self.send_signal(widget.Inputs.data, Table(\"zoo\"))\n        self.assertTrue(widget.Error.no_numeric_variables.is_shown())\n        self.assertFalse(widget.Warning.ignoring_disc_variables.is_shown())\n        self.assertIsNone(widget.data)\n        self.assertIsNone(widget.cont_x)\n\n        self.send_signal(widget.Inputs.data, heart)\n        self.assertFalse(widget.Error.no_numeric_variables.is_shown())\n        self.assertTrue(widget.Warning.ignoring_disc_variables.is_shown())\n        self.assertEqual(\n            widget.cont_x.shape[1],\n            sum(attr.is_continuous for attr in heart.domain.attributes))\n\n        self.send_signal(widget.Inputs.data, self.iris)\n        self.assertFalse(widget.Error.no_numeric_variables.is_shown())\n        self.assertFalse(widget.Warning.ignoring_disc_variables.is_shown())\n        self.assertEqual(widget.cont_x.shape, (150, 4))\n\n    def test_single_attribute(self):\n        widget = self.widget\n        self.send_signal(widget.Inputs.data, self.iris)\n        self.assertFalse(widget.Warning.single_attribute.is_shown())\n        iris = self.iris[:, 0]\n        self.send_signal(widget.Inputs.data, iris)\n        self.assertTrue(widget.Warning.single_attribute.is_shown())\n\n    def test_missing_all_data(self):\n        widget = self.widget\n\n        self.send_signal(widget.Inputs.data, Table(\"heart_disease\"))\n        self.assertTrue(widget.Warning.ignoring_disc_variables.is_shown())\n\n        with self.iris.unlocked():\n            for i in range(150):\n                self.iris.X[i, i % 4] = np.nan\n\n        self.send_signal(widget.Inputs.data, self.iris)\n        self.assertTrue(widget.Error.no_defined_rows.is_shown())\n        self.assertFalse(widget.Warning.ignoring_disc_variables.is_shown())\n        self.assertIsNone(widget.data)\n        self.assertIsNone(widget.cont_x)\n\n        self.send_signal(widget.Inputs.data, None)\n        self.assertFalse(widget.Warning.ignoring_disc_variables.is_shown())\n\n    def test_missing_some_data(self):\n        widget = self.widget\n        with self.iris.unlocked():\n            self.iris.X[:50, 0] = np.nan\n\n        self.send_signal(widget.Inputs.data, self.iris)\n        self.assertFalse(widget.Error.no_defined_rows.is_shown())\n        self.assertTrue(widget.Warning.missing_values.is_shown())\n        np.testing.assert_almost_equal(\n            widget.data.Y.flatten(), [1] * 50 + [2] * 50)\n        self.assertEqual(widget.cont_x.shape, (100, 4))\n\n        self.send_signal(widget.Inputs.data, None)\n        self.assertFalse(widget.Warning.missing_values.is_shown())\n\n    def test_missing_one_row_data(self):\n        widget = self.widget\n        with self.iris.unlocked():\n            self.iris.X[5, 0] = np.nan\n\n        self.send_signal(widget.Inputs.data, self.iris)\n        self.assertFalse(widget.Error.no_defined_rows.is_shown())\n        self.assertTrue(widget.Warning.missing_values.is_shown())\n\n        self.send_signal(widget.Inputs.data, None)\n        self.assertFalse(widget.Warning.missing_values.is_shown())\n\n    @_patch_recompute_som\n    def test_sparse_data(self):\n        widget = self.widget\n        with self.iris.unlocked():\n            self.iris.X = sp.csc_matrix(self.iris.X)\n\n        # Table.from_table can decide to return dense data\n        with patch.object(Table, \"from_table\", lambda _, x: x):\n            # update_output would fail because of above patch\n            widget.update_output = Mock()\n            self.send_signal(widget.Inputs.data, self.iris)\n        self.assertIs(widget.data, self.iris)\n        self.assertTrue(sp.isspmatrix_csr(widget.cont_x))\n        self.assertEqual(widget.cont_x.shape, (150, 4))\n\n    def test_auto_compute_dimensions(self):\n        widget = self.widget\n        self.send_signal(widget.Inputs.data, self.iris)\n        spin_x = widget.opt_controls.spin_x\n        spin_y = widget.opt_controls.spin_y\n\n        spin_x.setValue(100)\n        spin_y.setValue(100)\n        widget.auto_dimension = True\n        widget.controls.auto_dimension.toggled.emit(True)\n        self.assertLess(spin_x.value(), 100)\n        self.assertLess(spin_y.value(), 100)\n\n        spin_x.setValue(9)\n        spin_y.setValue(7)\n        widget.auto_dimension = False\n        widget.controls.auto_dimension.toggled.emit(False)\n        self.assertEqual(spin_x.value(), 10)\n        self.assertEqual(spin_y.value(), 5)\n\n    @_patch_recompute_som\n    def test_redraw_grid(self):\n        widget = self.widget\n\n        widget.auto_dimension = False\n\n        widget.opt_controls.spin_x.setValue(8)\n        widget.opt_controls.spin_y.setValue(5)\n        widget.opt_controls.shape.setCurrentIndex(1)\n        self.send_signal(widget.Inputs.data, self.iris)\n        self.assertEqual(widget.grid_cells.shape, (5, 8))\n        self.assertEqual(\n            sum(c is not None for c in widget.grid_cells.ravel()), 40)\n\n        widget.opt_controls.shape.setCurrentIndex(0)\n        widget.opt_controls.start.clicked.emit()\n        self.assertEqual(widget.grid_cells.shape, (5, 8))\n        self.assertEqual(\n            sum(c is not None for c in widget.grid_cells.ravel()), 38)\n        self.assertIsNone(widget.grid_cells[1, 7])\n        self.assertIsNone(widget.grid_cells[3, 7])\n\n        widget.opt_controls.shape.setCurrentIndex(1)\n        widget.opt_controls.start.clicked.emit()\n        self.assertEqual(widget.grid_cells.shape, (5, 8))\n        self.assertEqual(\n            sum(c is not None for c in widget.grid_cells.ravel()), 40)\n\n    def test_attr_color_change(self):\n        widget = self.widget\n        widget._redraw = Mock()\n\n        heart = Table(\"heart_disease\")\n        self.send_signal(widget.Inputs.data, heart)\n\n        widget._redraw.reset_mock()\n        combo = widget.controls.attr_color\n        combo.setCurrentIndex(0)\n        combo.activated[int].emit(0)\n        self.assertFalse(widget.controls.pie_charts.isEnabled())\n        self.assertIsNone(widget.colors)\n        self.assertIsNone(widget.thresholds)\n        widget._redraw.assert_called()\n\n        widget._redraw.reset_mock()\n        combo = widget.controls.attr_color\n        gender = heart.domain[\"gender\"]\n        ind_gen = combo.model().indexOf(gender)\n        combo.setCurrentIndex(ind_gen)\n        combo.activated[int].emit(ind_gen)\n        self.assertTrue(widget.controls.pie_charts.isEnabled())\n        np.testing.assert_equal(widget.colors.palette, gender.colors)\n        self.assertIsNone(widget.thresholds)\n        widget._redraw.assert_called()\n\n        widget._redraw.reset_mock()\n        combo = widget.controls.attr_color\n        age = heart.domain[\"age\"]\n        ind_age = combo.model().indexOf(age)\n        combo.setCurrentIndex(ind_age)\n        combo.activated[int].emit(ind_age)\n        self.assertTrue(widget.controls.pie_charts.isEnabled())\n        self.assertIsNotNone(widget.thresholds)\n        widget._redraw.assert_called()\n\n    @_patch_recompute_som\n    def test_cell_sizes(self):\n        widget = self.widget\n        widget._draw_same_color = widget._draw_pie_charts = \\\n            widget._draw_colored_circles = draw = Mock()\n\n        widget.size_by_instances = False\n        self.send_signal(widget.Inputs.data, self.iris)\n\n        widget.size_by_instances = True\n        widget.pie_charts = False\n        widget.controls.size_by_instances.toggled.emit(True)\n        sizes = draw.call_args[0][0]  # pylint: disable=unsubscriptable-object\n        self.assertAlmostEqual(sizes[0, 0], 0.8 * (30 / 100))\n        self.assertAlmostEqual(sizes[0, 1], 0.8 * (20 / 100))\n        self.assertAlmostEqual(sizes[1, 2], 0.8)\n        sizes[0, 0] = sizes[0, 1] = sizes[1, 2] = 0\n        self.assertTrue(np.all(sizes == 0))\n\n        widget.size_by_instances = False\n        widget.controls.size_by_instances.toggled.emit(False)\n        sizes = draw.call_args[0][0]  # pylint: disable=unsubscriptable-object\n        self.assertAlmostEqual(sizes[0, 0], 0.8)\n        self.assertAlmostEqual(sizes[0, 1], 0.8)\n        self.assertAlmostEqual(sizes[0, 1], 0.8)\n        sizes[0, 0] = sizes[0, 1] = sizes[1, 2] = 0\n        self.assertTrue(np.all(sizes == 0))\n\n    @_patch_recompute_som\n    def test_same_color_same_size(self):\n        widget = self.widget\n\n        widget.size_by_instances = False\n\n        self.send_signal(widget.Inputs.data, self.iris)\n        widget.attr_color = None\n        widget.controls.attr_color.activated[int].emit(0)\n        a = widget.elements.childItems()[0]\n        for e, t in zip(widget.elements.childItems(),\n                        [(0, 0, 0.4),\n                         (0.5, np.sqrt(3) / 2, 0.4),\n                         (1, np.sqrt(3), 0.4)]):\n            np.testing.assert_almost_equal((e.x(), e.y(), e.r), t)\n            self.assertEqual(e.color.getRgb(), a.color.getRgb())\n\n        widget.opt_controls.shape.setCurrentIndex(1)\n        widget.opt_controls.start.clicked.emit()\n        a = widget.elements.childItems()[0]\n        for e, t in zip(widget.elements.childItems(),\n                        [(0, 0, 0.4), (0, 1, 0.4), (1, 2, 0.4)]):\n            np.testing.assert_almost_equal((e.x(), e.y(), e.r), t)\n            self.assertEqual(e.color.getRgb(), a.color.getRgb())\n\n    @_patch_recompute_som\n    def test_diff_color_same_size(self):\n        widget = self.widget\n\n        widget.size_by_instances = False\n\n        widget.opt_controls.shape.setCurrentIndex(0)\n        self.send_signal(widget.Inputs.data, self.iris)\n        a, b, c = widget.elements.childItems()\n        for e, t in zip(widget.elements.childItems(),\n                        [(0, 0, 0.4),\n                         (0.5, np.sqrt(3) / 2, 0.4),\n                         (1, np.sqrt(3), 0.4)]):\n            np.testing.assert_almost_equal((e.x(), e.y(), e.r), t)\n        self.assertEqual(a.color.getRgb(), b.color.getRgb())\n        self.assertNotEqual(a.color.getRgb(), c.color.getRgb())\n\n        widget.opt_controls.shape.setCurrentIndex(1)\n        widget.opt_controls.start.clicked.emit()\n        a, b, c = widget.elements.childItems()\n        for e, t in zip(widget.elements.childItems(),\n                        [(0, 0, 0.4), (0, 1, 0.4), (1, 2, 0.4)]):\n            np.testing.assert_almost_equal((e.x(), e.y(), e.r), t)\n        self.assertEqual(a.color.getRgb(), b.color.getRgb())\n        self.assertNotEqual(a.color.getRgb(), c.color.getRgb())\n\n    @_patch_recompute_som\n    def test_same_color_diff_size(self):\n        widget = self.widget\n\n        widget.size_by_instances = True\n        widget.opt_controls.shape.setCurrentIndex(0)\n\n        self.send_signal(widget.Inputs.data, self.iris)\n        widget.attr_color = None\n        widget.controls.attr_color.activated[int].emit(0)\n        a = widget.elements.childItems()[0]\n        for e, t in zip(widget.elements.childItems(),\n                        [(0, 0, 0.4 * (30 / 100)),\n                         (0.5, np.sqrt(3) / 2, 0.4 * (20 / 100)),\n                         (1, np.sqrt(3), 0.4)]):\n            np.testing.assert_almost_equal((e.x(), e.y(), e.r), t)\n            self.assertEqual(e.color.getRgb(), a.color.getRgb())\n\n        widget.opt_controls.shape.setCurrentIndex(1)\n        widget.opt_controls.start.clicked.emit()\n        a = widget.elements.childItems()[0]\n        for e, t in zip(widget.elements.childItems(),\n                        [(0, 0, 0.4 * 30 / 100),\n                         (0, 1, 0.4 * 20 / 100),\n                         (1, 2, 0.4)]):\n            np.testing.assert_almost_equal((e.x(), e.y(), e.r), t)\n            self.assertEqual(e.color.getRgb(), a.color.getRgb())\n\n    @_patch_recompute_som\n    def test_diff_color_diff_size(self):\n        widget = self.widget\n\n        widget.size_by_instances = True\n\n        widget.opt_controls.shape.setCurrentIndex(0)\n        self.send_signal(widget.Inputs.data, self.iris)\n        a, b, c = widget.elements.childItems()\n        for e, t in zip(widget.elements.childItems(),\n                        [(0, 0, 0.4 * 30 / 100),\n                         (0.5, np.sqrt(3) / 2, 0.4 * 20 / 100),\n                         (1, np.sqrt(3), 0.4)]):\n            np.testing.assert_almost_equal((e.x(), e.y(), e.r), t)\n        self.assertEqual(a.color.getRgb(), b.color.getRgb())\n        self.assertNotEqual(a.color.getRgb(), c.color.getRgb())\n\n        widget.opt_controls.shape.setCurrentIndex(1)\n        widget.opt_controls.start.clicked.emit()\n        a, b, c = widget.elements.childItems()\n        for e, t in zip(widget.elements.childItems(),\n                        [(0, 0, 0.4 * 30 / 100),\n                         (0, 1, 0.4 * 20 / 100),\n                         (1, 2, 0.4)]):\n            np.testing.assert_almost_equal((e.x(), e.y(), e.r), t)\n        self.assertEqual(a.color.getRgb(), b.color.getRgb())\n        self.assertNotEqual(a.color.getRgb(), c.color.getRgb())\n\n    @_patch_recompute_som\n    def test_pie_charts(self):\n        widget = self.widget\n\n        widget.size_by_instances = True\n\n        widget.opt_controls.shape.setCurrentIndex(0)\n        self.send_signal(widget.Inputs.data, self.iris)\n\n        widget.pie_charts = True\n        widget.controls.pie_charts.toggled.emit(True)\n\n        a, b, c = widget.elements.childItems()\n        for e, (x, y, r) in zip(widget.elements.childItems(),\n                                [(0, 0, 0.8 * 30 / 100),\n                                 (0.5, np.sqrt(3) / 2, 0.8 * 20 / 100),\n                                 (1, np.sqrt(3), 0.8)]):\n            self.assertEqual(e.x(), x)\n            self.assertEqual(e.y(), y)\n            self.assertEqual(e.r, r / 2)\n            self.assertEqual(len(e.colors), len(widget.colors) + 1)\n        np.testing.assert_equal(a.dist, [1, 0, 0, 0])\n        np.testing.assert_equal(b.dist, [1, 0, 0, 0])\n        np.testing.assert_equal(c.dist, [0, 0.5, 0.5, 0])\n\n        widget.opt_controls.shape.setCurrentIndex(1)\n        widget.opt_controls.start.clicked.emit()\n        for e, (x, y, r) in zip(widget.elements.childItems(),\n                                [(0, 0, 0.8 * 30 / 100),\n                                 (0, 1, 0.8 * 20 / 100),\n                                 (1, 2, 0.8)]):\n            self.assertEqual(e.x(), x)\n            self.assertEqual(e.y(), y)\n            self.assertEqual(e.r, r / 2)\n\n        with self.iris.unlocked():\n            self.iris.Y[:15] = np.nan\n        self.send_signal(widget.Inputs.data, self.iris)\n        a = widget.elements.childItems()[0]\n        np.testing.assert_equal(a.dist, [0.5, 0, 0, 0.5])\n\n    def test_get_color_column(self):\n        widget = self.widget\n\n        table = Table(\"heart_disease\")\n        domain = table.domain\n        new_domain = Domain(\n            domain.attributes[3:], domain.class_var, domain.attributes[:3])\n        new_table = table.transform(new_domain).copy()\n        with new_table.unlocked(new_table.metas):\n            new_table.metas = new_table.metas.astype(object)\n        self.send_signal(widget.Inputs.data, new_table)\n\n        # discrete attribute\n        widget.attr_color = domain[\"rest ECG\"]\n        np.testing.assert_equal(\n            widget._get_color_column(),\n            widget.data.get_column_view(\"rest ECG\")[0].astype(int))\n\n        # discrete meta\n        widget.attr_color = domain[\"gender\"]\n        np.testing.assert_equal(\n            widget._get_color_column(),\n            widget.data.get_column_view(\"gender\")[0].astype(int))\n\n\n        # numeric attribute\n        widget.thresholds = np.array([120, 150])\n        widget.attr_color = domain[\"max HR\"]\n        for c, d in zip(widget._get_color_column(),\n                        widget.data.get_column_view(\"max HR\")[0]):\n            if d < 120:\n                self.assertEqual(c, 0)\n            if 120 <= d < 150:\n                self.assertEqual(c, 1)\n            if d >= 150:\n                self.assertEqual(c, 2)\n\n        # numeric meta\n        widget.thresholds = np.array([50, 60])\n        widget.attr_color = domain[\"age\"]\n        for c, d in zip(widget._get_color_column(),\n                        widget.data.get_column_view(\"age\")[0]):\n            if d < 50:\n                self.assertEqual(c, 0)\n            if 50 <= d < 60:\n                self.assertEqual(c, 1)\n            if d >= 60:\n                self.assertEqual(c, 2)\n\n        # discrete meta with missing values\n        widget.attr_color = domain[\"gender\"]\n        with widget.data.unlocked():\n            col = widget.data.get_column_view(\"gender\")[0]\n            col[:5] = np.nan\n        col = col.copy()\n        col[:5] = 2\n        np.testing.assert_equal(widget._get_color_column(), col)\n\n    @_patch_recompute_som\n    def test_colored_circles_with_missing_values(self):\n        with self.iris.unlocked():\n            self.iris.get_column_view(\"iris\")[0][:5] = np.nan\n        self.send_signal(self.widget.Inputs.data, self.iris)\n        self.assertTrue(self.widget.Warning.missing_colors.is_shown())\n\n        self.send_signal(self.widget.Inputs.data, None)\n        self.assertFalse(self.widget.Warning.missing_colors.is_shown())\n\n    def test_send_report(self):\n        self.widget.send_report()\n\n        self.send_signal(self.widget.Inputs.data, self.iris)\n        self.widget.send_report()\n\n    @_patch_recompute_som\n    def test_on_selection_change(self):\n        def selm(*cells):\n            m = np.zeros((widget.size_x, widget.size_y), dtype=bool)\n            for x, y in cells:\n                m[x, y] = True\n            return m\n\n        widget = self.widget\n        self.send_signal(self.widget.Inputs.data, self.iris)\n\n        widget.redraw_selection = Mock()\n        widget.update_output = Mock()\n\n        m = selm((0, 0)).astype(int)\n        widget.on_selection_change(selm((0, 0)))\n        np.testing.assert_equal(widget.selection, m)\n        widget.redraw_selection.assert_called_once()\n        widget.update_output.assert_called_once()\n\n        m = selm((0, 1)).astype(int)\n        widget.on_selection_change(selm((0, 1)))\n        np.testing.assert_equal(widget.selection, m)\n\n        m[0, 0] = 1\n        widget.on_selection_change(selm((0, 0)), SomView.SelectionAddToGroup)\n        np.testing.assert_equal(widget.selection, m)\n\n        m[0, 0] = 0\n        widget.on_selection_change(selm((0, 0)), SomView.SelectionRemove)\n        np.testing.assert_equal(widget.selection, m)\n\n        m[0, 0] = 2\n        widget.on_selection_change(selm((0, 0)), SomView.SelectionNewGroup)\n        np.testing.assert_equal(widget.selection, m)\n\n    @_patch_recompute_som\n    def test_on_selection_change_on_empty(self):\n        \"\"\"Test clicks on empty scene, when no data\"\"\"\n        widget = self.widget\n        widget.on_selection_change([])\n\n    @_patch_recompute_som\n    def test_output(self):\n        widget = self.widget\n        self.send_signal(self.widget.Inputs.data, self.iris)\n\n        self.assertIsNone(self.get_output(widget.Outputs.selected_data))\n        out = self.get_output(widget.Outputs.annotated_data)\n        self.assertEqual(len(out), 150)\n        self.assertTrue(\n            np.all(out.get_column_view(ANNOTATED_DATA_FEATURE_NAME)[0] == 0))\n\n        m = np.zeros((widget.size_x, widget.size_y), dtype=bool)\n        m[0, 0] = True\n        widget.on_selection_change(m)\n        out = self.get_output(widget.Outputs.selected_data)\n        np.testing.assert_equal(out.ids, self.iris.ids[:30])\n\n        out = self.get_output(widget.Outputs.annotated_data)\n        np.testing.assert_equal(\n            out.get_column_view(ANNOTATED_DATA_FEATURE_NAME)[0],\n            [1] * 30 + [0] * 120)\n\n        m[0, 0] = False\n        m[0, 1] = True\n        widget.on_selection_change(m, SomView.SelectionNewGroup)\n        out = self.get_output(widget.Outputs.selected_data)\n        np.testing.assert_equal(out.ids, self.iris.ids[:50])\n\n        out = self.get_output(widget.Outputs.annotated_data)\n        np.testing.assert_equal(\n            out.get_column_view(ANNOTATED_DATA_FEATURE_NAME)[0],\n            [0] * 30 + [1] * 20 + [2] * 100)\n\n        self.send_signal(self.widget.Inputs.data, None)\n        self.assertIsNone(self.get_output(widget.Outputs.selected_data))\n        self.assertIsNone(self.get_output(widget.Outputs.annotated_data))\n\n    def test_invalidated(self):\n        heart = Table(\"heart_disease\")\n        self.widget._recompute_som = Mock()\n\n        # New data - replot\n        self.send_signal(self.widget.Inputs.data, heart)\n        self.widget._recompute_som.assert_called_once()\n\n        # Same data - no replot\n        self.widget._recompute_som.reset_mock()\n        self.send_signal(self.widget.Inputs.data, heart)\n        self.widget._recompute_som.assert_not_called()\n\n        # Same data.X - no replot\n        domain = heart.domain\n        domain = Domain(domain.attributes, metas=domain.class_vars)\n        heart_with_metas = self.iris.transform(domain)\n        self.widget._recompute_som.reset_mock()\n        self.send_signal(self.widget.Inputs.data, heart_with_metas)\n        self.widget._recompute_som.assert_not_called()\n\n        # Different data, same set of cont. vars - no replot\n        attrs = [a for a in heart.domain.attributes if a.is_continuous]\n        domain = Domain(attrs)\n        heart_with_cont_features = self.iris.transform(domain)\n        self.widget._recompute_som.reset_mock()\n        self.send_signal(self.widget.Inputs.data, heart_with_cont_features)\n        self.widget._recompute_som.assert_not_called()\n\n        # Different data.X - replot\n        domain = Domain(heart.domain.attributes[:5])\n        heart_with_less_features = heart.transform(domain)\n        self.widget._recompute_som.reset_mock()\n        self.send_signal(self.widget.Inputs.data, heart_with_less_features)\n        self.widget._recompute_som.assert_called_once()",
                                "ast_type_str": "ClassDef",
                                "start_lineno": 38,
                                "end_lineno": 586,
                                "parent": null
                            }
                        }
                    ],
                    "deleted_entities": []
                }
            ]
        }
    ],
    "old_commit_hash": "62549d259297989c27c095ad8f98d686b521075b^",
    "new_commit_hash": "62549d259297989c27c095ad8f98d686b521075b",
    "commit_message": "Merge pull request #5857 from VesnaT/som_replot\n\nSOM: Recompute only when effective data changes",
    "commit_date": "2022-03-11T13:13:21+01:00",
    "metadata": {}
}
{
    "repo_name": "orange3",
    "new_commit_hash": "62549d259297989c27c095ad8f98d686b521075b",
    "test_file_codes": [
        "# pylint: disable=protected-access\n\nimport unittest\nfrom unittest.mock import Mock, patch\n\nimport numpy as np\nimport scipy.sparse as sp\n\nfrom Orange.data import Table, Domain\nfrom Orange.widgets.tests.base import WidgetTest\nfrom Orange.widgets.utils.annotated_data import ANNOTATED_DATA_FEATURE_NAME\nfrom Orange.widgets.unsupervised.owsom import OWSOM, SomView, SOM\n\n\ndef _patch_recompute_som(meth):\n    def winners_from_weights(cont_x, *_1, **_2):\n        n = cont_x.shape[0]\n        w = np.zeros((n, 2), dtype=int)\n        w[n // 5:] = [0, 1]\n        w[n // 3:] = [1, 2]\n        return w\n\n    def recompute(self):\n        if not self.data:\n            return\n        self._assign_instances(None, None)\n        self._redraw()\n        self.update_output()\n\n    def patched(*args, **kwargs):\n        with patch.object(SOM, \"winner_from_weights\", new=winners_from_weights), \\\n                patch.object(OWSOM, \"_recompute_som\", new=recompute):\n            meth(*args, **kwargs)\n\n    return patched\n\n\nclass TestOWSOM(WidgetTest):\n    def setUp(self):\n        self.widget = self.create_widget(OWSOM)  #: OWSOM\n        self.iris = Table(\"iris\")\n\n    def tearDown(self):\n        self.widget.onDeleteWidget()\n        super().tearDown()\n\n    def test_requires_continuous(self):\n        widget = self.widget\n        heart = Table(\"heart_disease\")\n\n        self.send_signal(widget.Inputs.data, Table(\"zoo\"))\n        self.assertTrue(widget.Error.no_numeric_variables.is_shown())\n        self.assertFalse(widget.Warning.ignoring_disc_variables.is_shown())\n        self.assertIsNone(widget.data)\n        self.assertIsNone(widget.cont_x)\n\n        self.send_signal(widget.Inputs.data, heart)\n        self.assertFalse(widget.Error.no_numeric_variables.is_shown())\n        self.assertTrue(widget.Warning.ignoring_disc_variables.is_shown())\n        self.assertEqual(\n            widget.cont_x.shape[1],\n            sum(attr.is_continuous for attr in heart.domain.attributes))\n\n        self.send_signal(widget.Inputs.data, self.iris)\n        self.assertFalse(widget.Error.no_numeric_variables.is_shown())\n        self.assertFalse(widget.Warning.ignoring_disc_variables.is_shown())\n        self.assertEqual(widget.cont_x.shape, (150, 4))\n\n    def test_single_attribute(self):\n        widget = self.widget\n        self.send_signal(widget.Inputs.data, self.iris)\n        self.assertFalse(widget.Warning.single_attribute.is_shown())\n        iris = self.iris[:, 0]\n        self.send_signal(widget.Inputs.data, iris)\n        self.assertTrue(widget.Warning.single_attribute.is_shown())\n\n    def test_missing_all_data(self):\n        widget = self.widget\n\n        self.send_signal(widget.Inputs.data, Table(\"heart_disease\"))\n        self.assertTrue(widget.Warning.ignoring_disc_variables.is_shown())\n\n        with self.iris.unlocked():\n            for i in range(150):\n                self.iris.X[i, i % 4] = np.nan\n\n        self.send_signal(widget.Inputs.data, self.iris)\n        self.assertTrue(widget.Error.no_defined_rows.is_shown())\n        self.assertFalse(widget.Warning.ignoring_disc_variables.is_shown())\n        self.assertIsNone(widget.data)\n        self.assertIsNone(widget.cont_x)\n\n        self.send_signal(widget.Inputs.data, None)\n        self.assertFalse(widget.Warning.ignoring_disc_variables.is_shown())\n\n    def test_missing_some_data(self):\n        widget = self.widget\n        with self.iris.unlocked():\n            self.iris.X[:50, 0] = np.nan\n\n        self.send_signal(widget.Inputs.data, self.iris)\n        self.assertFalse(widget.Error.no_defined_rows.is_shown())\n        self.assertTrue(widget.Warning.missing_values.is_shown())\n        np.testing.assert_almost_equal(\n            widget.data.Y.flatten(), [1] * 50 + [2] * 50)\n        self.assertEqual(widget.cont_x.shape, (100, 4))\n\n        self.send_signal(widget.Inputs.data, None)\n        self.assertFalse(widget.Warning.missing_values.is_shown())\n\n    def test_missing_one_row_data(self):\n        widget = self.widget\n        with self.iris.unlocked():\n            self.iris.X[5, 0] = np.nan\n\n        self.send_signal(widget.Inputs.data, self.iris)\n        self.assertFalse(widget.Error.no_defined_rows.is_shown())\n        self.assertTrue(widget.Warning.missing_values.is_shown())\n\n        self.send_signal(widget.Inputs.data, None)\n        self.assertFalse(widget.Warning.missing_values.is_shown())\n\n    @_patch_recompute_som\n    def test_sparse_data(self):\n        widget = self.widget\n        with self.iris.unlocked():\n            self.iris.X = sp.csc_matrix(self.iris.X)\n\n        # Table.from_table can decide to return dense data\n        with patch.object(Table, \"from_table\", lambda _, x: x):\n            # update_output would fail because of above patch\n            widget.update_output = Mock()\n            self.send_signal(widget.Inputs.data, self.iris)\n        self.assertIs(widget.data, self.iris)\n        self.assertTrue(sp.isspmatrix_csr(widget.cont_x))\n        self.assertEqual(widget.cont_x.shape, (150, 4))\n\n    def test_auto_compute_dimensions(self):\n        widget = self.widget\n        self.send_signal(widget.Inputs.data, self.iris)\n        spin_x = widget.opt_controls.spin_x\n        spin_y = widget.opt_controls.spin_y\n\n        spin_x.setValue(100)\n        spin_y.setValue(100)\n        widget.auto_dimension = True\n        widget.controls.auto_dimension.toggled.emit(True)\n        self.assertLess(spin_x.value(), 100)\n        self.assertLess(spin_y.value(), 100)\n\n        spin_x.setValue(9)\n        spin_y.setValue(7)\n        widget.auto_dimension = False\n        widget.controls.auto_dimension.toggled.emit(False)\n        self.assertEqual(spin_x.value(), 10)\n        self.assertEqual(spin_y.value(), 5)\n\n    @_patch_recompute_som\n    def test_redraw_grid(self):\n        widget = self.widget\n\n        widget.auto_dimension = False\n\n        widget.opt_controls.spin_x.setValue(8)\n        widget.opt_controls.spin_y.setValue(5)\n        widget.opt_controls.shape.setCurrentIndex(1)\n        self.send_signal(widget.Inputs.data, self.iris)\n        self.assertEqual(widget.grid_cells.shape, (5, 8))\n        self.assertEqual(\n            sum(c is not None for c in widget.grid_cells.ravel()), 40)\n\n        widget.opt_controls.shape.setCurrentIndex(0)\n        widget.opt_controls.start.clicked.emit()\n        self.assertEqual(widget.grid_cells.shape, (5, 8))\n        self.assertEqual(\n            sum(c is not None for c in widget.grid_cells.ravel()), 38)\n        self.assertIsNone(widget.grid_cells[1, 7])\n        self.assertIsNone(widget.grid_cells[3, 7])\n\n        widget.opt_controls.shape.setCurrentIndex(1)\n        widget.opt_controls.start.clicked.emit()\n        self.assertEqual(widget.grid_cells.shape, (5, 8))\n        self.assertEqual(\n            sum(c is not None for c in widget.grid_cells.ravel()), 40)\n\n    def test_attr_color_change(self):\n        widget = self.widget\n        widget._redraw = Mock()\n\n        heart = Table(\"heart_disease\")\n        self.send_signal(widget.Inputs.data, heart)\n\n        widget._redraw.reset_mock()\n        combo = widget.controls.attr_color\n        combo.setCurrentIndex(0)\n        combo.activated[int].emit(0)\n        self.assertFalse(widget.controls.pie_charts.isEnabled())\n        self.assertIsNone(widget.colors)\n        self.assertIsNone(widget.thresholds)\n        widget._redraw.assert_called()\n\n        widget._redraw.reset_mock()\n        combo = widget.controls.attr_color\n        gender = heart.domain[\"gender\"]\n        ind_gen = combo.model().indexOf(gender)\n        combo.setCurrentIndex(ind_gen)\n        combo.activated[int].emit(ind_gen)\n        self.assertTrue(widget.controls.pie_charts.isEnabled())\n        np.testing.assert_equal(widget.colors.palette, gender.colors)\n        self.assertIsNone(widget.thresholds)\n        widget._redraw.assert_called()\n\n        widget._redraw.reset_mock()\n        combo = widget.controls.attr_color\n        age = heart.domain[\"age\"]\n        ind_age = combo.model().indexOf(age)\n        combo.setCurrentIndex(ind_age)\n        combo.activated[int].emit(ind_age)\n        self.assertTrue(widget.controls.pie_charts.isEnabled())\n        self.assertIsNotNone(widget.thresholds)\n        widget._redraw.assert_called()\n\n    @_patch_recompute_som\n    def test_cell_sizes(self):\n        widget = self.widget\n        widget._draw_same_color = widget._draw_pie_charts = \\\n            widget._draw_colored_circles = draw = Mock()\n\n        widget.size_by_instances = False\n        self.send_signal(widget.Inputs.data, self.iris)\n\n        widget.size_by_instances = True\n        widget.pie_charts = False\n        widget.controls.size_by_instances.toggled.emit(True)\n        sizes = draw.call_args[0][0]  # pylint: disable=unsubscriptable-object\n        self.assertAlmostEqual(sizes[0, 0], 0.8 * (30 / 100))\n        self.assertAlmostEqual(sizes[0, 1], 0.8 * (20 / 100))\n        self.assertAlmostEqual(sizes[1, 2], 0.8)\n        sizes[0, 0] = sizes[0, 1] = sizes[1, 2] = 0\n        self.assertTrue(np.all(sizes == 0))\n\n        widget.size_by_instances = False\n        widget.controls.size_by_instances.toggled.emit(False)\n        sizes = draw.call_args[0][0]  # pylint: disable=unsubscriptable-object\n        self.assertAlmostEqual(sizes[0, 0], 0.8)\n        self.assertAlmostEqual(sizes[0, 1], 0.8)\n        self.assertAlmostEqual(sizes[0, 1], 0.8)\n        sizes[0, 0] = sizes[0, 1] = sizes[1, 2] = 0\n        self.assertTrue(np.all(sizes == 0))\n\n    @_patch_recompute_som\n    def test_same_color_same_size(self):\n        widget = self.widget\n\n        widget.size_by_instances = False\n\n        self.send_signal(widget.Inputs.data, self.iris)\n        widget.attr_color = None\n        widget.controls.attr_color.activated[int].emit(0)\n        a = widget.elements.childItems()[0]\n        for e, t in zip(widget.elements.childItems(),\n                        [(0, 0, 0.4),\n                         (0.5, np.sqrt(3) / 2, 0.4),\n                         (1, np.sqrt(3), 0.4)]):\n            np.testing.assert_almost_equal((e.x(), e.y(), e.r), t)\n            self.assertEqual(e.color.getRgb(), a.color.getRgb())\n\n        widget.opt_controls.shape.setCurrentIndex(1)\n        widget.opt_controls.start.clicked.emit()\n        a = widget.elements.childItems()[0]\n        for e, t in zip(widget.elements.childItems(),\n                        [(0, 0, 0.4), (0, 1, 0.4), (1, 2, 0.4)]):\n            np.testing.assert_almost_equal((e.x(), e.y(), e.r), t)\n            self.assertEqual(e.color.getRgb(), a.color.getRgb())\n\n    @_patch_recompute_som\n    def test_diff_color_same_size(self):\n        widget = self.widget\n\n        widget.size_by_instances = False\n\n        widget.opt_controls.shape.setCurrentIndex(0)\n        self.send_signal(widget.Inputs.data, self.iris)\n        a, b, c = widget.elements.childItems()\n        for e, t in zip(widget.elements.childItems(),\n                        [(0, 0, 0.4),\n                         (0.5, np.sqrt(3) / 2, 0.4),\n                         (1, np.sqrt(3), 0.4)]):\n            np.testing.assert_almost_equal((e.x(), e.y(), e.r), t)\n        self.assertEqual(a.color.getRgb(), b.color.getRgb())\n        self.assertNotEqual(a.color.getRgb(), c.color.getRgb())\n\n        widget.opt_controls.shape.setCurrentIndex(1)\n        widget.opt_controls.start.clicked.emit()\n        a, b, c = widget.elements.childItems()\n        for e, t in zip(widget.elements.childItems(),\n                        [(0, 0, 0.4), (0, 1, 0.4), (1, 2, 0.4)]):\n            np.testing.assert_almost_equal((e.x(), e.y(), e.r), t)\n        self.assertEqual(a.color.getRgb(), b.color.getRgb())\n        self.assertNotEqual(a.color.getRgb(), c.color.getRgb())\n\n    @_patch_recompute_som\n    def test_same_color_diff_size(self):\n        widget = self.widget\n\n        widget.size_by_instances = True\n        widget.opt_controls.shape.setCurrentIndex(0)\n\n        self.send_signal(widget.Inputs.data, self.iris)\n        widget.attr_color = None\n        widget.controls.attr_color.activated[int].emit(0)\n        a = widget.elements.childItems()[0]\n        for e, t in zip(widget.elements.childItems(),\n                        [(0, 0, 0.4 * (30 / 100)),\n                         (0.5, np.sqrt(3) / 2, 0.4 * (20 / 100)),\n                         (1, np.sqrt(3), 0.4)]):\n            np.testing.assert_almost_equal((e.x(), e.y(), e.r), t)\n            self.assertEqual(e.color.getRgb(), a.color.getRgb())\n\n        widget.opt_controls.shape.setCurrentIndex(1)\n        widget.opt_controls.start.clicked.emit()\n        a = widget.elements.childItems()[0]\n        for e, t in zip(widget.elements.childItems(),\n                        [(0, 0, 0.4 * 30 / 100),\n                         (0, 1, 0.4 * 20 / 100),\n                         (1, 2, 0.4)]):\n            np.testing.assert_almost_equal((e.x(), e.y(), e.r), t)\n            self.assertEqual(e.color.getRgb(), a.color.getRgb())\n\n    @_patch_recompute_som\n    def test_diff_color_diff_size(self):\n        widget = self.widget\n\n        widget.size_by_instances = True\n\n        widget.opt_controls.shape.setCurrentIndex(0)\n        self.send_signal(widget.Inputs.data, self.iris)\n        a, b, c = widget.elements.childItems()\n        for e, t in zip(widget.elements.childItems(),\n                        [(0, 0, 0.4 * 30 / 100),\n                         (0.5, np.sqrt(3) / 2, 0.4 * 20 / 100),\n                         (1, np.sqrt(3), 0.4)]):\n            np.testing.assert_almost_equal((e.x(), e.y(), e.r), t)\n        self.assertEqual(a.color.getRgb(), b.color.getRgb())\n        self.assertNotEqual(a.color.getRgb(), c.color.getRgb())\n\n        widget.opt_controls.shape.setCurrentIndex(1)\n        widget.opt_controls.start.clicked.emit()\n        a, b, c = widget.elements.childItems()\n        for e, t in zip(widget.elements.childItems(),\n                        [(0, 0, 0.4 * 30 / 100),\n                         (0, 1, 0.4 * 20 / 100),\n                         (1, 2, 0.4)]):\n            np.testing.assert_almost_equal((e.x(), e.y(), e.r), t)\n        self.assertEqual(a.color.getRgb(), b.color.getRgb())\n        self.assertNotEqual(a.color.getRgb(), c.color.getRgb())\n\n    @_patch_recompute_som\n    def test_pie_charts(self):\n        widget = self.widget\n\n        widget.size_by_instances = True\n\n        widget.opt_controls.shape.setCurrentIndex(0)\n        self.send_signal(widget.Inputs.data, self.iris)\n\n        widget.pie_charts = True\n        widget.controls.pie_charts.toggled.emit(True)\n\n        a, b, c = widget.elements.childItems()\n        for e, (x, y, r) in zip(widget.elements.childItems(),\n                                [(0, 0, 0.8 * 30 / 100),\n                                 (0.5, np.sqrt(3) / 2, 0.8 * 20 / 100),\n                                 (1, np.sqrt(3), 0.8)]):\n            self.assertEqual(e.x(), x)\n            self.assertEqual(e.y(), y)\n            self.assertEqual(e.r, r / 2)\n            self.assertEqual(len(e.colors), len(widget.colors) + 1)\n        np.testing.assert_equal(a.dist, [1, 0, 0, 0])\n        np.testing.assert_equal(b.dist, [1, 0, 0, 0])\n        np.testing.assert_equal(c.dist, [0, 0.5, 0.5, 0])\n\n        widget.opt_controls.shape.setCurrentIndex(1)\n        widget.opt_controls.start.clicked.emit()\n        for e, (x, y, r) in zip(widget.elements.childItems(),\n                                [(0, 0, 0.8 * 30 / 100),\n                                 (0, 1, 0.8 * 20 / 100),\n                                 (1, 2, 0.8)]):\n            self.assertEqual(e.x(), x)\n            self.assertEqual(e.y(), y)\n            self.assertEqual(e.r, r / 2)\n\n        with self.iris.unlocked():\n            self.iris.Y[:15] = np.nan\n        self.send_signal(widget.Inputs.data, self.iris)\n        a = widget.elements.childItems()[0]\n        np.testing.assert_equal(a.dist, [0.5, 0, 0, 0.5])\n\n    def test_get_color_column(self):\n        widget = self.widget\n\n        table = Table(\"heart_disease\")\n        domain = table.domain\n        new_domain = Domain(\n            domain.attributes[3:], domain.class_var, domain.attributes[:3])\n        new_table = table.transform(new_domain).copy()\n        with new_table.unlocked(new_table.metas):\n            new_table.metas = new_table.metas.astype(object)\n        self.send_signal(widget.Inputs.data, new_table)\n\n        # discrete attribute\n        widget.attr_color = domain[\"rest ECG\"]\n        np.testing.assert_equal(\n            widget._get_color_column(),\n            widget.data.get_column_view(\"rest ECG\")[0].astype(int))\n\n        # discrete meta\n        widget.attr_color = domain[\"gender\"]\n        np.testing.assert_equal(\n            widget._get_color_column(),\n            widget.data.get_column_view(\"gender\")[0].astype(int))\n\n\n        # numeric attribute\n        widget.thresholds = np.array([120, 150])\n        widget.attr_color = domain[\"max HR\"]\n        for c, d in zip(widget._get_color_column(),\n                        widget.data.get_column_view(\"max HR\")[0]):\n            if d < 120:\n                self.assertEqual(c, 0)\n            if 120 <= d < 150:\n                self.assertEqual(c, 1)\n            if d >= 150:\n                self.assertEqual(c, 2)\n\n        # numeric meta\n        widget.thresholds = np.array([50, 60])\n        widget.attr_color = domain[\"age\"]\n        for c, d in zip(widget._get_color_column(),\n                        widget.data.get_column_view(\"age\")[0]):\n            if d < 50:\n                self.assertEqual(c, 0)\n            if 50 <= d < 60:\n                self.assertEqual(c, 1)\n            if d >= 60:\n                self.assertEqual(c, 2)\n\n        # discrete meta with missing values\n        widget.attr_color = domain[\"gender\"]\n        with widget.data.unlocked():\n            col = widget.data.get_column_view(\"gender\")[0]\n            col[:5] = np.nan\n        col = col.copy()\n        col[:5] = 2\n        np.testing.assert_equal(widget._get_color_column(), col)\n\n    @_patch_recompute_som\n    def test_colored_circles_with_missing_values(self):\n        with self.iris.unlocked():\n            self.iris.get_column_view(\"iris\")[0][:5] = np.nan\n        self.send_signal(self.widget.Inputs.data, self.iris)\n        self.assertTrue(self.widget.Warning.missing_colors.is_shown())\n\n        self.send_signal(self.widget.Inputs.data, None)\n        self.assertFalse(self.widget.Warning.missing_colors.is_shown())\n\n    def test_send_report(self):\n        self.widget.send_report()\n\n        self.send_signal(self.widget.Inputs.data, self.iris)\n        self.widget.send_report()\n\n    @_patch_recompute_som\n    def test_on_selection_change(self):\n        def selm(*cells):\n            m = np.zeros((widget.size_x, widget.size_y), dtype=bool)\n            for x, y in cells:\n                m[x, y] = True\n            return m\n\n        widget = self.widget\n        self.send_signal(self.widget.Inputs.data, self.iris)\n\n        widget.redraw_selection = Mock()\n        widget.update_output = Mock()\n\n        m = selm((0, 0)).astype(int)\n        widget.on_selection_change(selm((0, 0)))\n        np.testing.assert_equal(widget.selection, m)\n        widget.redraw_selection.assert_called_once()\n        widget.update_output.assert_called_once()\n\n        m = selm((0, 1)).astype(int)\n        widget.on_selection_change(selm((0, 1)))\n        np.testing.assert_equal(widget.selection, m)\n\n        m[0, 0] = 1\n        widget.on_selection_change(selm((0, 0)), SomView.SelectionAddToGroup)\n        np.testing.assert_equal(widget.selection, m)\n\n        m[0, 0] = 0\n        widget.on_selection_change(selm((0, 0)), SomView.SelectionRemove)\n        np.testing.assert_equal(widget.selection, m)\n\n        m[0, 0] = 2\n        widget.on_selection_change(selm((0, 0)), SomView.SelectionNewGroup)\n        np.testing.assert_equal(widget.selection, m)\n\n    @_patch_recompute_som\n    def test_on_selection_change_on_empty(self):\n        \"\"\"Test clicks on empty scene, when no data\"\"\"\n        widget = self.widget\n        widget.on_selection_change([])\n\n    @_patch_recompute_som\n    def test_output(self):\n        widget = self.widget\n        self.send_signal(self.widget.Inputs.data, self.iris)\n\n        self.assertIsNone(self.get_output(widget.Outputs.selected_data))\n        out = self.get_output(widget.Outputs.annotated_data)\n        self.assertEqual(len(out), 150)\n        self.assertTrue(\n            np.all(out.get_column_view(ANNOTATED_DATA_FEATURE_NAME)[0] == 0))\n\n        m = np.zeros((widget.size_x, widget.size_y), dtype=bool)\n        m[0, 0] = True\n        widget.on_selection_change(m)\n        out = self.get_output(widget.Outputs.selected_data)\n        np.testing.assert_equal(out.ids, self.iris.ids[:30])\n\n        out = self.get_output(widget.Outputs.annotated_data)\n        np.testing.assert_equal(\n            out.get_column_view(ANNOTATED_DATA_FEATURE_NAME)[0],\n            [1] * 30 + [0] * 120)\n\n        m[0, 0] = False\n        m[0, 1] = True\n        widget.on_selection_change(m, SomView.SelectionNewGroup)\n        out = self.get_output(widget.Outputs.selected_data)\n        np.testing.assert_equal(out.ids, self.iris.ids[:50])\n\n        out = self.get_output(widget.Outputs.annotated_data)\n        np.testing.assert_equal(\n            out.get_column_view(ANNOTATED_DATA_FEATURE_NAME)[0],\n            [0] * 30 + [1] * 20 + [2] * 100)\n\n        self.send_signal(self.widget.Inputs.data, None)\n        self.assertIsNone(self.get_output(widget.Outputs.selected_data))\n        self.assertIsNone(self.get_output(widget.Outputs.annotated_data))\n\n    def test_invalidated(self):\n        heart = Table(\"heart_disease\")\n        self.widget._recompute_som = Mock()\n\n        # New data - replot\n        self.send_signal(self.widget.Inputs.data, heart)\n        self.widget._recompute_som.assert_called_once()\n\n        # Same data - no replot\n        self.widget._recompute_som.reset_mock()\n        self.send_signal(self.widget.Inputs.data, heart)\n        self.widget._recompute_som.assert_not_called()\n\n        # Same data.X - no replot\n        domain = heart.domain\n        domain = Domain(domain.attributes, metas=domain.class_vars)\n        heart_with_metas = self.iris.transform(domain)\n        self.widget._recompute_som.reset_mock()\n        self.send_signal(self.widget.Inputs.data, heart_with_metas)\n        self.widget._recompute_som.assert_not_called()\n\n        # Different data, same set of cont. vars - no replot\n        attrs = [a for a in heart.domain.attributes if a.is_continuous]\n        domain = Domain(attrs)\n        heart_with_cont_features = self.iris.transform(domain)\n        self.widget._recompute_som.reset_mock()\n        self.send_signal(self.widget.Inputs.data, heart_with_cont_features)\n        self.widget._recompute_som.assert_not_called()\n\n        # Different data.X - replot\n        domain = Domain(heart.domain.attributes[:5])\n        heart_with_less_features = heart.transform(domain)\n        self.widget._recompute_som.reset_mock()\n        self.send_signal(self.widget.Inputs.data, heart_with_less_features)\n        self.widget._recompute_som.assert_called_once()\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_62549d259297989c27c095ad8f98d686b521075b/.venv/lib/python3.7/site-packages/numpy/core/include -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_62549d259297989c27c095ad8f98d686b521075b/.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_62549d259297989c27c095ad8f98d686b521075b/.venv/lib/python3.7/site-packages/numpy/core/include -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_62549d259297989c27c095ad8f98d686b521075b/.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_62549d259297989c27c095ad8f98d686b521075b/.venv/lib/python3.7/site-packages/numpy/core/include -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_62549d259297989c27c095ad8f98d686b521075b/.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_62549d259297989c27c095ad8f98d686b521075b/.venv/lib/python3.7/site-packages/numpy/core/include -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_62549d259297989c27c095ad8f98d686b521075b/.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_62549d259297989c27c095ad8f98d686b521075b/.venv/lib/python3.7/site-packages/numpy/core/include -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_62549d259297989c27c095ad8f98d686b521075b/.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_62549d259297989c27c095ad8f98d686b521075b/.venv/lib/python3.7/site-packages/numpy/core/include -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_62549d259297989c27c095ad8f98d686b521075b/.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_62549d259297989c27c095ad8f98d686b521075b/.venv/lib/python3.7/site-packages/numpy/core/include -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_62549d259297989c27c095ad8f98d686b521075b/.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_62549d259297989c27c095ad8f98d686b521075b/.venv/lib/python3.7/site-packages/numpy/core/include -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_62549d259297989c27c095ad8f98d686b521075b/.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_62549d259297989c27c095ad8f98d686b521075b/.venv/lib/python3.7/site-packages/numpy/core/include -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_62549d259297989c27c095ad8f98d686b521075b/.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_62549d259297989c27c095ad8f98d686b521075b/.venv/lib/python3.7/site-packages/numpy/core/include -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_62549d259297989c27c095ad8f98d686b521075b/.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_62549d259297989c27c095ad8f98d686b521075b/.venv/lib/python3.7/site-packages/numpy/core/include -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_62549d259297989c27c095ad8f98d686b521075b/.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_62549d259297989c27c095ad8f98d686b521075b/.venv/lib/python3.7/site-packages/Orange3.egg-link (link to .)\nAdding Orange3 3.32.0.dev0+62549d2 to easy-install.pth file\nInstalling orange-canvas script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_62549d259297989c27c095ad8f98d686b521075b/.venv/bin\n\nInstalled /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_62549d259297989c27c095ad8f98d686b521075b\nProcessing dependencies for Orange3==3.32.0.dev0+62549d2\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_62549d259297989c27c095ad8f98d686b521075b/.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_62549d259297989c27c095ad8f98d686b521075b/.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_62549d259297989c27c095ad8f98d686b521075b/.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_62549d259297989c27c095ad8f98d686b521075b/.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_62549d259297989c27c095ad8f98d686b521075b/.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_62549d259297989c27c095ad8f98d686b521075b/.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_62549d259297989c27c095ad8f98d686b521075b/.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_62549d259297989c27c095ad8f98d686b521075b/.venv/bin\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_62549d259297989c27c095ad8f98d686b521075b/.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_62549d259297989c27c095ad8f98d686b521075b/.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_62549d259297989c27c095ad8f98d686b521075b/.venv/lib/python3.7/site-packages\nSearching for pyqtgraph==0.12.4\nBest match: pyqtgraph 0.12.4\nAdding pyqtgraph 0.12.4 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_62549d259297989c27c095ad8f98d686b521075b/.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_62549d259297989c27c095ad8f98d686b521075b/.venv/bin\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_62549d259297989c27c095ad8f98d686b521075b/.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_62549d259297989c27c095ad8f98d686b521075b/.venv/bin\nInstalling pip3 script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_62549d259297989c27c095ad8f98d686b521075b/.venv/bin\nInstalling pip3.10 script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_62549d259297989c27c095ad8f98d686b521075b/.venv/bin\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_62549d259297989c27c095ad8f98d686b521075b/.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_62549d259297989c27c095ad8f98d686b521075b/.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_62549d259297989c27c095ad8f98d686b521075b/.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_62549d259297989c27c095ad8f98d686b521075b/.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_62549d259297989c27c095ad8f98d686b521075b/.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_62549d259297989c27c095ad8f98d686b521075b/.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_62549d259297989c27c095ad8f98d686b521075b/.venv/bin\nInstalling f2py3 script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_62549d259297989c27c095ad8f98d686b521075b/.venv/bin\nInstalling f2py3.7 script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_62549d259297989c27c095ad8f98d686b521075b/.venv/bin\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_62549d259297989c27c095ad8f98d686b521075b/.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_62549d259297989c27c095ad8f98d686b521075b/.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_62549d259297989c27c095ad8f98d686b521075b/.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_62549d259297989c27c095ad8f98d686b521075b/.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_62549d259297989c27c095ad8f98d686b521075b/.venv/bin\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_62549d259297989c27c095ad8f98d686b521075b/.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_62549d259297989c27c095ad8f98d686b521075b/.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_62549d259297989c27c095ad8f98d686b521075b/.venv/bin\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_62549d259297989c27c095ad8f98d686b521075b/.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_62549d259297989c27c095ad8f98d686b521075b/.venv/bin\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_62549d259297989c27c095ad8f98d686b521075b/.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_62549d259297989c27c095ad8f98d686b521075b/.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_62549d259297989c27c095ad8f98d686b521075b/.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_62549d259297989c27c095ad8f98d686b521075b/.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_62549d259297989c27c095ad8f98d686b521075b/.venv/bin\nInstalling pyrcc5 script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_62549d259297989c27c095ad8f98d686b521075b/.venv/bin\nInstalling pyuic5 script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_62549d259297989c27c095ad8f98d686b521075b/.venv/bin\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_62549d259297989c27c095ad8f98d686b521075b/.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_62549d259297989c27c095ad8f98d686b521075b/.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_62549d259297989c27c095ad8f98d686b521075b/.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_62549d259297989c27c095ad8f98d686b521075b/.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_62549d259297989c27c095ad8f98d686b521075b/.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_62549d259297989c27c095ad8f98d686b521075b/.venv/bin\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_62549d259297989c27c095ad8f98d686b521075b/.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_62549d259297989c27c095ad8f98d686b521075b/.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_62549d259297989c27c095ad8f98d686b521075b/.venv/bin\nInstalling jupyter-migrate script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_62549d259297989c27c095ad8f98d686b521075b/.venv/bin\nInstalling jupyter-troubleshoot script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_62549d259297989c27c095ad8f98d686b521075b/.venv/bin\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_62549d259297989c27c095ad8f98d686b521075b/.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_62549d259297989c27c095ad8f98d686b521075b/.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_62549d259297989c27c095ad8f98d686b521075b/.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_62549d259297989c27c095ad8f98d686b521075b/.venv/bin\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_62549d259297989c27c095ad8f98d686b521075b/.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_62549d259297989c27c095ad8f98d686b521075b/.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_62549d259297989c27c095ad8f98d686b521075b/.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_62549d259297989c27c095ad8f98d686b521075b/.venv/bin\nInstalling jupyter-kernelspec script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_62549d259297989c27c095ad8f98d686b521075b/.venv/bin\nInstalling jupyter-run script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_62549d259297989c27c095ad8f98d686b521075b/.venv/bin\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_62549d259297989c27c095ad8f98d686b521075b/.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_62549d259297989c27c095ad8f98d686b521075b/.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_62549d259297989c27c095ad8f98d686b521075b/.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_62549d259297989c27c095ad8f98d686b521075b/.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_62549d259297989c27c095ad8f98d686b521075b/.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_62549d259297989c27c095ad8f98d686b521075b/.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_62549d259297989c27c095ad8f98d686b521075b/.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_62549d259297989c27c095ad8f98d686b521075b/.venv/bin\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_62549d259297989c27c095ad8f98d686b521075b/.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_62549d259297989c27c095ad8f98d686b521075b/.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_62549d259297989c27c095ad8f98d686b521075b/.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_62549d259297989c27c095ad8f98d686b521075b/.venv/bin\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_62549d259297989c27c095ad8f98d686b521075b/.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_62549d259297989c27c095ad8f98d686b521075b/.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_62549d259297989c27c095ad8f98d686b521075b/.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_62549d259297989c27c095ad8f98d686b521075b/.venv/bin\nInstalling pyftmerge script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_62549d259297989c27c095ad8f98d686b521075b/.venv/bin\nInstalling pyftsubset script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_62549d259297989c27c095ad8f98d686b521075b/.venv/bin\nInstalling ttx script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_62549d259297989c27c095ad8f98d686b521075b/.venv/bin\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_62549d259297989c27c095ad8f98d686b521075b/.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_62549d259297989c27c095ad8f98d686b521075b/.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_62549d259297989c27c095ad8f98d686b521075b/.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_62549d259297989c27c095ad8f98d686b521075b/.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_62549d259297989c27c095ad8f98d686b521075b/.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_62549d259297989c27c095ad8f98d686b521075b/.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_62549d259297989c27c095ad8f98d686b521075b/.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_62549d259297989c27c095ad8f98d686b521075b/.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_62549d259297989c27c095ad8f98d686b521075b/.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_62549d259297989c27c095ad8f98d686b521075b/.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_62549d259297989c27c095ad8f98d686b521075b/.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_62549d259297989c27c095ad8f98d686b521075b/.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_62549d259297989c27c095ad8f98d686b521075b/.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_62549d259297989c27c095ad8f98d686b521075b/.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_62549d259297989c27c095ad8f98d686b521075b/.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_62549d259297989c27c095ad8f98d686b521075b/.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_62549d259297989c27c095ad8f98d686b521075b/.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_62549d259297989c27c095ad8f98d686b521075b/.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_62549d259297989c27c095ad8f98d686b521075b/.venv/bin\nInstalling iptest3 script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_62549d259297989c27c095ad8f98d686b521075b/.venv/bin\nInstalling ipython script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_62549d259297989c27c095ad8f98d686b521075b/.venv/bin\nInstalling ipython3 script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_62549d259297989c27c095ad8f98d686b521075b/.venv/bin\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_62549d259297989c27c095ad8f98d686b521075b/.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_62549d259297989c27c095ad8f98d686b521075b/.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_62549d259297989c27c095ad8f98d686b521075b/.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_62549d259297989c27c095ad8f98d686b521075b/.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_62549d259297989c27c095ad8f98d686b521075b/.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_62549d259297989c27c095ad8f98d686b521075b/.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_62549d259297989c27c095ad8f98d686b521075b/.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_62549d259297989c27c095ad8f98d686b521075b/.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_62549d259297989c27c095ad8f98d686b521075b/.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_62549d259297989c27c095ad8f98d686b521075b/.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_62549d259297989c27c095ad8f98d686b521075b/.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_62549d259297989c27c095ad8f98d686b521075b/.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_62549d259297989c27c095ad8f98d686b521075b/.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_62549d259297989c27c095ad8f98d686b521075b/.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_62549d259297989c27c095ad8f98d686b521075b/.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_62549d259297989c27c095ad8f98d686b521075b/.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_62549d259297989c27c095ad8f98d686b521075b/.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_62549d259297989c27c095ad8f98d686b521075b/.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_62549d259297989c27c095ad8f98d686b521075b/.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_62549d259297989c27c095ad8f98d686b521075b/.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_62549d259297989c27c095ad8f98d686b521075b/.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_62549d259297989c27c095ad8f98d686b521075b/.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_62549d259297989c27c095ad8f98d686b521075b/.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_62549d259297989c27c095ad8f98d686b521075b/.venv/lib/python3.7/site-packages\nFinished processing dependencies for Orange3==3.32.0.dev0+62549d2\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 107ms\nPrepared 18 packages in 31ms\nInstalled 18 packages in 79ms\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 279ms\nInstalled 47 packages in 638ms\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 385ms\nInstalled 38 packages in 495ms\n + anyqt==0.2.0\n + attrs==24.2.0\n + backcall==0.2.0\n + cattrs==23.1.2\n + commonmark==0.9.1\n + debugpy==1.7.0\n + decorator==5.1.1\n + dictdiffer==0.9.0\n + docutils==0.20.1\n + entrypoints==0.4\n + ipykernel==6.16.2\n + ipython==7.34.0\n + ipython-genutils==0.2.0\n + jedi==0.19.2\n + jupyter-client==7.4.9\n + jupyter-core==4.12.0\n + matplotlib-inline==0.1.6\n + nest-asyncio==1.6.0\n + orange-canvas-core==0.1.35\n + orange-widget-base==4.22.0\n + parso==0.8.4\n + pexpect==4.9.0\n + pickleshare==0.7.5\n + platformdirs==4.0.0\n + prompt-toolkit==3.0.48\n + psutil==6.1.1\n + ptyprocess==0.7.0\n + pygments==2.17.2\n + pyqtgraph==0.12.4\n + pyzmq==26.2.0\n + qasync==0.25.0\n + qtconsole==5.4.4\n + qtpy==2.4.2\n + requests-cache==1.1.1\n + tornado==6.2\n + traitlets==5.9.0\n + url-normalize==1.4.3\n + wcwidth==0.2.13\nResolved 2 packages in 5ms\nInstalled 2 packages in 28ms\n + psycopg2==2.9.9\n + pymssql==2.3.2\nResolved 19 packages in 36ms\nInstalled 5 packages in 1.04s\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 2ms\nIn file included from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_62549d259297989c27c095ad8f98d686b521075b/.venv/lib/python3.7/site-packages/numpy/core/include/numpy/ndarraytypes.h:1830,\n                 from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_62549d259297989c27c095ad8f98d686b521075b/.venv/lib/python3.7/site-packages/numpy/core/include/numpy/ndarrayobject.h:12,\n                 from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_62549d259297989c27c095ad8f98d686b521075b/.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_62549d259297989c27c095ad8f98d686b521075b/.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_62549d259297989c27c095ad8f98d686b521075b/.venv/lib/python3.7/site-packages/numpy/core/include/numpy/ndarraytypes.h:1830,\n                 from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_62549d259297989c27c095ad8f98d686b521075b/.venv/lib/python3.7/site-packages/numpy/core/include/numpy/ndarrayobject.h:12,\n                 from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_62549d259297989c27c095ad8f98d686b521075b/.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_62549d259297989c27c095ad8f98d686b521075b/.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_62549d259297989c27c095ad8f98d686b521075b/.venv/lib/python3.7/site-packages/numpy/core/include/numpy/ndarraytypes.h:1830,\n                 from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_62549d259297989c27c095ad8f98d686b521075b/.venv/lib/python3.7/site-packages/numpy/core/include/numpy/ndarrayobject.h:12,\n                 from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_62549d259297989c27c095ad8f98d686b521075b/.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_62549d259297989c27c095ad8f98d686b521075b/.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_62549d259297989c27c095ad8f98d686b521075b/.venv/lib/python3.7/site-packages/numpy/core/include/numpy/ndarraytypes.h:1830,\n                 from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_62549d259297989c27c095ad8f98d686b521075b/.venv/lib/python3.7/site-packages/numpy/core/include/numpy/ndarrayobject.h:12,\n                 from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_62549d259297989c27c095ad8f98d686b521075b/.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_62549d259297989c27c095ad8f98d686b521075b/.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_62549d259297989c27c095ad8f98d686b521075b/.venv/lib/python3.7/site-packages/numpy/core/include/numpy/ndarraytypes.h:1830,\n                 from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_62549d259297989c27c095ad8f98d686b521075b/.venv/lib/python3.7/site-packages/numpy/core/include/numpy/ndarrayobject.h:12,\n                 from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_62549d259297989c27c095ad8f98d686b521075b/.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_62549d259297989c27c095ad8f98d686b521075b/.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_62549d259297989c27c095ad8f98d686b521075b/.venv/lib/python3.7/site-packages/numpy/core/include/numpy/ndarraytypes.h:1830,\n                 from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_62549d259297989c27c095ad8f98d686b521075b/.venv/lib/python3.7/site-packages/numpy/core/include/numpy/ndarrayobject.h:12,\n                 from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_62549d259297989c27c095ad8f98d686b521075b/.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_62549d259297989c27c095ad8f98d686b521075b/.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_62549d259297989c27c095ad8f98d686b521075b/.venv/lib/python3.7/site-packages/numpy/core/include/numpy/ndarraytypes.h:1830,\n                 from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_62549d259297989c27c095ad8f98d686b521075b/.venv/lib/python3.7/site-packages/numpy/core/include/numpy/ndarrayobject.h:12,\n                 from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_62549d259297989c27c095ad8f98d686b521075b/.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_62549d259297989c27c095ad8f98d686b521075b/.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_62549d259297989c27c095ad8f98d686b521075b/.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_62549d259297989c27c095ad8f98d686b521075b/.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_62549d259297989c27c095ad8f98d686b521075b\nplugins: anyio-3.7.1\ncollected 28 items\n\nr2e_tests/test_1.py sss.........................                         [100%]\n\n==================================== PASSES ====================================\n=========================== short test summary info ============================\nPASSED r2e_tests/test_1.py::TestOWSOM::test_attr_color_change\nPASSED r2e_tests/test_1.py::TestOWSOM::test_auto_compute_dimensions\nPASSED r2e_tests/test_1.py::TestOWSOM::test_cell_sizes\nPASSED r2e_tests/test_1.py::TestOWSOM::test_colored_circles_with_missing_values\nPASSED r2e_tests/test_1.py::TestOWSOM::test_diff_color_diff_size\nPASSED r2e_tests/test_1.py::TestOWSOM::test_diff_color_same_size\nPASSED r2e_tests/test_1.py::TestOWSOM::test_get_color_column\nPASSED r2e_tests/test_1.py::TestOWSOM::test_image_export\nPASSED r2e_tests/test_1.py::TestOWSOM::test_invalidated\nPASSED r2e_tests/test_1.py::TestOWSOM::test_minimum_size\nPASSED r2e_tests/test_1.py::TestOWSOM::test_missing_all_data\nPASSED r2e_tests/test_1.py::TestOWSOM::test_missing_one_row_data\nPASSED r2e_tests/test_1.py::TestOWSOM::test_missing_some_data\nPASSED r2e_tests/test_1.py::TestOWSOM::test_msg_base_class\nPASSED r2e_tests/test_1.py::TestOWSOM::test_on_selection_change\nPASSED r2e_tests/test_1.py::TestOWSOM::test_on_selection_change_on_empty\nPASSED r2e_tests/test_1.py::TestOWSOM::test_output\nPASSED r2e_tests/test_1.py::TestOWSOM::test_pie_charts\nPASSED r2e_tests/test_1.py::TestOWSOM::test_redraw_grid\nPASSED r2e_tests/test_1.py::TestOWSOM::test_requires_continuous\nPASSED r2e_tests/test_1.py::TestOWSOM::test_same_color_diff_size\nPASSED r2e_tests/test_1.py::TestOWSOM::test_same_color_same_size\nPASSED r2e_tests/test_1.py::TestOWSOM::test_send_report\nPASSED r2e_tests/test_1.py::TestOWSOM::test_single_attribute\nPASSED r2e_tests/test_1.py::TestOWSOM::test_sparse_data\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======================== 25 passed, 3 skipped in 6.79s =========================\nWidgets left: 0    Max widgets: 2101 \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_62549d259297989c27c095ad8f98d686b521075b\nplugins: anyio-3.7.1\ncollected 28 items\n\nr2e_tests/test_1.py sss........F................                         [100%]\n\n=================================== FAILURES ===================================\n__________________________ TestOWSOM.test_invalidated __________________________\n\nself = <r2e_tests.test_1.TestOWSOM testMethod=test_invalidated>\n\n    def test_invalidated(self):\n        heart = Table(\"heart_disease\")\n        self.widget._recompute_som = Mock()\n    \n        # New data - replot\n        self.send_signal(self.widget.Inputs.data, heart)\n        self.widget._recompute_som.assert_called_once()\n    \n        # Same data - no replot\n        self.widget._recompute_som.reset_mock()\n        self.send_signal(self.widget.Inputs.data, heart)\n>       self.widget._recompute_som.assert_not_called()\n\nr2e_tests/test_1.py:563: \n_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ \n\n_mock_self = <Mock id='140098452744848'>\n\n    def assert_not_called(_mock_self):\n        \"\"\"assert that the mock was never called.\n        \"\"\"\n        self = _mock_self\n        if self.call_count != 0:\n            msg = (\"Expected '%s' to not have been called. Called %s times.\" %\n                   (self._mock_name or 'mock', self.call_count))\n>           raise AssertionError(msg)\nE           AssertionError: Expected 'mock' to not have been called. Called 1 times.\n\n../../../../.local/share/uv/python/cpython-3.7.9-linux-x86_64-gnu/lib/python3.7/unittest/mock.py:841: AssertionError\n==================================== PASSES ====================================\n=========================== short test summary info ============================\nPASSED r2e_tests/test_1.py::TestOWSOM::test_attr_color_change\nPASSED r2e_tests/test_1.py::TestOWSOM::test_auto_compute_dimensions\nPASSED r2e_tests/test_1.py::TestOWSOM::test_cell_sizes\nPASSED r2e_tests/test_1.py::TestOWSOM::test_colored_circles_with_missing_values\nPASSED r2e_tests/test_1.py::TestOWSOM::test_diff_color_diff_size\nPASSED r2e_tests/test_1.py::TestOWSOM::test_diff_color_same_size\nPASSED r2e_tests/test_1.py::TestOWSOM::test_get_color_column\nPASSED r2e_tests/test_1.py::TestOWSOM::test_image_export\nPASSED r2e_tests/test_1.py::TestOWSOM::test_minimum_size\nPASSED r2e_tests/test_1.py::TestOWSOM::test_missing_all_data\nPASSED r2e_tests/test_1.py::TestOWSOM::test_missing_one_row_data\nPASSED r2e_tests/test_1.py::TestOWSOM::test_missing_some_data\nPASSED r2e_tests/test_1.py::TestOWSOM::test_msg_base_class\nPASSED r2e_tests/test_1.py::TestOWSOM::test_on_selection_change\nPASSED r2e_tests/test_1.py::TestOWSOM::test_on_selection_change_on_empty\nPASSED r2e_tests/test_1.py::TestOWSOM::test_output\nPASSED r2e_tests/test_1.py::TestOWSOM::test_pie_charts\nPASSED r2e_tests/test_1.py::TestOWSOM::test_redraw_grid\nPASSED r2e_tests/test_1.py::TestOWSOM::test_requires_continuous\nPASSED r2e_tests/test_1.py::TestOWSOM::test_same_color_diff_size\nPASSED r2e_tests/test_1.py::TestOWSOM::test_same_color_same_size\nPASSED r2e_tests/test_1.py::TestOWSOM::test_send_report\nPASSED r2e_tests/test_1.py::TestOWSOM::test_single_attribute\nPASSED r2e_tests/test_1.py::TestOWSOM::test_sparse_data\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::TestOWSOM::test_invalidated - AssertionError: Exp...\n=================== 1 failed, 24 passed, 3 skipped in 3.73s ====================\nWidgets left: 0    Max widgets: 2101 \n\n",
    "old_commit_res_stderr": ""
}
Orange/widgets/unsupervised/owsom.py Orange/widgets/unsupervised/tests/test_owsom.py
We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
[ISSUE]
**Title:** SOM widget recomputes unnecessarily when input data remains unchanged

**Description:**
When using the Self-Organizing Map (SOM) widget, providing the same dataset multiple times triggers a recomputation of the SOM each time, even though the data hasn't effectively changed. This leads to unnecessary processing and can degrade performance, especially with large datasets.

**Example Code:**
```python
heart = Table("heart_disease")
widget.recompute_som = Mock()

# Initial data input - should trigger recompute
widget.send_signal(widget.Inputs.data, heart)
widget.recompute_som.assert_called_once()

# Sending the same data again - should not trigger recompute
widget.recompute_som.reset_mock()
widget.send_signal(widget.Inputs.data, heart)
widget.recompute_som.assert_not_called()
```

**Expected Behavior:**
The SOM should only recompute when there are actual changes in the input data. Sending the same dataset a second time should not initiate another recomputation.

**Actual Behavior:**
Even when the input data remains unchanged, the SOM widget triggers a recomputation each time the data is sent, leading to redundant processing.

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

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

--- BEGIN FILE ---
```
### Orange/widgets/unsupervised/owsom.py
from collections import defaultdict, namedtuple
from xml.sax.saxutils import escape

import numpy as np
import scipy.sparse as sp

from AnyQt.QtCore import Qt, QRectF, pyqtSignal as Signal, QObject, QThread, \
    pyqtSlot as Slot
from AnyQt.QtGui import QTransform, QPen, QBrush, QColor, QPainter, \
    QPainterPath, QKeyEvent
from AnyQt.QtWidgets import \
    QGraphicsView, QGraphicsScene, \
    QGraphicsItem, QGraphicsRectItem, QGraphicsItemGroup, QSizePolicy, \
    QGraphicsPathItem

from Orange.data import Table, Domain
from Orange.preprocess import decimal_binnings, time_binnings
from Orange.projection.som import SOM

from Orange.widgets import gui
from Orange.widgets.widget import OWWidget, Msg, Input, Output
from Orange.widgets.settings import \
    DomainContextHandler, ContextSetting, Setting
from Orange.widgets.utils.itemmodels import DomainModel
from Orange.widgets.utils.widgetpreview import WidgetPreview
from Orange.widgets.utils.annotated_data import \
    create_annotated_table, create_groups_table, ANNOTATED_DATA_SIGNAL_NAME
from Orange.widgets.utils.colorpalettes import \
    BinnedContinuousPalette, LimitedDiscretePalette
from Orange.widgets.visualize.utils import CanvasRectangle, CanvasText
from Orange.widgets.visualize.utils.plotutils import wrap_legend_items


sqrt3_2 = np.sqrt(3) / 2


class SomView(QGraphicsView):
    SelectionSet, SelectionNewGroup, SelectionAddToGroup, SelectionRemove \
        = range(4)
    selection_changed = Signal(np.ndarray, int)
    selection_moved = Signal(QKeyEvent)
    selection_mark_changed = Signal(np.ndarray)

    def __init__(self, scene):
        super().__init__(scene)
        self.__button_down_pos = None
        self.size_x = self.size_y = 1
        self.hexagonal = True

    def set_dimensions(self, size_x, size_y, hexagonal):
        self.size_x = size_x
        self.size_y = size_y
        self.hexagonal = hexagonal

    def _get_marked_cells(self, event):
        x0, y0 = self.__button_down_pos.x(), self.__button_down_pos.y()
        pos = self.mapToScene(event.pos())
        x1, y1 = pos.x(), pos.y()
        x0, x1 = sorted((x0, x1))
        y0, y1 = sorted((y0, y1))
        selection = np.zeros((self.size_x, self.size_y), dtype=bool)
        if self.hexagonal:
            y0 = max(0, int(y0 / sqrt3_2 + 0.5))
            y1 = min(self.size_y, int(np.ceil(y1 / sqrt3_2 + 0.5)))
            for y in range(y0, y1):
                if x1 < 0:
                    continue
                x0_ = max(0, int(x0 + 0.5 - (y % 2) / 2))
                x1_ = min(self.size_x - y % 2,
                          int(np.ceil(x1 + 0.5 - (y % 2) / 2)))
                selection[x0_:x1_, y] = True
        elif not(x1 < -0.5 or x0 > self.size_x - 0.5
                 or y1 < -0.5 or y0 > self.size_y - 0.5):

            def roundclip(z, zmax):
                return int(np.clip(np.round(z), 0, zmax - 1))\

            x0 = roundclip(x0, self.size_x)
            y0 = roundclip(y0, self.size_y)
            x1 = roundclip(x1, self.size_x)
            y1 = roundclip(y1, self.size_y)
            selection[x0:x1 + 1, y0:y1 + 1] = True

        return selection

    def mousePressEvent(self, event):
        if event.button() != Qt.LeftButton:
            return

        self.__button_down_pos = self.mapToScene(event.pos())
        event.accept()

    def mouseMoveEvent(self, event):
        if event.buttons() != Qt.LeftButton:
            return

        self.selection_mark_changed.emit(self._get_marked_cells(event))
        event.accept()

    def mouseReleaseEvent(self, event):
        if event.button() != Qt.LeftButton:
            return

        if event.modifiers() & Qt.ShiftModifier:
            if event.modifiers() & Qt.ControlModifier:
                action = self.SelectionAddToGroup
            else:
                action = self.SelectionNewGroup
        elif event.modifiers() & Qt.AltModifier:
            action = self.SelectionRemove
        else:
            action = self.SelectionSet
        selection = self._get_marked_cells(event)
        self.selection_changed.emit(selection, action)
        event.accept()

    def keyPressEvent(self, event: QKeyEvent):
        if event.key() in (Qt.Key_Up, Qt.Key_Down, Qt.Key_Left, Qt.Key_Right):
            self.selection_moved.emit(event)
        else:
            super().keyPressEvent(event)


class PieChart(QGraphicsItem):
    def __init__(self, dist, r, colors):
        super().__init__()
        self.dist = dist
        self.r = r
        self.colors = colors

    def boundingRect(self):
        return QRectF(- self.r, - self.r, 2 * self.r, 2 * self.r)

    def paint(self, painter, _option, _index):
        painter.save()
        start_angle = 0
        pen = QPen(QBrush(Qt.black), 2)
        pen.setCosmetic(True)
        painter.setPen(pen)
        painter.drawEllipse(self.boundingRect())
        for angle, color in zip(self.dist * 16 * 360, self.colors):
            if angle == 0:
                continue
            painter.setBrush(QBrush(color))
            painter.setPen(Qt.NoPen)
            painter.drawPie(self.boundingRect(), int(start_angle), int(angle))
            start_angle += angle
        painter.restore()


class ColoredCircle(QGraphicsItem):
    def __init__(self, r, color, proportion):
        super().__init__()
        self.r = r
        self.color = color
        self.proportion = proportion

    def boundingRect(self):
        return QRectF(-self.r, -self.r, 2 * self.r, 2 * self.r)

    def paint(self, painter, _option, _index):
        painter.save()
        pen = QPen(QBrush(self.color), 2)
        pen.setCosmetic(True)
        painter.setPen(pen)
        painter.setBrush(QBrush(self.color.lighter(int(200 - 80 * self.proportion))))
        painter.drawEllipse(self.boundingRect())
        painter.restore()


N_ITERATIONS = 200


class OWSOM(OWWidget):
    name = "Self-Organizing Map"
    description = "Computation of self-organizing map."
    icon = "icons/SOM.svg"
    keywords = ["SOM"]

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

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

    settingsHandler = DomainContextHandler()
    auto_dimension = Setting(True)
    size_x = Setting(10)
    size_y = Setting(10)
    hexagonal = Setting(1)
    initialization = Setting(0)

    attr_color = ContextSetting(None)
    size_by_instances = Setting(True)
    pie_charts = Setting(False)
    selection = Setting(None, schema_only=True)

    graph_name = "view"

    _grid_pen = QPen(QBrush(QColor(224, 224, 224)), 2)
    _grid_pen.setCosmetic(True)

    OptControls = namedtuple(
        "OptControls",
        ("shape", "auto_dim", "spin_x", "spin_y", "initialization", "start")
    )

    class Warning(OWWidget.Warning):
        ignoring_disc_variables = Msg("SOM ignores categorical variables.")
        missing_colors = \
            Msg("Some data instances have undefined value of '{}'.")
        missing_values = \
            Msg("{} data instance{} with undefined value(s) {} not shown.")
        single_attribute = Msg("Data contains a single numeric column.")

    class Error(OWWidget.Error):
        no_numeric_variables = Msg("Data contains no numeric columns.")
        no_defined_rows = Msg("All rows contain at least one undefined value.")

    def __init__(self):
        super().__init__()
        self.__pending_selection = self.selection
        self._optimizer = None
        self._optimizer_thread = None
        self.stop_optimization = False

        self.data = self.cont_x = None
        self.cells = self.member_data = None
        self.selection = None
        self.colors = self.thresholds = self.bin_labels = None

        box = gui.vBox(self.controlArea, box="SOM")
        shape = gui.comboBox(
            box, self, "", items=("Hexagonal grid", "Square grid"))
        shape.setCurrentIndex(1 - self.hexagonal)

        box2 = gui.indentedBox(box, 10)
        auto_dim = gui.checkBox(
            box2, self, "auto_dimension", "Set dimensions automatically",
            callback=self.on_auto_dimension_changed)
        self.manual_box = box3 = gui.hBox(box2)
        spinargs = dict(
            value="", widget=box3, master=self, minv=5, maxv=100, step=5,
            alignment=Qt.AlignRight)
        spin_x = gui.spin(**spinargs)
        spin_x.setValue(self.size_x)
        gui.widgetLabel(box3, "×")
        spin_y = gui.spin(**spinargs)
        spin_y.setValue(self.size_y)
        gui.rubber(box3)
        self.manual_box.setEnabled(not self.auto_dimension)

        initialization = gui.comboBox(
            box, self, "initialization",
            items=("Initialize with PCA", "Random initialization",
                   "Replicable random"))

        start = gui.button(
            box, self, "Restart", callback=self.restart_som_pressed,
            sizePolicy=(QSizePolicy.MinimumExpanding, QSizePolicy.Fixed))

        self.opt_controls = self.OptControls(
            shape, auto_dim, spin_x, spin_y, initialization, start)

        box = gui.vBox(self.controlArea, "Color")
        gui.comboBox(
            box, self, "attr_color", searchable=True,
            callback=self.on_attr_color_change,
            model=DomainModel(placeholder="(Same color)",
                              valid_types=DomainModel.PRIMITIVE))
        gui.checkBox(
            box, self, "pie_charts", label="Show pie charts",
            callback=self.on_pie_chart_change)
        gui.checkBox(
            box, self, "size_by_instances", label="Size by number of instances",
            callback=self.on_attr_size_change)

        gui.rubber(self.controlArea)

        self.scene = QGraphicsScene(self)

        self.view = SomView(self.scene)
        self.view.setMinimumWidth(400)
        self.view.setMinimumHeight(400)
        self.view.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
        self.view.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
        self.view.setRenderHint(QPainter.Antialiasing)
        self.view.selection_changed.connect(self.on_selection_change)
        self.view.selection_moved.connect(self.on_selection_move)
        self.view.selection_mark_changed.connect(self.on_selection_mark_change)
        self.mainArea.layout().addWidget(self.view)

        self.elements = None
        self.grid = None
        self.grid_cells = None
        self.legend = None

    @Inputs.data
    def set_data(self, data):
        def prepare_data():
            if len(cont_attrs) < len(attrs):
                self.Warning.ignoring_disc_variables()
            if len(cont_attrs) == 1:
                self.Warning.single_attribute()
            x = Table.from_table(Domain(cont_attrs), data).X
            if sp.issparse(x):
                self.data = data
                self.cont_x = x.tocsr()
            else:
                mask = np.all(np.isfinite(x), axis=1)
                if not np.any(mask):
                    self.Error.no_defined_rows()
                else:
                    if np.all(mask):
                        self.data = data
                        self.cont_x = x.copy()
                    else:
                        self.data = data[mask]
                        self.cont_x = x[mask]
                    self.cont_x -= np.min(self.cont_x, axis=0)[None, :]
                    sums = np.sum(self.cont_x, axis=0)[None, :]
                    sums[sums == 0] = 1
                    self.cont_x /= sums

        def set_warnings():
            missing = len(data) - len(self.data)
            if missing == 1:
                self.Warning.missing_values(1, "", "is")
            elif missing > 1:
                self.Warning.missing_values(missing, "s", "are")

        self.stop_optimization_and_wait()

        self.closeContext()
        self.clear()
        self.Error.clear()
        self.Warning.clear()

        if data is not None:
            attrs = data.domain.attributes
            cont_attrs = [var for var in attrs if var.is_continuous]
            if not cont_attrs:
                self.Error.no_numeric_variables()
            else:
                prepare_data()

        if self.data is not None:
            self.controls.attr_color.model().set_domain(data.domain)
            self.attr_color = data.domain.class_var
            set_warnings()

        self.openContext(self.data)
        self.set_color_bins()
        self.create_legend()
        self.recompute_dimensions()
        self.start_som()

    def clear(self):
        self.data = self.cont_x = None
        self.cells = self.member_data = None
        self.attr_color = None
        self.colors = self.thresholds = self.bin_labels = None
        if self.elements is not None:
            self.scene.removeItem(self.elements)
            self.elements = None
        self.clear_selection()
        self.controls.attr_color.model().set_domain(None)
        self.Warning.clear()
        self.Error.clear()

    def recompute_dimensions(self):
        if not self.auto_dimension or self.cont_x is None:
            return
        dim = max(5, int(np.ceil(np.sqrt(5 * np.sqrt(self.cont_x.shape[0])))))
        self.opt_controls.spin_x.setValue(dim)
        self.opt_controls.spin_y.setValue(dim)

    def on_auto_dimension_changed(self):
        self.manual_box.setEnabled(not self.auto_dimension)
        if self.auto_dimension:
            self.recompute_dimensions()
        else:
            spin_x = self.opt_controls.spin_x
            spin_y = self.opt_controls.spin_y
            dimx = int(5 * np.round(spin_x.value() / 5))
            dimy = int(5 * np.round(spin_y.value() / 5))
            spin_x.setValue(dimx)
            spin_y.setValue(dimy)

    def on_attr_color_change(self):
        self.controls.pie_charts.setEnabled(self.attr_color is not None)
        self.set_color_bins()
        self.create_legend()
        self.rescale()
        self._redraw()

    def on_attr_size_change(self):
        self._redraw()

    def on_pie_chart_change(self):
        self._redraw()

    def clear_selection(self):
        self.selection = None
        self.redraw_selection()

    def on_selection_change(self, selection, action=SomView.SelectionSet):
        if self.data is None:  # clicks on empty canvas
            return
        if self.selection is None:
            self.selection = np.zeros(self.grid_cells.T.shape, dtype=np.int16)
        if action == SomView.SelectionSet:
            self.selection[:] = 0
            self.selection[selection] = 1
        elif action == SomView.SelectionAddToGroup:
            self.selection[selection] = max(1, np.max(self.selection))
        elif action == SomView.SelectionNewGroup:
            self.selection[selection] = 1 + np.max(self.selection)
        elif action & SomView.SelectionRemove:
            self.selection[selection] = 0
        self.redraw_selection()
        self.update_output()

    def on_selection_move(self, event: QKeyEvent):
        if self.selection is None or not np.any(self.selection):
            if event.key() in (Qt.Key_Right, Qt.Key_Down):
                x = y = 0
            else:
                x = self.size_x - 1
                y = self.size_y - 1
        else:
            x, y = np.nonzero(self.selection)
            if len(x) > 1:
                return
            if event.key() == Qt.Key_Up and y > 0:
                y -= 1
            if event.key() == Qt.Key_Down and y < self.size_y - 1:
                y += 1
            if event.key() == Qt.Key_Left and x:
                x -= 1
            if event.key() == Qt.Key_Right and x < self.size_x - 1:
                x += 1
            x -= self.hexagonal and x == self.size_x - 1 and y % 2

        if self.selection is not None and self.selection[x, y]:
            return
        selection = np.zeros(self.grid_cells.shape, dtype=bool)
        selection[x, y] = True
        self.on_selection_change(selection)

    def on_selection_mark_change(self, marks):
        self.redraw_selection(marks=marks)

    def redraw_selection(self, marks=None):
        if self.grid_cells is None:
            return

        sel_pen = QPen(QBrush(QColor(128, 128, 128)), 2)
        sel_pen.setCosmetic(True)
        mark_pen = QPen(QBrush(QColor(128, 128, 128)), 4)
        mark_pen.setCosmetic(True)
        pens = [self._grid_pen, sel_pen]

        mark_brush = QBrush(QColor(224, 255, 255))
        sels = self.selection is not None and np.max(self.selection)
        palette = LimitedDiscretePalette(number_of_colors=sels + 1)
        brushes = [QBrush(Qt.NoBrush)] + \
                  [QBrush(palette[i].lighter(165)) for i in range(sels)]

        for y in range(self.size_y):
            for x in range(self.size_x - (y % 2) * self.hexagonal):
                cell = self.grid_cells[y, x]
                marked = marks is not None and marks[x, y]
                sel_group = self.selection is not None and self.selection[x, y]
                if marked:
                    cell.setBrush(mark_brush)
                    cell.setPen(mark_pen)
                else:
                    cell.setBrush(brushes[sel_group])
                    cell.setPen(pens[bool(sel_group)])
                cell.setZValue(marked or sel_group)

    def restart_som_pressed(self):
        if self._optimizer_thread is not None:
            self.stop_optimization = True
            self._optimizer.stop_optimization = True
        else:
            self.start_som()

    def start_som(self):
        self.read_controls()
        self.update_layout()
        self.clear_selection()
        if self.cont_x is not None:
            self.enable_controls(False)
            self._recompute_som()
        else:
            self.update_output()

    def read_controls(self):
        c = self.opt_controls
        self.hexagonal = c.shape.currentIndex() == 0
        self.size_x = c.spin_x.value()
        self.size_y = c.spin_y.value()

    def enable_controls(self, enable):
        c = self.opt_controls
        c.shape.setEnabled(enable)
        c.auto_dim.setEnabled(enable)
        c.start.setText("Start" if enable else "Stop")

    def update_layout(self):
        self.set_legend_pos()
        if self.elements:  # Prevent having redrawn grid but with old elements
            self.scene.removeItem(self.elements)
            self.elements = None
        self.redraw_grid()
        self.rescale()

    def _redraw(self):
        self.Warning.missing_colors.clear()
        if self.elements:
            self.scene.removeItem(self.elements)
            self.elements = None
        self.view.set_dimensions(self.size_x, self.size_y, self.hexagonal)

        if self.cells is None:
            return
        sizes = self.cells[:, :, 1] - self.cells[:, :, 0]
        sizes = sizes.astype(float)
        if not self.size_by_instances:
            sizes[sizes != 0] = 0.8
        else:
            sizes *= 0.8 / np.max(sizes)

        self.elements = QGraphicsItemGroup()
        self.scene.addItem(self.elements)
        if self.attr_color is None:
            self._draw_same_color(sizes)
        elif self.pie_charts:
            self._draw_pie_charts(sizes)
        else:
            self._draw_colored_circles(sizes)

    @property
    def _grid_factors(self):
        return (0.5, sqrt3_2) if self.hexagonal else (0, 1)

    def _draw_same_color(self, sizes):
        fx, fy = self._grid_factors
        color = QColor(64, 64, 64)
        for y in range(self.size_y):
            for x in range(self.size_x - self.hexagonal * (y % 2)):
                r = sizes[x, y]
                n = len(self.get_member_indices(x, y))
                if not r:
                    continue
                ellipse = ColoredCircle(r / 2, color, 0)
                ellipse.setPos(x + (y % 2) * fx, y * fy)
                ellipse.setToolTip(f"{n} instances")
                self.elements.addToGroup(ellipse)

    def _get_color_column(self):
        color_column = \
            self.data.get_column_view(self.attr_color)[0].astype(float,
                                                                 copy=False)
        if self.attr_color.is_discrete:
            with np.errstate(invalid="ignore"):
                int_col = color_column.astype(int)
            int_col[np.isnan(color_column)] = len(self.colors)
        else:
            int_col = np.zeros(len(color_column), dtype=int)
            # The following line is unnecessary because rows with missing
            # numeric data are excluded. Uncomment it if you change SOM to
            # tolerate missing values.
            # int_col[np.isnan(color_column)] = len(self.colors)
            for i, thresh in enumerate(self.thresholds, start=1):
                int_col[color_column >= thresh] = i
        return int_col

    def _tooltip(self, colors, distribution):
        if self.attr_color.is_discrete:
            values = self.attr_color.values
        else:
            values = self._bin_names()
        tot = np.sum(distribution)
        nbhp = "\N{NON-BREAKING HYPHEN}"
        return '<table style="white-space: nowrap">' + "".join(f"""
            <tr>
                <td>
                    <font color={color.name()}>■</font>
                    <b>{escape(val).replace("-", nbhp)}</b>:
                </td>
                <td>
                    {n} ({n / tot * 100:.1f}&nbsp;%)
                </td>
            </tr>
            """ for color, val, n in zip(colors, values, distribution) if n) \
            + "</table>"

    def _draw_pie_charts(self, sizes):
        fx, fy = self._grid_factors
        color_column = self._get_color_column()
        colors = self.colors.qcolors_w_nan
        for y in range(self.size_y):
            for x in range(self.size_x - self.hexagonal * (y % 2)):
                r = sizes[x, y]
                if not r:
                    self.grid_cells[y, x].setToolTip("")
                    continue
                members = self.get_member_indices(x, y)
                color_dist = np.bincount(color_column[members],
                                         minlength=len(colors))
                rel_color_dist = color_dist.astype(float) / len(members)
                pie = PieChart(rel_color_dist, r / 2, colors)
                pie.setToolTip(self._tooltip(colors, color_dist))
                self.elements.addToGroup(pie)
                pie.setPos(x + (y % 2) * fx, y * fy)

    def _draw_colored_circles(self, sizes):
        fx, fy = self._grid_factors
        color_column = self._get_color_column()
        qcolors = self.colors.qcolors_w_nan
        for y in range(self.size_y):
            for x in range(self.size_x - self.hexagonal * (y % 2)):
                r = sizes[x, y]
                if not r:
                    continue
                members = self.get_member_indices(x, y)
                color_dist = color_column[members]
                color_dist = color_dist[color_dist < len(self.colors)]
                if len(color_dist) != len(members):
                    self.Warning.missing_colors(self.attr_color.name)
                bc = np.bincount(color_dist, minlength=len(self.colors))
                color = qcolors[np.argmax(bc)]
                ellipse = ColoredCircle(r / 2, color, np.max(bc) / len(members))
                ellipse.setPos(x + (y % 2) * fx, y * fy)
                ellipse.setToolTip(self._tooltip(qcolors, bc))
                self.elements.addToGroup(ellipse)

    def redraw_grid(self):
        if self.grid is not None:
            self.scene.removeItem(self.grid)
        self.grid = QGraphicsItemGroup()
        self.grid.setZValue(-200)
        self.grid_cells = np.full((self.size_y, self.size_x), None)
        for y in range(self.size_y):
            for x in range(self.size_x - (y % 2) * self.hexagonal):
                if self.hexagonal:
                    cell = QGraphicsPathItem(_hexagon_path)
                    cell.setPos(x + (y % 2) / 2, y * sqrt3_2)
                else:
                    cell = QGraphicsRectItem(x - 0.5, y - 0.5, 1, 1)
                self.grid_cells[y, x] = cell
                cell.setPen(self._grid_pen)
                self.grid.addToGroup(cell)
        self.scene.addItem(self.grid)

    def get_member_indices(self, x, y):
        i, j = self.cells[x, y]
        return self.member_data[i:j]

    def _recompute_som(self):
        if self.cont_x is None:
            return

        som = SOM(
            self.size_x, self.size_y,
            hexagonal=self.hexagonal,
            pca_init=self.initialization == 0,
            random_seed=0 if self.initialization == 2 else None
        )

        class Optimizer(QObject):
            update = Signal(float, np.ndarray, np.ndarray)
            done = Signal(SOM)
            stopped = Signal()
            stop_optimization = False

            def __init__(self, data, som):
                super().__init__()
                self.som = som
                self.data = data

            def callback(self, progress):
                self.update.emit(
                    progress,
                    self.som.weights.copy(), self.som.ssum_weights.copy())
                return not self.stop_optimization

            def run(self):
                try:
                    self.som.fit(self.data, N_ITERATIONS,
                                 callback=self.callback)
                    # Report an exception, but still remove the thread
                finally:
                    self.done.emit(self.som)
                    self.stopped.emit()

        def thread_finished():
            self._optimizer = None
            self._optimizer_thread = None

        self.progressBarInit()

        self._optimizer = Optimizer(self.cont_x, som)
        self._optimizer_thread = QThread()
        self._optimizer_thread.setStackSize(5 * 2 ** 20)
        self._optimizer.update.connect(self.__update)
        self._optimizer.done.connect(self.__done)
        self._optimizer.stopped.connect(self._optimizer_thread.quit)
        self._optimizer.moveToThread(self._optimizer_thread)
        self._optimizer_thread.started.connect(self._optimizer.run)
        self._optimizer_thread.finished.connect(thread_finished)
        self.stop_optimization = False
        self._optimizer_thread.start()

    @Slot(float, object, object)
    def __update(self, _progress, weights, ssum_weights):
        self.progressBarSet(_progress)
        self._assign_instances(weights, ssum_weights)
        self._redraw()

    @Slot(object)
    def __done(self, som):
        self.enable_controls(True)
        self.progressBarFinished()
        self._assign_instances(som.weights, som.ssum_weights)
        self._redraw()
        # This is the first time we know what was selected (assuming that
        # initialization is not set to random)
        if self.__pending_selection is not None:
            self.on_selection_change(self.__pending_selection)
            self.__pending_selection = None
        self.update_output()

    def stop_optimization_and_wait(self):
        if self._optimizer_thread is not None:
            self.stop_optimization = True
            self._optimizer.stop_optimization = True
            self._optimizer_thread.quit()
            self._optimizer_thread.wait()
            self._optimizer_thread = None

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

    def _assign_instances(self, weights, ssum_weights):
        if self.cont_x is None:
            return  # the widget is shutting down while signals still processed
        assignments = SOM.winner_from_weights(
            self.cont_x, weights, ssum_weights, self.hexagonal)
        members = defaultdict(list)
        for i, (x, y) in enumerate(assignments):
            members[(x, y)].append(i)
        members.pop(None, None)
        self.cells = np.empty((self.size_x, self.size_y, 2), dtype=int)
        self.member_data = np.empty(self.cont_x.shape[0], dtype=int)
        index = 0
        for x in range(self.size_x):
            for y in range(self.size_y):
                nmembers = len(members[(x, y)])
                self.member_data[index:index + nmembers] = members[(x, y)]
                self.cells[x, y] = [index, index + nmembers]
                index += nmembers

    def resizeEvent(self, event):
        super().resizeEvent(event)
        self.create_legend()  # re-wrap lines if necessary
        self.rescale()

    def rescale(self):
        if self.legend:
            leg_height = self.legend.boundingRect().height()
            leg_extra = 1.5
        else:
            leg_height = 0
            leg_extra = 1

        vw, vh = self.view.width(), self.view.height() - leg_height
        scale = min(vw / (self.size_x + 1),
                    vh / ((self.size_y + leg_extra) * self._grid_factors[1]))
        self.view.setTransform(QTransform.fromScale(scale, scale))
        if self.hexagonal:
            self.view.setSceneRect(
                0, -1, self.size_x - 1,
                (self.size_y + leg_extra) * sqrt3_2 + leg_height / scale)
        else:
            self.view.setSceneRect(
                -0.25, -0.25, self.size_x - 0.5,
                self.size_y - 0.5 + leg_height / scale)

    def update_output(self):
        if self.data is None:
            self.Outputs.selected_data.send(None)
            self.Outputs.annotated_data.send(None)
            return

        indices = np.zeros(len(self.data), dtype=int)
        if self.selection is not None and np.any(self.selection):
            for y in range(self.size_y):
                for x in range(self.size_x):
                    rows = self.get_member_indices(x, y)
                    indices[rows] = self.selection[x, y]

        if np.any(indices):
            sel_data = create_groups_table(self.data, indices, False, "Group")
            self.Outputs.selected_data.send(sel_data)
        else:
            self.Outputs.selected_data.send(None)

        if np.max(indices) > 1:
            annotated = create_groups_table(self.data, indices)
        else:
            annotated = create_annotated_table(
                self.data, np.flatnonzero(indices))
        self.Outputs.annotated_data.send(annotated)

    def set_color_bins(self):
        if self.attr_color is None:
            self.thresholds = self.bin_labels = self.colors = None
        elif self.attr_color.is_discrete:
            self.thresholds = self.bin_labels = None
            self.colors = self.attr_color.palette
        else:
            col = self.data.get_column_view(self.attr_color)[0].astype(float)
            if self.attr_color.is_time:
                binning = time_binnings(col, min_bins=4)[-1]
            else:
                binning = decimal_binnings(col, min_bins=4)[-1]
            self.thresholds = binning.thresholds[1:-1]
            self.bin_labels = (binning.labels[1:-1], binning.short_labels[1:-1])
            palette = BinnedContinuousPalette.from_palette(
                self.attr_color.palette, binning.thresholds)
            self.colors = palette

    def create_legend(self):
        if self.legend is not None:
            self.scene.removeItem(self.legend)
            self.legend = None
        if self.attr_color is None:
            return

        if self.attr_color.is_discrete:
            names = self.attr_color.values
        else:
            names = self._bin_names()

        items = []
        size = 8
        for name, color in zip(names, self.colors.qcolors):
            item = QGraphicsItemGroup()
            item.addToGroup(
                CanvasRectangle(None, -size / 2, -size / 2, size, size,
                                Qt.gray, color))
            item.addToGroup(CanvasText(None, name, size, 0, Qt.AlignVCenter))
            items.append(item)

        self.legend = wrap_legend_items(
            items, hspacing=20, vspacing=16 + size,
            max_width=self.view.width() - 25)
        self.legend.setFlags(self.legend.ItemIgnoresTransformations)
        self.legend.setTransform(
            QTransform.fromTranslate(-self.legend.boundingRect().width() / 2,
                                     0))
        self.scene.addItem(self.legend)
        self.set_legend_pos()

    def _bin_names(self):
        labels, short_labels = self.bin_labels
        return \
            [f"< {labels[0]}"] \
            + [f"{x} - {y}" for x, y in zip(labels, short_labels[1:])] \
            + [f"≥ {labels[-1]}"]

    def set_legend_pos(self):
        if self.legend is None:
            return
        self.legend.setPos(
            self.size_x / 2,
            (self.size_y + 0.2 + 0.3 * self.hexagonal) * self._grid_factors[1])

    def send_report(self):
        self.report_plot()
        if self.attr_color:
            self.report_caption(
                f"Self-organizing map colored by '{self.attr_color.name}'")


def _draw_hexagon():
    path = QPainterPath()
    s = 0.5 / (np.sqrt(3) / 2)
    path.moveTo(-0.5, -s / 2)
    path.lineTo(-0.5, s / 2)
    path.lineTo(0, s)
    path.lineTo(0.5, s / 2)
    path.lineTo(0.5, -s / 2)
    path.lineTo(0, -s)
    path.lineTo(-0.5, -s / 2)
    return path


_hexagon_path = _draw_hexagon()


if __name__ == "__main__":  # pragma: no cover
    WidgetPreview(OWSOM).run(Table("iris"))

```
--- 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:** SOM widget recomputes unnecessarily when input data remains unchanged

**Description:**
When using the Self-Organizing Map (SOM) widget, providing the same dataset multiple times triggers a recomputation of the SOM each time, even though the data hasn't effectively changed. This leads to unnecessary processing and can degrade performance, especially with large datasets.

**Example Code:**
```python
heart = Table("heart_disease")
widget.recompute_som = Mock()

# Initial data input - should trigger recompute
widget.send_signal(widget.Inputs.data, heart)
widget.recompute_som.assert_called_once()

# Sending the same data again - should not trigger recompute
widget.recompute_som.reset_mock()
widget.send_signal(widget.Inputs.data, heart)
widget.recompute_som.assert_not_called()
```

**Expected Behavior:**
The SOM should only recompute when there are actual changes in the input data. Sending the same dataset a second time should not initiate another recomputation.

**Actual Behavior:**
Even when the input data remains unchanged, the SOM widget triggers a recomputation each time the data is sent, leading to redundant processing.

[/ISSUE]
{
    "TestOWSOM.test_attr_color_change": "PASSED",
    "TestOWSOM.test_auto_compute_dimensions": "PASSED",
    "TestOWSOM.test_cell_sizes": "PASSED",
    "TestOWSOM.test_colored_circles_with_missing_values": "PASSED",
    "TestOWSOM.test_diff_color_diff_size": "PASSED",
    "TestOWSOM.test_diff_color_same_size": "PASSED",
    "TestOWSOM.test_get_color_column": "PASSED",
    "TestOWSOM.test_image_export": "PASSED",
    "TestOWSOM.test_invalidated": "PASSED",
    "TestOWSOM.test_minimum_size": "PASSED",
    "TestOWSOM.test_missing_all_data": "PASSED",
    "TestOWSOM.test_missing_one_row_data": "PASSED",
    "TestOWSOM.test_missing_some_data": "PASSED",
    "TestOWSOM.test_msg_base_class": "PASSED",
    "TestOWSOM.test_on_selection_change": "PASSED",
    "TestOWSOM.test_on_selection_change_on_empty": "PASSED",
    "TestOWSOM.test_output": "PASSED",
    "TestOWSOM.test_pie_charts": "PASSED",
    "TestOWSOM.test_redraw_grid": "PASSED",
    "TestOWSOM.test_requires_continuous": "PASSED",
    "TestOWSOM.test_same_color_diff_size": "PASSED",
    "TestOWSOM.test_same_color_same_size": "PASSED",
    "TestOWSOM.test_send_report": "PASSED",
    "TestOWSOM.test_single_attribute": "PASSED",
    "TestOWSOM.test_sparse_data": "PASSED"
}
diff --git a/Orange/widgets/unsupervised/owsom.py b/Orange/widgets/unsupervised/owsom.py
index 65cdda50f..f29d774cd 100644
--- a/Orange/widgets/unsupervised/owsom.py
+++ b/Orange/widgets/unsupervised/owsom.py
@@ -14,6 +14,7 @@ from AnyQt.QtWidgets import \
     QGraphicsPathItem
 
 from Orange.data import Table, Domain
+from Orange.data.util import array_equal
 from Orange.preprocess import decimal_binnings, time_binnings
 from Orange.projection.som import SOM
 
@@ -330,12 +331,10 @@ class OWSOM(OWWidget):
             elif missing > 1:
                 self.Warning.missing_values(missing, "s", "are")
 
-        self.stop_optimization_and_wait()
-
+        cont_x = self.cont_x.copy() if self.cont_x is not None else None
+        self.data = self.cont_x = None
         self.closeContext()
-        self.clear()
-        self.Error.clear()
-        self.Warning.clear()
+        self.clear_messages()
 
         if data is not None:
             attrs = data.domain.attributes
@@ -345,6 +344,12 @@ class OWSOM(OWWidget):
             else:
                 prepare_data()
 
+        invalidated = cont_x is None or self.cont_x is None \
+            or not array_equal(cont_x, self.cont_x)
+        if invalidated:
+            self.stop_optimization_and_wait()
+            self.clear()
+
         if self.data is not None:
             self.controls.attr_color.model().set_domain(data.domain)
             self.attr_color = data.domain.class_var
@@ -353,11 +358,13 @@ class OWSOM(OWWidget):
         self.openContext(self.data)
         self.set_color_bins()
         self.create_legend()
-        self.recompute_dimensions()
-        self.start_som()
+        if invalidated:
+            self.recompute_dimensions()
+            self.start_som()
+        else:
+            self._redraw()
 
     def clear(self):
-        self.data = self.cont_x = None
         self.cells = self.member_data = None
         self.attr_color = None
         self.colors = self.thresholds = self.bin_labels = None
@@ -366,8 +373,6 @@ class OWSOM(OWWidget):
             self.elements = None
         self.clear_selection()
         self.controls.attr_color.model().set_domain(None)
-        self.Warning.clear()
-        self.Error.clear()
 
     def recompute_dimensions(self):
         if not self.auto_dimension or self.cont_x is None:

{"Orange/widgets/unsupervised/owsom.py": "from collections import defaultdict, namedtuple\nfrom xml.sax.saxutils import escape\n\nimport numpy as np\nimport scipy.sparse as sp\n\nfrom AnyQt.QtCore import Qt, QRectF, pyqtSignal as Signal, QObject, QThread, \\\n    pyqtSlot as Slot\nfrom AnyQt.QtGui import QTransform, QPen, QBrush, QColor, QPainter, \\\n    QPainterPath, QKeyEvent\nfrom AnyQt.QtWidgets import \\\n    QGraphicsView, QGraphicsScene, \\\n    QGraphicsItem, QGraphicsRectItem, QGraphicsItemGroup, QSizePolicy, \\\n    QGraphicsPathItem\n\nfrom Orange.data import Table, Domain\nfrom Orange.preprocess import decimal_binnings, time_binnings\nfrom Orange.projection.som import SOM\n\nfrom Orange.widgets import gui\nfrom Orange.widgets.widget import OWWidget, Msg, Input, Output\nfrom Orange.widgets.settings import \\\n    DomainContextHandler, ContextSetting, Setting\nfrom Orange.widgets.utils.itemmodels import DomainModel\nfrom Orange.widgets.utils.widgetpreview import WidgetPreview\nfrom Orange.widgets.utils.annotated_data import \\\n    create_annotated_table, create_groups_table, ANNOTATED_DATA_SIGNAL_NAME\nfrom Orange.widgets.utils.colorpalettes import \\\n    BinnedContinuousPalette, LimitedDiscretePalette\nfrom Orange.widgets.visualize.utils import CanvasRectangle, CanvasText\nfrom Orange.widgets.visualize.utils.plotutils import wrap_legend_items\n\n\nsqrt3_2 = np.sqrt(3) / 2\n\n\nclass SomView(QGraphicsView):\n    SelectionSet, SelectionNewGroup, SelectionAddToGroup, SelectionRemove \\\n        = range(4)\n    selection_changed = Signal(np.ndarray, int)\n    selection_moved = Signal(QKeyEvent)\n    selection_mark_changed = Signal(np.ndarray)\n\n    def __init__(self, scene):\n        super().__init__(scene)\n        self.__button_down_pos = None\n        self.size_x = self.size_y = 1\n        self.hexagonal = True\n\n    def set_dimensions(self, size_x, size_y, hexagonal):\n        self.size_x = size_x\n        self.size_y = size_y\n        self.hexagonal = hexagonal\n\n    def _get_marked_cells(self, event):\n        x0, y0 = self.__button_down_pos.x(), self.__button_down_pos.y()\n        pos = self.mapToScene(event.pos())\n        x1, y1 = pos.x(), pos.y()\n        x0, x1 = sorted((x0, x1))\n        y0, y1 = sorted((y0, y1))\n        selection = np.zeros((self.size_x, self.size_y), dtype=bool)\n        if self.hexagonal:\n            y0 = max(0, int(y0 / sqrt3_2 + 0.5))\n            y1 = min(self.size_y, int(np.ceil(y1 / sqrt3_2 + 0.5)))\n            for y in range(y0, y1):\n                if x1 < 0:\n                    continue\n                x0_ = max(0, int(x0 + 0.5 - (y % 2) / 2))\n                x1_ = min(self.size_x - y % 2,\n                          int(np.ceil(x1 + 0.5 - (y % 2) / 2)))\n                selection[x0_:x1_, y] = True\n        elif not(x1 < -0.5 or x0 > self.size_x - 0.5\n                 or y1 < -0.5 or y0 > self.size_y - 0.5):\n\n            def roundclip(z, zmax):\n                return int(np.clip(np.round(z), 0, zmax - 1))\\\n\n            x0 = roundclip(x0, self.size_x)\n            y0 = roundclip(y0, self.size_y)\n            x1 = roundclip(x1, self.size_x)\n            y1 = roundclip(y1, self.size_y)\n            selection[x0:x1 + 1, y0:y1 + 1] = True\n\n        return selection\n\n    def mousePressEvent(self, event):\n        if event.button() != Qt.LeftButton:\n            return\n\n        self.__button_down_pos = self.mapToScene(event.pos())\n        event.accept()\n\n    def mouseMoveEvent(self, event):\n        if event.buttons() != Qt.LeftButton:\n            return\n\n        self.selection_mark_changed.emit(self._get_marked_cells(event))\n        event.accept()\n\n    def mouseReleaseEvent(self, event):\n        if event.button() != Qt.LeftButton:\n            return\n\n        if event.modifiers() & Qt.ShiftModifier:\n            if event.modifiers() & Qt.ControlModifier:\n                action = self.SelectionAddToGroup\n            else:\n                action = self.SelectionNewGroup\n        elif event.modifiers() & Qt.AltModifier:\n            action = self.SelectionRemove\n        else:\n            action = self.SelectionSet\n        selection = self._get_marked_cells(event)\n        self.selection_changed.emit(selection, action)\n        event.accept()\n\n    def keyPressEvent(self, event: QKeyEvent):\n        if event.key() in (Qt.Key_Up, Qt.Key_Down, Qt.Key_Left, Qt.Key_Right):\n            self.selection_moved.emit(event)\n        else:\n            super().keyPressEvent(event)\n\n\nclass PieChart(QGraphicsItem):\n    def __init__(self, dist, r, colors):\n        super().__init__()\n        self.dist = dist\n        self.r = r\n        self.colors = colors\n\n    def boundingRect(self):\n        return QRectF(- self.r, - self.r, 2 * self.r, 2 * self.r)\n\n    def paint(self, painter, _option, _index):\n        painter.save()\n        start_angle = 0\n        pen = QPen(QBrush(Qt.black), 2)\n        pen.setCosmetic(True)\n        painter.setPen(pen)\n        painter.drawEllipse(self.boundingRect())\n        for angle, color in zip(self.dist * 16 * 360, self.colors):\n            if angle == 0:\n                continue\n            painter.setBrush(QBrush(color))\n            painter.setPen(Qt.NoPen)\n            painter.drawPie(self.boundingRect(), int(start_angle), int(angle))\n            start_angle += angle\n        painter.restore()\n\n\nclass ColoredCircle(QGraphicsItem):\n    def __init__(self, r, color, proportion):\n        super().__init__()\n        self.r = r\n        self.color = color\n        self.proportion = proportion\n\n    def boundingRect(self):\n        return QRectF(-self.r, -self.r, 2 * self.r, 2 * self.r)\n\n    def paint(self, painter, _option, _index):\n        painter.save()\n        pen = QPen(QBrush(self.color), 2)\n        pen.setCosmetic(True)\n        painter.setPen(pen)\n        painter.setBrush(QBrush(self.color.lighter(int(200 - 80 * self.proportion))))\n        painter.drawEllipse(self.boundingRect())\n        painter.restore()\n\n\nN_ITERATIONS = 200\n\n\nclass OWSOM(OWWidget):\n    name = \"Self-Organizing Map\"\n    description = \"Computation of self-organizing map.\"\n    icon = \"icons/SOM.svg\"\n    keywords = [\"SOM\"]\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    settingsHandler = DomainContextHandler()\n    auto_dimension = Setting(True)\n    size_x = Setting(10)\n    size_y = Setting(10)\n    hexagonal = Setting(1)\n    initialization = Setting(0)\n\n    attr_color = ContextSetting(None)\n    size_by_instances = Setting(True)\n    pie_charts = Setting(False)\n    selection = Setting(None, schema_only=True)\n\n    graph_name = \"view\"\n\n    _grid_pen = QPen(QBrush(QColor(224, 224, 224)), 2)\n    _grid_pen.setCosmetic(True)\n\n    OptControls = namedtuple(\n        \"OptControls\",\n        (\"shape\", \"auto_dim\", \"spin_x\", \"spin_y\", \"initialization\", \"start\")\n    )\n\n    class Warning(OWWidget.Warning):\n        ignoring_disc_variables = Msg(\"SOM ignores categorical variables.\")\n        missing_colors = \\\n            Msg(\"Some data instances have undefined value of '{}'.\")\n        missing_values = \\\n            Msg(\"{} data instance{} with undefined value(s) {} not shown.\")\n        single_attribute = Msg(\"Data contains a single numeric column.\")\n\n    class Error(OWWidget.Error):\n        no_numeric_variables = Msg(\"Data contains no numeric columns.\")\n        no_defined_rows = Msg(\"All rows contain at least one undefined value.\")\n\n    def __init__(self):\n        super().__init__()\n        self.__pending_selection = self.selection\n        self._optimizer = None\n        self._optimizer_thread = None\n        self.stop_optimization = False\n\n        self.data = self.cont_x = None\n        self.cells = self.member_data = None\n        self.selection = None\n        self.colors = self.thresholds = self.bin_labels = None\n\n        box = gui.vBox(self.controlArea, box=\"SOM\")\n        shape = gui.comboBox(\n            box, self, \"\", items=(\"Hexagonal grid\", \"Square grid\"))\n        shape.setCurrentIndex(1 - self.hexagonal)\n\n        box2 = gui.indentedBox(box, 10)\n        auto_dim = gui.checkBox(\n            box2, self, \"auto_dimension\", \"Set dimensions automatically\",\n            callback=self.on_auto_dimension_changed)\n        self.manual_box = box3 = gui.hBox(box2)\n        spinargs = dict(\n            value=\"\", widget=box3, master=self, minv=5, maxv=100, step=5,\n            alignment=Qt.AlignRight)\n        spin_x = gui.spin(**spinargs)\n        spin_x.setValue(self.size_x)\n        gui.widgetLabel(box3, \"\u00d7\")\n        spin_y = gui.spin(**spinargs)\n        spin_y.setValue(self.size_y)\n        gui.rubber(box3)\n        self.manual_box.setEnabled(not self.auto_dimension)\n\n        initialization = gui.comboBox(\n            box, self, \"initialization\",\n            items=(\"Initialize with PCA\", \"Random initialization\",\n                   \"Replicable random\"))\n\n        start = gui.button(\n            box, self, \"Restart\", callback=self.restart_som_pressed,\n            sizePolicy=(QSizePolicy.MinimumExpanding, QSizePolicy.Fixed))\n\n        self.opt_controls = self.OptControls(\n            shape, auto_dim, spin_x, spin_y, initialization, start)\n\n        box = gui.vBox(self.controlArea, \"Color\")\n        gui.comboBox(\n            box, self, \"attr_color\", searchable=True,\n            callback=self.on_attr_color_change,\n            model=DomainModel(placeholder=\"(Same color)\",\n                              valid_types=DomainModel.PRIMITIVE))\n        gui.checkBox(\n            box, self, \"pie_charts\", label=\"Show pie charts\",\n            callback=self.on_pie_chart_change)\n        gui.checkBox(\n            box, self, \"size_by_instances\", label=\"Size by number of instances\",\n            callback=self.on_attr_size_change)\n\n        gui.rubber(self.controlArea)\n\n        self.scene = QGraphicsScene(self)\n\n        self.view = SomView(self.scene)\n        self.view.setMinimumWidth(400)\n        self.view.setMinimumHeight(400)\n        self.view.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)\n        self.view.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)\n        self.view.setRenderHint(QPainter.Antialiasing)\n        self.view.selection_changed.connect(self.on_selection_change)\n        self.view.selection_moved.connect(self.on_selection_move)\n        self.view.selection_mark_changed.connect(self.on_selection_mark_change)\n        self.mainArea.layout().addWidget(self.view)\n\n        self.elements = None\n        self.grid = None\n        self.grid_cells = None\n        self.legend = None\n\n    @Inputs.data\n    def set_data(self, data):\n        def prepare_data():\n            if len(cont_attrs) < len(attrs):\n                self.Warning.ignoring_disc_variables()\n            if len(cont_attrs) == 1:\n                self.Warning.single_attribute()\n            x = Table.from_table(Domain(cont_attrs), data).X\n            if sp.issparse(x):\n                self.data = data\n                self.cont_x = x.tocsr()\n            else:\n                mask = np.all(np.isfinite(x), axis=1)\n                if not np.any(mask):\n                    self.Error.no_defined_rows()\n                else:\n                    if np.all(mask):\n                        self.data = data\n                        self.cont_x = x.copy()\n                    else:\n                        self.data = data[mask]\n                        self.cont_x = x[mask]\n                    self.cont_x -= np.min(self.cont_x, axis=0)[None, :]\n                    sums = np.sum(self.cont_x, axis=0)[None, :]\n                    sums[sums == 0] = 1\n                    self.cont_x /= sums\n\n        def set_warnings():\n            missing = len(data) - len(self.data)\n            if missing == 1:\n                self.Warning.missing_values(1, \"\", \"is\")\n            elif missing > 1:\n                self.Warning.missing_values(missing, \"s\", \"are\")\n\n        self.stop_optimization_and_wait()\n\n        self.closeContext()\n        self.clear()\n        self.Error.clear()\n        self.Warning.clear()\n\n        if data is not None:\n            attrs = data.domain.attributes\n            cont_attrs = [var for var in attrs if var.is_continuous]\n            if not cont_attrs:\n                self.Error.no_numeric_variables()\n            else:\n                prepare_data()\n\n        if self.data is not None:\n            self.controls.attr_color.model().set_domain(data.domain)\n            self.attr_color = data.domain.class_var\n            set_warnings()\n\n        self.openContext(self.data)\n        self.set_color_bins()\n        self.create_legend()\n        self.recompute_dimensions()\n        self.start_som()\n\n    def clear(self):\n        self.data = self.cont_x = None\n        self.cells = self.member_data = None\n        self.attr_color = None\n        self.colors = self.thresholds = self.bin_labels = None\n        if self.elements is not None:\n            self.scene.removeItem(self.elements)\n            self.elements = None\n        self.clear_selection()\n        self.controls.attr_color.model().set_domain(None)\n        self.Warning.clear()\n        self.Error.clear()\n\n    def recompute_dimensions(self):\n        if not self.auto_dimension or self.cont_x is None:\n            return\n        dim = max(5, int(np.ceil(np.sqrt(5 * np.sqrt(self.cont_x.shape[0])))))\n        self.opt_controls.spin_x.setValue(dim)\n        self.opt_controls.spin_y.setValue(dim)\n\n    def on_auto_dimension_changed(self):\n        self.manual_box.setEnabled(not self.auto_dimension)\n        if self.auto_dimension:\n            self.recompute_dimensions()\n        else:\n            spin_x = self.opt_controls.spin_x\n            spin_y = self.opt_controls.spin_y\n            dimx = int(5 * np.round(spin_x.value() / 5))\n            dimy = int(5 * np.round(spin_y.value() / 5))\n            spin_x.setValue(dimx)\n            spin_y.setValue(dimy)\n\n    def on_attr_color_change(self):\n        self.controls.pie_charts.setEnabled(self.attr_color is not None)\n        self.set_color_bins()\n        self.create_legend()\n        self.rescale()\n        self._redraw()\n\n    def on_attr_size_change(self):\n        self._redraw()\n\n    def on_pie_chart_change(self):\n        self._redraw()\n\n    def clear_selection(self):\n        self.selection = None\n        self.redraw_selection()\n\n    def on_selection_change(self, selection, action=SomView.SelectionSet):\n        if self.data is None:  # clicks on empty canvas\n            return\n        if self.selection is None:\n            self.selection = np.zeros(self.grid_cells.T.shape, dtype=np.int16)\n        if action == SomView.SelectionSet:\n            self.selection[:] = 0\n            self.selection[selection] = 1\n        elif action == SomView.SelectionAddToGroup:\n            self.selection[selection] = max(1, np.max(self.selection))\n        elif action == SomView.SelectionNewGroup:\n            self.selection[selection] = 1 + np.max(self.selection)\n        elif action & SomView.SelectionRemove:\n            self.selection[selection] = 0\n        self.redraw_selection()\n        self.update_output()\n\n    def on_selection_move(self, event: QKeyEvent):\n        if self.selection is None or not np.any(self.selection):\n            if event.key() in (Qt.Key_Right, Qt.Key_Down):\n                x = y = 0\n            else:\n                x = self.size_x - 1\n                y = self.size_y - 1\n        else:\n            x, y = np.nonzero(self.selection)\n            if len(x) > 1:\n                return\n            if event.key() == Qt.Key_Up and y > 0:\n                y -= 1\n            if event.key() == Qt.Key_Down and y < self.size_y - 1:\n                y += 1\n            if event.key() == Qt.Key_Left and x:\n                x -= 1\n            if event.key() == Qt.Key_Right and x < self.size_x - 1:\n                x += 1\n            x -= self.hexagonal and x == self.size_x - 1 and y % 2\n\n        if self.selection is not None and self.selection[x, y]:\n            return\n        selection = np.zeros(self.grid_cells.shape, dtype=bool)\n        selection[x, y] = True\n        self.on_selection_change(selection)\n\n    def on_selection_mark_change(self, marks):\n        self.redraw_selection(marks=marks)\n\n    def redraw_selection(self, marks=None):\n        if self.grid_cells is None:\n            return\n\n        sel_pen = QPen(QBrush(QColor(128, 128, 128)), 2)\n        sel_pen.setCosmetic(True)\n        mark_pen = QPen(QBrush(QColor(128, 128, 128)), 4)\n        mark_pen.setCosmetic(True)\n        pens = [self._grid_pen, sel_pen]\n\n        mark_brush = QBrush(QColor(224, 255, 255))\n        sels = self.selection is not None and np.max(self.selection)\n        palette = LimitedDiscretePalette(number_of_colors=sels + 1)\n        brushes = [QBrush(Qt.NoBrush)] + \\\n                  [QBrush(palette[i].lighter(165)) for i in range(sels)]\n\n        for y in range(self.size_y):\n            for x in range(self.size_x - (y % 2) * self.hexagonal):\n                cell = self.grid_cells[y, x]\n                marked = marks is not None and marks[x, y]\n                sel_group = self.selection is not None and self.selection[x, y]\n                if marked:\n                    cell.setBrush(mark_brush)\n                    cell.setPen(mark_pen)\n                else:\n                    cell.setBrush(brushes[sel_group])\n                    cell.setPen(pens[bool(sel_group)])\n                cell.setZValue(marked or sel_group)\n\n    def restart_som_pressed(self):\n        if self._optimizer_thread is not None:\n            self.stop_optimization = True\n            self._optimizer.stop_optimization = True\n        else:\n            self.start_som()\n\n    def start_som(self):\n        self.read_controls()\n        self.update_layout()\n        self.clear_selection()\n        if self.cont_x is not None:\n            self.enable_controls(False)\n            self._recompute_som()\n        else:\n            self.update_output()\n\n    def read_controls(self):\n        c = self.opt_controls\n        self.hexagonal = c.shape.currentIndex() == 0\n        self.size_x = c.spin_x.value()\n        self.size_y = c.spin_y.value()\n\n    def enable_controls(self, enable):\n        c = self.opt_controls\n        c.shape.setEnabled(enable)\n        c.auto_dim.setEnabled(enable)\n        c.start.setText(\"Start\" if enable else \"Stop\")\n\n    def update_layout(self):\n        self.set_legend_pos()\n        if self.elements:  # Prevent having redrawn grid but with old elements\n            self.scene.removeItem(self.elements)\n            self.elements = None\n        self.redraw_grid()\n        self.rescale()\n\n    def _redraw(self):\n        self.Warning.missing_colors.clear()\n        if self.elements:\n            self.scene.removeItem(self.elements)\n            self.elements = None\n        self.view.set_dimensions(self.size_x, self.size_y, self.hexagonal)\n\n        if self.cells is None:\n            return\n        sizes = self.cells[:, :, 1] - self.cells[:, :, 0]\n        sizes = sizes.astype(float)\n        if not self.size_by_instances:\n            sizes[sizes != 0] = 0.8\n        else:\n            sizes *= 0.8 / np.max(sizes)\n\n        self.elements = QGraphicsItemGroup()\n        self.scene.addItem(self.elements)\n        if self.attr_color is None:\n            self._draw_same_color(sizes)\n        elif self.pie_charts:\n            self._draw_pie_charts(sizes)\n        else:\n            self._draw_colored_circles(sizes)\n\n    @property\n    def _grid_factors(self):\n        return (0.5, sqrt3_2) if self.hexagonal else (0, 1)\n\n    def _draw_same_color(self, sizes):\n        fx, fy = self._grid_factors\n        color = QColor(64, 64, 64)\n        for y in range(self.size_y):\n            for x in range(self.size_x - self.hexagonal * (y % 2)):\n                r = sizes[x, y]\n                n = len(self.get_member_indices(x, y))\n                if not r:\n                    continue\n                ellipse = ColoredCircle(r / 2, color, 0)\n                ellipse.setPos(x + (y % 2) * fx, y * fy)\n                ellipse.setToolTip(f\"{n} instances\")\n                self.elements.addToGroup(ellipse)\n\n    def _get_color_column(self):\n        color_column = \\\n            self.data.get_column_view(self.attr_color)[0].astype(float,\n                                                                 copy=False)\n        if self.attr_color.is_discrete:\n            with np.errstate(invalid=\"ignore\"):\n                int_col = color_column.astype(int)\n            int_col[np.isnan(color_column)] = len(self.colors)\n        else:\n            int_col = np.zeros(len(color_column), dtype=int)\n            # The following line is unnecessary because rows with missing\n            # numeric data are excluded. Uncomment it if you change SOM to\n            # tolerate missing values.\n            # int_col[np.isnan(color_column)] = len(self.colors)\n            for i, thresh in enumerate(self.thresholds, start=1):\n                int_col[color_column >= thresh] = i\n        return int_col\n\n    def _tooltip(self, colors, distribution):\n        if self.attr_color.is_discrete:\n            values = self.attr_color.values\n        else:\n            values = self._bin_names()\n        tot = np.sum(distribution)\n        nbhp = \"\\N{NON-BREAKING HYPHEN}\"\n        return '<table style=\"white-space: nowrap\">' + \"\".join(f\"\"\"\n            <tr>\n                <td>\n                    <font color={color.name()}>\u25a0</font>\n                    <b>{escape(val).replace(\"-\", nbhp)}</b>:\n                </td>\n                <td>\n                    {n} ({n / tot * 100:.1f}&nbsp;%)\n                </td>\n            </tr>\n            \"\"\" for color, val, n in zip(colors, values, distribution) if n) \\\n            + \"</table>\"\n\n    def _draw_pie_charts(self, sizes):\n        fx, fy = self._grid_factors\n        color_column = self._get_color_column()\n        colors = self.colors.qcolors_w_nan\n        for y in range(self.size_y):\n            for x in range(self.size_x - self.hexagonal * (y % 2)):\n                r = sizes[x, y]\n                if not r:\n                    self.grid_cells[y, x].setToolTip(\"\")\n                    continue\n                members = self.get_member_indices(x, y)\n                color_dist = np.bincount(color_column[members],\n                                         minlength=len(colors))\n                rel_color_dist = color_dist.astype(float) / len(members)\n                pie = PieChart(rel_color_dist, r / 2, colors)\n                pie.setToolTip(self._tooltip(colors, color_dist))\n                self.elements.addToGroup(pie)\n                pie.setPos(x + (y % 2) * fx, y * fy)\n\n    def _draw_colored_circles(self, sizes):\n        fx, fy = self._grid_factors\n        color_column = self._get_color_column()\n        qcolors = self.colors.qcolors_w_nan\n        for y in range(self.size_y):\n            for x in range(self.size_x - self.hexagonal * (y % 2)):\n                r = sizes[x, y]\n                if not r:\n                    continue\n                members = self.get_member_indices(x, y)\n                color_dist = color_column[members]\n                color_dist = color_dist[color_dist < len(self.colors)]\n                if len(color_dist) != len(members):\n                    self.Warning.missing_colors(self.attr_color.name)\n                bc = np.bincount(color_dist, minlength=len(self.colors))\n                color = qcolors[np.argmax(bc)]\n                ellipse = ColoredCircle(r / 2, color, np.max(bc) / len(members))\n                ellipse.setPos(x + (y % 2) * fx, y * fy)\n                ellipse.setToolTip(self._tooltip(qcolors, bc))\n                self.elements.addToGroup(ellipse)\n\n    def redraw_grid(self):\n        if self.grid is not None:\n            self.scene.removeItem(self.grid)\n        self.grid = QGraphicsItemGroup()\n        self.grid.setZValue(-200)\n        self.grid_cells = np.full((self.size_y, self.size_x), None)\n        for y in range(self.size_y):\n            for x in range(self.size_x - (y % 2) * self.hexagonal):\n                if self.hexagonal:\n                    cell = QGraphicsPathItem(_hexagon_path)\n                    cell.setPos(x + (y % 2) / 2, y * sqrt3_2)\n                else:\n                    cell = QGraphicsRectItem(x - 0.5, y - 0.5, 1, 1)\n                self.grid_cells[y, x] = cell\n                cell.setPen(self._grid_pen)\n                self.grid.addToGroup(cell)\n        self.scene.addItem(self.grid)\n\n    def get_member_indices(self, x, y):\n        i, j = self.cells[x, y]\n        return self.member_data[i:j]\n\n    def _recompute_som(self):\n        if self.cont_x is None:\n            return\n\n        som = SOM(\n            self.size_x, self.size_y,\n            hexagonal=self.hexagonal,\n            pca_init=self.initialization == 0,\n            random_seed=0 if self.initialization == 2 else None\n        )\n\n        class Optimizer(QObject):\n            update = Signal(float, np.ndarray, np.ndarray)\n            done = Signal(SOM)\n            stopped = Signal()\n            stop_optimization = False\n\n            def __init__(self, data, som):\n                super().__init__()\n                self.som = som\n                self.data = data\n\n            def callback(self, progress):\n                self.update.emit(\n                    progress,\n                    self.som.weights.copy(), self.som.ssum_weights.copy())\n                return not self.stop_optimization\n\n            def run(self):\n                try:\n                    self.som.fit(self.data, N_ITERATIONS,\n                                 callback=self.callback)\n                    # Report an exception, but still remove the thread\n                finally:\n                    self.done.emit(self.som)\n                    self.stopped.emit()\n\n        def thread_finished():\n            self._optimizer = None\n            self._optimizer_thread = None\n\n        self.progressBarInit()\n\n        self._optimizer = Optimizer(self.cont_x, som)\n        self._optimizer_thread = QThread()\n        self._optimizer_thread.setStackSize(5 * 2 ** 20)\n        self._optimizer.update.connect(self.__update)\n        self._optimizer.done.connect(self.__done)\n        self._optimizer.stopped.connect(self._optimizer_thread.quit)\n        self._optimizer.moveToThread(self._optimizer_thread)\n        self._optimizer_thread.started.connect(self._optimizer.run)\n        self._optimizer_thread.finished.connect(thread_finished)\n        self.stop_optimization = False\n        self._optimizer_thread.start()\n\n    @Slot(float, object, object)\n    def __update(self, _progress, weights, ssum_weights):\n        self.progressBarSet(_progress)\n        self._assign_instances(weights, ssum_weights)\n        self._redraw()\n\n    @Slot(object)\n    def __done(self, som):\n        self.enable_controls(True)\n        self.progressBarFinished()\n        self._assign_instances(som.weights, som.ssum_weights)\n        self._redraw()\n        # This is the first time we know what was selected (assuming that\n        # initialization is not set to random)\n        if self.__pending_selection is not None:\n            self.on_selection_change(self.__pending_selection)\n            self.__pending_selection = None\n        self.update_output()\n\n    def stop_optimization_and_wait(self):\n        if self._optimizer_thread is not None:\n            self.stop_optimization = True\n            self._optimizer.stop_optimization = True\n            self._optimizer_thread.quit()\n            self._optimizer_thread.wait()\n            self._optimizer_thread = None\n\n    def onDeleteWidget(self):\n        self.stop_optimization_and_wait()\n        self.clear()\n        super().onDeleteWidget()\n\n    def _assign_instances(self, weights, ssum_weights):\n        if self.cont_x is None:\n            return  # the widget is shutting down while signals still processed\n        assignments = SOM.winner_from_weights(\n            self.cont_x, weights, ssum_weights, self.hexagonal)\n        members = defaultdict(list)\n        for i, (x, y) in enumerate(assignments):\n            members[(x, y)].append(i)\n        members.pop(None, None)\n        self.cells = np.empty((self.size_x, self.size_y, 2), dtype=int)\n        self.member_data = np.empty(self.cont_x.shape[0], dtype=int)\n        index = 0\n        for x in range(self.size_x):\n            for y in range(self.size_y):\n                nmembers = len(members[(x, y)])\n                self.member_data[index:index + nmembers] = members[(x, y)]\n                self.cells[x, y] = [index, index + nmembers]\n                index += nmembers\n\n    def resizeEvent(self, event):\n        super().resizeEvent(event)\n        self.create_legend()  # re-wrap lines if necessary\n        self.rescale()\n\n    def rescale(self):\n        if self.legend:\n            leg_height = self.legend.boundingRect().height()\n            leg_extra = 1.5\n        else:\n            leg_height = 0\n            leg_extra = 1\n\n        vw, vh = self.view.width(), self.view.height() - leg_height\n        scale = min(vw / (self.size_x + 1),\n                    vh / ((self.size_y + leg_extra) * self._grid_factors[1]))\n        self.view.setTransform(QTransform.fromScale(scale, scale))\n        if self.hexagonal:\n            self.view.setSceneRect(\n                0, -1, self.size_x - 1,\n                (self.size_y + leg_extra) * sqrt3_2 + leg_height / scale)\n        else:\n            self.view.setSceneRect(\n                -0.25, -0.25, self.size_x - 0.5,\n                self.size_y - 0.5 + leg_height / scale)\n\n    def update_output(self):\n        if self.data is None:\n            self.Outputs.selected_data.send(None)\n            self.Outputs.annotated_data.send(None)\n            return\n\n        indices = np.zeros(len(self.data), dtype=int)\n        if self.selection is not None and np.any(self.selection):\n            for y in range(self.size_y):\n                for x in range(self.size_x):\n                    rows = self.get_member_indices(x, y)\n                    indices[rows] = self.selection[x, y]\n\n        if np.any(indices):\n            sel_data = create_groups_table(self.data, indices, False, \"Group\")\n            self.Outputs.selected_data.send(sel_data)\n        else:\n            self.Outputs.selected_data.send(None)\n\n        if np.max(indices) > 1:\n            annotated = create_groups_table(self.data, indices)\n        else:\n            annotated = create_annotated_table(\n                self.data, np.flatnonzero(indices))\n        self.Outputs.annotated_data.send(annotated)\n\n    def set_color_bins(self):\n        if self.attr_color is None:\n            self.thresholds = self.bin_labels = self.colors = None\n        elif self.attr_color.is_discrete:\n            self.thresholds = self.bin_labels = None\n            self.colors = self.attr_color.palette\n        else:\n            col = self.data.get_column_view(self.attr_color)[0].astype(float)\n            if self.attr_color.is_time:\n                binning = time_binnings(col, min_bins=4)[-1]\n            else:\n                binning = decimal_binnings(col, min_bins=4)[-1]\n            self.thresholds = binning.thresholds[1:-1]\n            self.bin_labels = (binning.labels[1:-1], binning.short_labels[1:-1])\n            palette = BinnedContinuousPalette.from_palette(\n                self.attr_color.palette, binning.thresholds)\n            self.colors = palette\n\n    def create_legend(self):\n        if self.legend is not None:\n            self.scene.removeItem(self.legend)\n            self.legend = None\n        if self.attr_color is None:\n            return\n\n        if self.attr_color.is_discrete:\n            names = self.attr_color.values\n        else:\n            names = self._bin_names()\n\n        items = []\n        size = 8\n        for name, color in zip(names, self.colors.qcolors):\n            item = QGraphicsItemGroup()\n            item.addToGroup(\n                CanvasRectangle(None, -size / 2, -size / 2, size, size,\n                                Qt.gray, color))\n            item.addToGroup(CanvasText(None, name, size, 0, Qt.AlignVCenter))\n            items.append(item)\n\n        self.legend = wrap_legend_items(\n            items, hspacing=20, vspacing=16 + size,\n            max_width=self.view.width() - 25)\n        self.legend.setFlags(self.legend.ItemIgnoresTransformations)\n        self.legend.setTransform(\n            QTransform.fromTranslate(-self.legend.boundingRect().width() / 2,\n                                     0))\n        self.scene.addItem(self.legend)\n        self.set_legend_pos()\n\n    def _bin_names(self):\n        labels, short_labels = self.bin_labels\n        return \\\n            [f\"< {labels[0]}\"] \\\n            + [f\"{x} - {y}\" for x, y in zip(labels, short_labels[1:])] \\\n            + [f\"\u2265 {labels[-1]}\"]\n\n    def set_legend_pos(self):\n        if self.legend is None:\n            return\n        self.legend.setPos(\n            self.size_x / 2,\n            (self.size_y + 0.2 + 0.3 * self.hexagonal) * self._grid_factors[1])\n\n    def send_report(self):\n        self.report_plot()\n        if self.attr_color:\n            self.report_caption(\n                f\"Self-organizing map colored by '{self.attr_color.name}'\")\n\n\ndef _draw_hexagon():\n    path = QPainterPath()\n    s = 0.5 / (np.sqrt(3) / 2)\n    path.moveTo(-0.5, -s / 2)\n    path.lineTo(-0.5, s / 2)\n    path.lineTo(0, s)\n    path.lineTo(0.5, s / 2)\n    path.lineTo(0.5, -s / 2)\n    path.lineTo(0, -s)\n    path.lineTo(-0.5, -s / 2)\n    return path\n\n\n_hexagon_path = _draw_hexagon()\n\n\nif __name__ == \"__main__\":  # pragma: no cover\n    WidgetPreview(OWSOM).run(Table(\"iris\"))\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:** SOM widget recomputes unnecessarily when input data remains unchanged

**Description:**
When using the Self-Organizing Map (SOM) widget, providing the same dataset multiple times triggers a recomputation of the SOM each time, even though the data hasn't effectively changed. This leads to unnecessary processing and can degrade performance, especially with large datasets.

**Example Code:**
```python
heart = Table("heart_disease")
widget.recompute_som = Mock()

# Initial data input - should trigger recompute
widget.send_signal(widget.Inputs.data, heart)
widget.recompute_som.assert_called_once()

# Sending the same data again - should not trigger recompute
widget.recompute_som.reset_mock()
widget.send_signal(widget.Inputs.data, heart)
widget.recompute_som.assert_not_called()
```

**Expected Behavior:**
The SOM should only recompute when there are actual changes in the input data. Sending the same dataset a second time should not initiate another recomputation.

**Actual Behavior:**
Even when the input data remains unchanged, the SOM widget triggers a recomputation each time the data is sent, leading to redundant processing.

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

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

--- BEGIN FILE ---
```
### Orange/widgets/unsupervised/owsom.py
from collections import defaultdict, namedtuple
from xml.sax.saxutils import escape

import numpy as np
import scipy.sparse as sp

from AnyQt.QtCore import Qt, QRectF, pyqtSignal as Signal, QObject, QThread, \
    pyqtSlot as Slot
from AnyQt.QtGui import QTransform, QPen, QBrush, QColor, QPainter, \
    QPainterPath, QKeyEvent
from AnyQt.QtWidgets import \
    QGraphicsView, QGraphicsScene, \
    QGraphicsItem, QGraphicsRectItem, QGraphicsItemGroup, QSizePolicy, \
    QGraphicsPathItem

from Orange.data import Table, Domain
from Orange.preprocess import decimal_binnings, time_binnings
from Orange.projection.som import SOM

from Orange.widgets import gui
from Orange.widgets.widget import OWWidget, Msg, Input, Output
from Orange.widgets.settings import \
    DomainContextHandler, ContextSetting, Setting
from Orange.widgets.utils.itemmodels import DomainModel
from Orange.widgets.utils.widgetpreview import WidgetPreview
from Orange.widgets.utils.annotated_data import \
    create_annotated_table, create_groups_table, ANNOTATED_DATA_SIGNAL_NAME
from Orange.widgets.utils.colorpalettes import \
    BinnedContinuousPalette, LimitedDiscretePalette
from Orange.widgets.visualize.utils import CanvasRectangle, CanvasText
from Orange.widgets.visualize.utils.plotutils import wrap_legend_items


sqrt3_2 = np.sqrt(3) / 2


class SomView(QGraphicsView):
    SelectionSet, SelectionNewGroup, SelectionAddToGroup, SelectionRemove \
        = range(4)
    selection_changed = Signal(np.ndarray, int)
    selection_moved = Signal(QKeyEvent)
    selection_mark_changed = Signal(np.ndarray)

    def __init__(self, scene):
        super().__init__(scene)
        self.__button_down_pos = None
        self.size_x = self.size_y = 1
        self.hexagonal = True

    def set_dimensions(self, size_x, size_y, hexagonal):
        self.size_x = size_x
        self.size_y = size_y
        self.hexagonal = hexagonal

    def _get_marked_cells(self, event):
        x0, y0 = self.__button_down_pos.x(), self.__button_down_pos.y()
        pos = self.mapToScene(event.pos())
        x1, y1 = pos.x(), pos.y()
        x0, x1 = sorted((x0, x1))
        y0, y1 = sorted((y0, y1))
        selection = np.zeros((self.size_x, self.size_y), dtype=bool)
        if self.hexagonal:
            y0 = max(0, int(y0 / sqrt3_2 + 0.5))
            y1 = min(self.size_y, int(np.ceil(y1 / sqrt3_2 + 0.5)))
            for y in range(y0, y1):
                if x1 < 0:
                    continue
                x0_ = max(0, int(x0 + 0.5 - (y % 2) / 2))
                x1_ = min(self.size_x - y % 2,
                          int(np.ceil(x1 + 0.5 - (y % 2) / 2)))
                selection[x0_:x1_, y] = True
        elif not(x1 < -0.5 or x0 > self.size_x - 0.5
                 or y1 < -0.5 or y0 > self.size_y - 0.5):

            def roundclip(z, zmax):
                return int(np.clip(np.round(z), 0, zmax - 1))\

            x0 = roundclip(x0, self.size_x)
            y0 = roundclip(y0, self.size_y)
            x1 = roundclip(x1, self.size_x)
            y1 = roundclip(y1, self.size_y)
            selection[x0:x1 + 1, y0:y1 + 1] = True

        return selection

    def mousePressEvent(self, event):
        if event.button() != Qt.LeftButton:
            return

        self.__button_down_pos = self.mapToScene(event.pos())
        event.accept()

    def mouseMoveEvent(self, event):
        if event.buttons() != Qt.LeftButton:
            return

        self.selection_mark_changed.emit(self._get_marked_cells(event))
        event.accept()

    def mouseReleaseEvent(self, event):
        if event.button() != Qt.LeftButton:
            return

        if event.modifiers() & Qt.ShiftModifier:
            if event.modifiers() & Qt.ControlModifier:
                action = self.SelectionAddToGroup
            else:
                action = self.SelectionNewGroup
        elif event.modifiers() & Qt.AltModifier:
            action = self.SelectionRemove
        else:
            action = self.SelectionSet
        selection = self._get_marked_cells(event)
        self.selection_changed.emit(selection, action)
        event.accept()

    def keyPressEvent(self, event: QKeyEvent):
        if event.key() in (Qt.Key_Up, Qt.Key_Down, Qt.Key_Left, Qt.Key_Right):
            self.selection_moved.emit(event)
        else:
            super().keyPressEvent(event)


class PieChart(QGraphicsItem):
    def __init__(self, dist, r, colors):
        super().__init__()
        self.dist = dist
        self.r = r
        self.colors = colors

    def boundingRect(self):
        return QRectF(- self.r, - self.r, 2 * self.r, 2 * self.r)

    def paint(self, painter, _option, _index):
        painter.save()
        start_angle = 0
        pen = QPen(QBrush(Qt.black), 2)
        pen.setCosmetic(True)
        painter.setPen(pen)
        painter.drawEllipse(self.boundingRect())
        for angle, color in zip(self.dist * 16 * 360, self.colors):
            if angle == 0:
                continue
            painter.setBrush(QBrush(color))
            painter.setPen(Qt.NoPen)
            painter.drawPie(self.boundingRect(), int(start_angle), int(angle))
            start_angle += angle
        painter.restore()


class ColoredCircle(QGraphicsItem):
    def __init__(self, r, color, proportion):
        super().__init__()
        self.r = r
        self.color = color
        self.proportion = proportion

    def boundingRect(self):
        return QRectF(-self.r, -self.r, 2 * self.r, 2 * self.r)

    def paint(self, painter, _option, _index):
        painter.save()
        pen = QPen(QBrush(self.color), 2)
        pen.setCosmetic(True)
        painter.setPen(pen)
        painter.setBrush(QBrush(self.color.lighter(int(200 - 80 * self.proportion))))
        painter.drawEllipse(self.boundingRect())
        painter.restore()


N_ITERATIONS = 200


class OWSOM(OWWidget):
    name = "Self-Organizing Map"
    description = "Computation of self-organizing map."
    icon = "icons/SOM.svg"
    keywords = ["SOM"]

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

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

    settingsHandler = DomainContextHandler()
    auto_dimension = Setting(True)
    size_x = Setting(10)
    size_y = Setting(10)
    hexagonal = Setting(1)
    initialization = Setting(0)

    attr_color = ContextSetting(None)
    size_by_instances = Setting(True)
    pie_charts = Setting(False)
    selection = Setting(None, schema_only=True)

    graph_name = "view"

    _grid_pen = QPen(QBrush(QColor(224, 224, 224)), 2)
    _grid_pen.setCosmetic(True)

    OptControls = namedtuple(
        "OptControls",
        ("shape", "auto_dim", "spin_x", "spin_y", "initialization", "start")
    )

    class Warning(OWWidget.Warning):
        ignoring_disc_variables = Msg("SOM ignores categorical variables.")
        missing_colors = \
            Msg("Some data instances have undefined value of '{}'.")
        missing_values = \
            Msg("{} data instance{} with undefined value(s) {} not shown.")
        single_attribute = Msg("Data contains a single numeric column.")

    class Error(OWWidget.Error):
        no_numeric_variables = Msg("Data contains no numeric columns.")
        no_defined_rows = Msg("All rows contain at least one undefined value.")

    def __init__(self):
        super().__init__()
        self.__pending_selection = self.selection
        self._optimizer = None
        self._optimizer_thread = None
        self.stop_optimization = False

        self.data = self.cont_x = None
        self.cells = self.member_data = None
        self.selection = None
        self.colors = self.thresholds = self.bin_labels = None

        box = gui.vBox(self.controlArea, box="SOM")
        shape = gui.comboBox(
            box, self, "", items=("Hexagonal grid", "Square grid"))
        shape.setCurrentIndex(1 - self.hexagonal)

        box2 = gui.indentedBox(box, 10)
        auto_dim = gui.checkBox(
            box2, self, "auto_dimension", "Set dimensions automatically",
            callback=self.on_auto_dimension_changed)
        self.manual_box = box3 = gui.hBox(box2)
        spinargs = dict(
            value="", widget=box3, master=self, minv=5, maxv=100, step=5,
            alignment=Qt.AlignRight)
        spin_x = gui.spin(**spinargs)
        spin_x.setValue(self.size_x)
        gui.widgetLabel(box3, "×")
        spin_y = gui.spin(**spinargs)
        spin_y.setValue(self.size_y)
        gui.rubber(box3)
        self.manual_box.setEnabled(not self.auto_dimension)

        initialization = gui.comboBox(
            box, self, "initialization",
            items=("Initialize with PCA", "Random initialization",
                   "Replicable random"))

        start = gui.button(
            box, self, "Restart", callback=self.restart_som_pressed,
            sizePolicy=(QSizePolicy.MinimumExpanding, QSizePolicy.Fixed))

        self.opt_controls = self.OptControls(
            shape, auto_dim, spin_x, spin_y, initialization, start)

        box = gui.vBox(self.controlArea, "Color")
        gui.comboBox(
            box, self, "attr_color", searchable=True,
            callback=self.on_attr_color_change,
            model=DomainModel(placeholder="(Same color)",
                              valid_types=DomainModel.PRIMITIVE))
        gui.checkBox(
            box, self, "pie_charts", label="Show pie charts",
            callback=self.on_pie_chart_change)
        gui.checkBox(
            box, self, "size_by_instances", label="Size by number of instances",
            callback=self.on_attr_size_change)

        gui.rubber(self.controlArea)

        self.scene = QGraphicsScene(self)

        self.view = SomView(self.scene)
        self.view.setMinimumWidth(400)
        self.view.setMinimumHeight(400)
        self.view.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
        self.view.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
        self.view.setRenderHint(QPainter.Antialiasing)
        self.view.selection_changed.connect(self.on_selection_change)
        self.view.selection_moved.connect(self.on_selection_move)
        self.view.selection_mark_changed.connect(self.on_selection_mark_change)
        self.mainArea.layout().addWidget(self.view)

        self.elements = None
        self.grid = None
        self.grid_cells = None
        self.legend = None

    @Inputs.data
    def set_data(self, data):
        def prepare_data():
            if len(cont_attrs) < len(attrs):
                self.Warning.ignoring_disc_variables()
            if len(cont_attrs) == 1:
                self.Warning.single_attribute()
            x = Table.from_table(Domain(cont_attrs), data).X
            if sp.issparse(x):
                self.data = data
                self.cont_x = x.tocsr()
            else:
                mask = np.all(np.isfinite(x), axis=1)
                if not np.any(mask):
                    self.Error.no_defined_rows()
                else:
                    if np.all(mask):
                        self.data = data
                        self.cont_x = x.copy()
                    else:
                        self.data = data[mask]
                        self.cont_x = x[mask]
                    self.cont_x -= np.min(self.cont_x, axis=0)[None, :]
                    sums = np.sum(self.cont_x, axis=0)[None, :]
                    sums[sums == 0] = 1
                    self.cont_x /= sums

        def set_warnings():
            missing = len(data) - len(self.data)
            if missing == 1:
                self.Warning.missing_values(1, "", "is")
            elif missing > 1:
                self.Warning.missing_values(missing, "s", "are")

        self.stop_optimization_and_wait()

        self.closeContext()
        self.clear()
        self.Error.clear()
        self.Warning.clear()

        if data is not None:
            attrs = data.domain.attributes
            cont_attrs = [var for var in attrs if var.is_continuous]
            if not cont_attrs:
                self.Error.no_numeric_variables()
            else:
                prepare_data()

        if self.data is not None:
            self.controls.attr_color.model().set_domain(data.domain)
            self.attr_color = data.domain.class_var
            set_warnings()

        self.openContext(self.data)
        self.set_color_bins()
        self.create_legend()
        self.recompute_dimensions()
        self.start_som()

    def clear(self):
        self.data = self.cont_x = None
        self.cells = self.member_data = None
        self.attr_color = None
        self.colors = self.thresholds = self.bin_labels = None
        if self.elements is not None:
            self.scene.removeItem(self.elements)
            self.elements = None
        self.clear_selection()
        self.controls.attr_color.model().set_domain(None)
        self.Warning.clear()
        self.Error.clear()

    def recompute_dimensions(self):
        if not self.auto_dimension or self.cont_x is None:
            return
        dim = max(5, int(np.ceil(np.sqrt(5 * np.sqrt(self.cont_x.shape[0])))))
        self.opt_controls.spin_x.setValue(dim)
        self.opt_controls.spin_y.setValue(dim)

    def on_auto_dimension_changed(self):
        self.manual_box.setEnabled(not self.auto_dimension)
        if self.auto_dimension:
            self.recompute_dimensions()
        else:
            spin_x = self.opt_controls.spin_x
            spin_y = self.opt_controls.spin_y
            dimx = int(5 * np.round(spin_x.value() / 5))
            dimy = int(5 * np.round(spin_y.value() / 5))
            spin_x.setValue(dimx)
            spin_y.setValue(dimy)

    def on_attr_color_change(self):
        self.controls.pie_charts.setEnabled(self.attr_color is not None)
        self.set_color_bins()
        self.create_legend()
        self.rescale()
        self._redraw()

    def on_attr_size_change(self):
        self._redraw()

    def on_pie_chart_change(self):
        self._redraw()

    def clear_selection(self):
        self.selection = None
        self.redraw_selection()

    def on_selection_change(self, selection, action=SomView.SelectionSet):
        if self.data is None:  # clicks on empty canvas
            return
        if self.selection is None:
            self.selection = np.zeros(self.grid_cells.T.shape, dtype=np.int16)
        if action == SomView.SelectionSet:
            self.selection[:] = 0
            self.selection[selection] = 1
        elif action == SomView.SelectionAddToGroup:
            self.selection[selection] = max(1, np.max(self.selection))
        elif action == SomView.SelectionNewGroup:
            self.selection[selection] = 1 + np.max(self.selection)
        elif action & SomView.SelectionRemove:
            self.selection[selection] = 0
        self.redraw_selection()
        self.update_output()

    def on_selection_move(self, event: QKeyEvent):
        if self.selection is None or not np.any(self.selection):
            if event.key() in (Qt.Key_Right, Qt.Key_Down):
                x = y = 0
            else:
                x = self.size_x - 1
                y = self.size_y - 1
        else:
            x, y = np.nonzero(self.selection)
            if len(x) > 1:
                return
            if event.key() == Qt.Key_Up and y > 0:
                y -= 1
            if event.key() == Qt.Key_Down and y < self.size_y - 1:
                y += 1
            if event.key() == Qt.Key_Left and x:
                x -= 1
            if event.key() == Qt.Key_Right and x < self.size_x - 1:
                x += 1
            x -= self.hexagonal and x == self.size_x - 1 and y % 2

        if self.selection is not None and self.selection[x, y]:
            return
        selection = np.zeros(self.grid_cells.shape, dtype=bool)
        selection[x, y] = True
        self.on_selection_change(selection)

    def on_selection_mark_change(self, marks):
        self.redraw_selection(marks=marks)

    def redraw_selection(self, marks=None):
        if self.grid_cells is None:
            return

        sel_pen = QPen(QBrush(QColor(128, 128, 128)), 2)
        sel_pen.setCosmetic(True)
        mark_pen = QPen(QBrush(QColor(128, 128, 128)), 4)
        mark_pen.setCosmetic(True)
        pens = [self._grid_pen, sel_pen]

        mark_brush = QBrush(QColor(224, 255, 255))
        sels = self.selection is not None and np.max(self.selection)
        palette = LimitedDiscretePalette(number_of_colors=sels + 1)
        brushes = [QBrush(Qt.NoBrush)] + \
                  [QBrush(palette[i].lighter(165)) for i in range(sels)]

        for y in range(self.size_y):
            for x in range(self.size_x - (y % 2) * self.hexagonal):
                cell = self.grid_cells[y, x]
                marked = marks is not None and marks[x, y]
                sel_group = self.selection is not None and self.selection[x, y]
                if marked:
                    cell.setBrush(mark_brush)
                    cell.setPen(mark_pen)
                else:
                    cell.setBrush(brushes[sel_group])
                    cell.setPen(pens[bool(sel_group)])
                cell.setZValue(marked or sel_group)

    def restart_som_pressed(self):
        if self._optimizer_thread is not None:
            self.stop_optimization = True
            self._optimizer.stop_optimization = True
        else:
            self.start_som()

    def start_som(self):
        self.read_controls()
        self.update_layout()
        self.clear_selection()
        if self.cont_x is not None:
            self.enable_controls(False)
            self._recompute_som()
        else:
            self.update_output()

    def read_controls(self):
        c = self.opt_controls
        self.hexagonal = c.shape.currentIndex() == 0
        self.size_x = c.spin_x.value()
        self.size_y = c.spin_y.value()

    def enable_controls(self, enable):
        c = self.opt_controls
        c.shape.setEnabled(enable)
        c.auto_dim.setEnabled(enable)
        c.start.setText("Start" if enable else "Stop")

    def update_layout(self):
        self.set_legend_pos()
        if self.elements:  # Prevent having redrawn grid but with old elements
            self.scene.removeItem(self.elements)
            self.elements = None
        self.redraw_grid()
        self.rescale()

    def _redraw(self):
        self.Warning.missing_colors.clear()
        if self.elements:
            self.scene.removeItem(self.elements)
            self.elements = None
        self.view.set_dimensions(self.size_x, self.size_y, self.hexagonal)

        if self.cells is None:
            return
        sizes = self.cells[:, :, 1] - self.cells[:, :, 0]
        sizes = sizes.astype(float)
        if not self.size_by_instances:
            sizes[sizes != 0] = 0.8
        else:
            sizes *= 0.8 / np.max(sizes)

        self.elements = QGraphicsItemGroup()
        self.scene.addItem(self.elements)
        if self.attr_color is None:
            self._draw_same_color(sizes)
        elif self.pie_charts:
            self._draw_pie_charts(sizes)
        else:
            self._draw_colored_circles(sizes)

    @property
    def _grid_factors(self):
        return (0.5, sqrt3_2) if self.hexagonal else (0, 1)

    def _draw_same_color(self, sizes):
        fx, fy = self._grid_factors
        color = QColor(64, 64, 64)
        for y in range(self.size_y):
            for x in range(self.size_x - self.hexagonal * (y % 2)):
                r = sizes[x, y]
                n = len(self.get_member_indices(x, y))
                if not r:
                    continue
                ellipse = ColoredCircle(r / 2, color, 0)
                ellipse.setPos(x + (y % 2) * fx, y * fy)
                ellipse.setToolTip(f"{n} instances")
                self.elements.addToGroup(ellipse)

    def _get_color_column(self):
        color_column = \
            self.data.get_column_view(self.attr_color)[0].astype(float,
                                                                 copy=False)
        if self.attr_color.is_discrete:
            with np.errstate(invalid="ignore"):
                int_col = color_column.astype(int)
            int_col[np.isnan(color_column)] = len(self.colors)
        else:
            int_col = np.zeros(len(color_column), dtype=int)
            # The following line is unnecessary because rows with missing
            # numeric data are excluded. Uncomment it if you change SOM to
            # tolerate missing values.
            # int_col[np.isnan(color_column)] = len(self.colors)
            for i, thresh in enumerate(self.thresholds, start=1):
                int_col[color_column >= thresh] = i
        return int_col

    def _tooltip(self, colors, distribution):
        if self.attr_color.is_discrete:
            values = self.attr_color.values
        else:
            values = self._bin_names()
        tot = np.sum(distribution)
        nbhp = "\N{NON-BREAKING HYPHEN}"
        return '<table style="white-space: nowrap">' + "".join(f"""
            <tr>
                <td>
                    <font color={color.name()}>■</font>
                    <b>{escape(val).replace("-", nbhp)}</b>:
                </td>
                <td>
                    {n} ({n / tot * 100:.1f}&nbsp;%)
                </td>
            </tr>
            """ for color, val, n in zip(colors, values, distribution) if n) \
            + "</table>"

    def _draw_pie_charts(self, sizes):
        fx, fy = self._grid_factors
        color_column = self._get_color_column()
        colors = self.colors.qcolors_w_nan
        for y in range(self.size_y):
            for x in range(self.size_x - self.hexagonal * (y % 2)):
                r = sizes[x, y]
                if not r:
                    self.grid_cells[y, x].setToolTip("")
                    continue
                members = self.get_member_indices(x, y)
                color_dist = np.bincount(color_column[members],
                                         minlength=len(colors))
                rel_color_dist = color_dist.astype(float) / len(members)
                pie = PieChart(rel_color_dist, r / 2, colors)
                pie.setToolTip(self._tooltip(colors, color_dist))
                self.elements.addToGroup(pie)
                pie.setPos(x + (y % 2) * fx, y * fy)

    def _draw_colored_circles(self, sizes):
        fx, fy = self._grid_factors
        color_column = self._get_color_column()
        qcolors = self.colors.qcolors_w_nan
        for y in range(self.size_y):
            for x in range(self.size_x - self.hexagonal * (y % 2)):
                r = sizes[x, y]
                if not r:
                    continue
                members = self.get_member_indices(x, y)
                color_dist = color_column[members]
                color_dist = color_dist[color_dist < len(self.colors)]
                if len(color_dist) != len(members):
                    self.Warning.missing_colors(self.attr_color.name)
                bc = np.bincount(color_dist, minlength=len(self.colors))
                color = qcolors[np.argmax(bc)]
                ellipse = ColoredCircle(r / 2, color, np.max(bc) / len(members))
                ellipse.setPos(x + (y % 2) * fx, y * fy)
                ellipse.setToolTip(self._tooltip(qcolors, bc))
                self.elements.addToGroup(ellipse)

    def redraw_grid(self):
        if self.grid is not None:
            self.scene.removeItem(self.grid)
        self.grid = QGraphicsItemGroup()
        self.grid.setZValue(-200)
        self.grid_cells = np.full((self.size_y, self.size_x), None)
        for y in range(self.size_y):
            for x in range(self.size_x - (y % 2) * self.hexagonal):
                if self.hexagonal:
                    cell = QGraphicsPathItem(_hexagon_path)
                    cell.setPos(x + (y % 2) / 2, y * sqrt3_2)
                else:
                    cell = QGraphicsRectItem(x - 0.5, y - 0.5, 1, 1)
                self.grid_cells[y, x] = cell
                cell.setPen(self._grid_pen)
                self.grid.addToGroup(cell)
        self.scene.addItem(self.grid)

    def get_member_indices(self, x, y):
        i, j = self.cells[x, y]
        return self.member_data[i:j]

    def _recompute_som(self):
        if self.cont_x is None:
            return

        som = SOM(
            self.size_x, self.size_y,
            hexagonal=self.hexagonal,
            pca_init=self.initialization == 0,
            random_seed=0 if self.initialization == 2 else None
        )

        class Optimizer(QObject):
            update = Signal(float, np.ndarray, np.ndarray)
            done = Signal(SOM)
            stopped = Signal()
            stop_optimization = False

            def __init__(self, data, som):
                super().__init__()
                self.som = som
                self.data = data

            def callback(self, progress):
                self.update.emit(
                    progress,
                    self.som.weights.copy(), self.som.ssum_weights.copy())
                return not self.stop_optimization

            def run(self):
                try:
                    self.som.fit(self.data, N_ITERATIONS,
                                 callback=self.callback)
                    # Report an exception, but still remove the thread
                finally:
                    self.done.emit(self.som)
                    self.stopped.emit()

        def thread_finished():
            self._optimizer = None
            self._optimizer_thread = None

        self.progressBarInit()

        self._optimizer = Optimizer(self.cont_x, som)
        self._optimizer_thread = QThread()
        self._optimizer_thread.setStackSize(5 * 2 ** 20)
        self._optimizer.update.connect(self.__update)
        self._optimizer.done.connect(self.__done)
        self._optimizer.stopped.connect(self._optimizer_thread.quit)
        self._optimizer.moveToThread(self._optimizer_thread)
        self._optimizer_thread.started.connect(self._optimizer.run)
        self._optimizer_thread.finished.connect(thread_finished)
        self.stop_optimization = False
        self._optimizer_thread.start()

    @Slot(float, object, object)
    def __update(self, _progress, weights, ssum_weights):
        self.progressBarSet(_progress)
        self._assign_instances(weights, ssum_weights)
        self._redraw()

    @Slot(object)
    def __done(self, som):
        self.enable_controls(True)
        self.progressBarFinished()
        self._assign_instances(som.weights, som.ssum_weights)
        self._redraw()
        # This is the first time we know what was selected (assuming that
        # initialization is not set to random)
        if self.__pending_selection is not None:
            self.on_selection_change(self.__pending_selection)
            self.__pending_selection = None
        self.update_output()

    def stop_optimization_and_wait(self):
        if self._optimizer_thread is not None:
            self.stop_optimization = True
            self._optimizer.stop_optimization = True
            self._optimizer_thread.quit()
            self._optimizer_thread.wait()
            self._optimizer_thread = None

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

    def _assign_instances(self, weights, ssum_weights):
        if self.cont_x is None:
            return  # the widget is shutting down while signals still processed
        assignments = SOM.winner_from_weights(
            self.cont_x, weights, ssum_weights, self.hexagonal)
        members = defaultdict(list)
        for i, (x, y) in enumerate(assignments):
            members[(x, y)].append(i)
        members.pop(None, None)
        self.cells = np.empty((self.size_x, self.size_y, 2), dtype=int)
        self.member_data = np.empty(self.cont_x.shape[0], dtype=int)
        index = 0
        for x in range(self.size_x):
            for y in range(self.size_y):
                nmembers = len(members[(x, y)])
                self.member_data[index:index + nmembers] = members[(x, y)]
                self.cells[x, y] = [index, index + nmembers]
                index += nmembers

    def resizeEvent(self, event):
        super().resizeEvent(event)
        self.create_legend()  # re-wrap lines if necessary
        self.rescale()

    def rescale(self):
        if self.legend:
            leg_height = self.legend.boundingRect().height()
            leg_extra = 1.5
        else:
            leg_height = 0
            leg_extra = 1

        vw, vh = self.view.width(), self.view.height() - leg_height
        scale = min(vw / (self.size_x + 1),
                    vh / ((self.size_y + leg_extra) * self._grid_factors[1]))
        self.view.setTransform(QTransform.fromScale(scale, scale))
        if self.hexagonal:
            self.view.setSceneRect(
                0, -1, self.size_x - 1,
                (self.size_y + leg_extra) * sqrt3_2 + leg_height / scale)
        else:
            self.view.setSceneRect(
                -0.25, -0.25, self.size_x - 0.5,
                self.size_y - 0.5 + leg_height / scale)

    def update_output(self):
        if self.data is None:
            self.Outputs.selected_data.send(None)
            self.Outputs.annotated_data.send(None)
            return

        indices = np.zeros(len(self.data), dtype=int)
        if self.selection is not None and np.any(self.selection):
            for y in range(self.size_y):
                for x in range(self.size_x):
                    rows = self.get_member_indices(x, y)
                    indices[rows] = self.selection[x, y]

        if np.any(indices):
            sel_data = create_groups_table(self.data, indices, False, "Group")
            self.Outputs.selected_data.send(sel_data)
        else:
            self.Outputs.selected_data.send(None)

        if np.max(indices) > 1:
            annotated = create_groups_table(self.data, indices)
        else:
            annotated = create_annotated_table(
                self.data, np.flatnonzero(indices))
        self.Outputs.annotated_data.send(annotated)

    def set_color_bins(self):
        if self.attr_color is None:
            self.thresholds = self.bin_labels = self.colors = None
        elif self.attr_color.is_discrete:
            self.thresholds = self.bin_labels = None
            self.colors = self.attr_color.palette
        else:
            col = self.data.get_column_view(self.attr_color)[0].astype(float)
            if self.attr_color.is_time:
                binning = time_binnings(col, min_bins=4)[-1]
            else:
                binning = decimal_binnings(col, min_bins=4)[-1]
            self.thresholds = binning.thresholds[1:-1]
            self.bin_labels = (binning.labels[1:-1], binning.short_labels[1:-1])
            palette = BinnedContinuousPalette.from_palette(
                self.attr_color.palette, binning.thresholds)
            self.colors = palette

    def create_legend(self):
        if self.legend is not None:
            self.scene.removeItem(self.legend)
            self.legend = None
        if self.attr_color is None:
            return

        if self.attr_color.is_discrete:
            names = self.attr_color.values
        else:
            names = self._bin_names()

        items = []
        size = 8
        for name, color in zip(names, self.colors.qcolors):
            item = QGraphicsItemGroup()
            item.addToGroup(
                CanvasRectangle(None, -size / 2, -size / 2, size, size,
                                Qt.gray, color))
            item.addToGroup(CanvasText(None, name, size, 0, Qt.AlignVCenter))
            items.append(item)

        self.legend = wrap_legend_items(
            items, hspacing=20, vspacing=16 + size,
            max_width=self.view.width() - 25)
        self.legend.setFlags(self.legend.ItemIgnoresTransformations)
        self.legend.setTransform(
            QTransform.fromTranslate(-self.legend.boundingRect().width() / 2,
                                     0))
        self.scene.addItem(self.legend)
        self.set_legend_pos()

    def _bin_names(self):
        labels, short_labels = self.bin_labels
        return \
            [f"< {labels[0]}"] \
            + [f"{x} - {y}" for x, y in zip(labels, short_labels[1:])] \
            + [f"≥ {labels[-1]}"]

    def set_legend_pos(self):
        if self.legend is None:
            return
        self.legend.setPos(
            self.size_x / 2,
            (self.size_y + 0.2 + 0.3 * self.hexagonal) * self._grid_factors[1])

    def send_report(self):
        self.report_plot()
        if self.attr_color:
            self.report_caption(
                f"Self-organizing map colored by '{self.attr_color.name}'")


def _draw_hexagon():
    path = QPainterPath()
    s = 0.5 / (np.sqrt(3) / 2)
    path.moveTo(-0.5, -s / 2)
    path.lineTo(-0.5, s / 2)
    path.lineTo(0, s)
    path.lineTo(0.5, s / 2)
    path.lineTo(0.5, -s / 2)
    path.lineTo(0, -s)
    path.lineTo(-0.5, -s / 2)
    return path


_hexagon_path = _draw_hexagon()


if __name__ == "__main__":  # pragma: no cover
    WidgetPreview(OWSOM).run(Table("iris"))

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