{
    "file_diffs": [
        {
            "old_file_content": "import time\nimport warnings\nfrom itertools import chain\nfrom typing import List\nfrom functools import singledispatch\nfrom enum import IntEnum\nfrom collections import OrderedDict\n\nimport numpy as np\n\nfrom AnyQt.QtWidgets import (\n    QGraphicsView, QGraphicsScene, QGraphicsItem, QGraphicsSimpleTextItem,\n    QGraphicsTextItem, QGraphicsLineItem, QGraphicsWidget, QGraphicsRectItem,\n    QGraphicsEllipseItem, QGraphicsLinearLayout, QGridLayout, QLabel, QFrame,\n    QSizePolicy, QDesktopWidget,\n)\nfrom AnyQt.QtGui import QColor, QPainter, QFont, QPen, QBrush\nfrom AnyQt.QtCore import Qt, QRectF, QSize, QPropertyAnimation, QObject, \\\n    pyqtProperty\n\nfrom Orange.data import Table, Domain, DiscreteVariable, ContinuousVariable\nfrom Orange.statistics.util import nanmin, nanmax, nanmean, unique\nfrom Orange.classification import Model\nfrom Orange.classification.naive_bayes import NaiveBayesModel\nfrom Orange.classification.logistic_regression import \\\n    LogisticRegressionClassifier\nfrom Orange.widgets.settings import Setting, ContextSetting, \\\n    ClassValuesContextHandler\nfrom Orange.widgets.utils.widgetpreview import WidgetPreview\nfrom Orange.widgets.utils.state_summary import format_summary_details\nfrom Orange.widgets.widget import OWWidget, Msg, Input, Output, AttributeList\nfrom Orange.widgets import gui\n\n\ndef collides(item, items):\n    return any(item.collidesWithItem(i) for i in items)\n\n\nclass SortBy(IntEnum):\n    NO_SORTING, NAME, ABSOLUTE, POSITIVE, NEGATIVE = 0, 1, 2, 3, 4\n\n    @staticmethod\n    def items():\n        return [\"No sorting\", \"Name\", \"Absolute importance\",\n                \"Positive influence\", \"Negative influence\"]\n\n\nclass MovableToolTip(QLabel):\n    def __init__(self):\n        super().__init__()\n        self.setFrameShape(QFrame.StyledPanel)\n        self.setWindowFlags(Qt.ToolTip)\n        self.hide()\n\n    def show(self, pos, text, change_y=True):\n        self.setText(text)\n        self.adjustSize()\n\n        x, y = pos.x(), (pos.y() + 15 if change_y else self.y())\n        avail = QDesktopWidget().availableGeometry(self)\n        if x + self.width() > avail.right():\n            x -= self.width()\n        if y + self.height() > avail.bottom():\n            y = (pos.y() - 10 - self.height() if change_y else self.y() - self.height())\n        self.move(x, y)\n        super().show()\n\n\nDOT_COLOR = QColor(170, 220, 255, 255)\n\n\nclass DotItem(QGraphicsEllipseItem):\n    TOOLTIP_STYLE = \"\"\"ul {margin-top: 1px; margin-bottom: 1px;}\"\"\"\n    TOOLTIP_TEMPLATE = \"\"\"<html><head><style type=\"text/css\">{}</style>\n    </head><body><b>{}</b><hr/>{}</body></html>\n    \"\"\"\n\n    def __init__(self, radius, scale, offset, min_x, max_x):\n        super().__init__(0, 0, radius, radius)\n        self._min_x = min_x * scale - radius / 2 + offset\n        self._max_x = max_x * scale - radius / 2 + offset\n        self._scale = scale\n        self._offset = offset\n        self.setPos(0, - radius / 2)\n        self.setFlag(QGraphicsItem.ItemIsMovable)\n        self.setBrush(DOT_COLOR)\n        self.setPen(QPen(QBrush(QColor(20, 130, 250, 255)), 2))\n        self.setZValue(100)\n        self.tool_tip = MovableToolTip()\n        self.setAcceptHoverEvents(True)\n\n    @property\n    def value(self):\n        return (self.x() + self.rect().width() / 2 - self._offset) / self._scale\n\n    def move(self, x):\n        self.setX(x)\n\n    def move_to_val(self, val):\n        x = np.clip(self._scale * val - self.rect().width() / 2 + self._offset,\n                    self._min_x, self._max_x)\n        self.move(x)\n\n    def hoverEnterEvent(self, event):\n        self.tool_tip.show(event.screenPos(), self.get_tooltip_text())\n\n    def hoverLeaveEvent(self, event):\n        self.tool_tip.hide()\n\n    def mouseMoveEvent(self, _):\n        # Prevent click-moving of these items\n        return\n\n\nclass ProbabilitiesDotItem(DotItem):\n    def __init__(self, radius, scale, offset, min_x, max_x, title,\n                 get_probabilities):\n        self.title = title\n        self.get_probabilities = get_probabilities\n        self.movable_dot_items = []\n        self._invisible_sum = 0\n        super().__init__(radius, scale, offset, min_x, max_x)\n        self.setBrush(QColor(150, 150, 150, 255))\n        self.setPen(QPen(QBrush(QColor(75, 75, 75, 255)), 2))\n\n    def move_to_sum(self, invisible_sum: float = None):\n        total = sum(item.value for item in self.movable_dot_items)\n\n        if invisible_sum is not None:\n            self._invisible_sum = invisible_sum\n        total += self._invisible_sum\n\n        self.move_to_val(total)\n        self.parentItem().rescale()\n\n    def get_tooltip_text(self):\n        text = \"Total: {} <br/>Probability: {:.0%}\".format(\n            np.round(self.value, 2),\n            np.round(self.get_probabilities(self.value), 2))\n        return self.TOOLTIP_TEMPLATE.format(\n            self.TOOLTIP_STYLE, self.title, text)\n\n\nclass MovableDotItem(DotItem):\n    def __init__(self, radius, scale, offset, min_x, max_x):\n        self.tooltip_labels = []\n        self.tooltip_values = []\n        super().__init__(radius, scale, offset, min_x, max_x)\n        self._x = min_x * scale - radius / 2 + offset\n        self._point_dot = None\n        self._total_dot = None\n        self._probs_dot = None\n        self._vertical_line = None\n        self._mousePressFunc = None\n\n    @property\n    def vertical_line(self):\n        return self._vertical_line\n\n    @vertical_line.setter\n    def vertical_line(self, line):\n        line.setVisible(False)\n        self._vertical_line = line\n\n    @property\n    def point_dot(self):\n        return self._point_dot\n\n    @point_dot.setter\n    def point_dot(self, dot):\n        dot.setVisible(False)\n        self._point_dot = dot\n\n    @property\n    def total_dot(self):\n        return self._total_dot\n\n    @total_dot.setter\n    def total_dot(self, dot):\n        self._total_dot = dot\n        self._total_dot.movable_dot_items.append(self)\n\n    @property\n    def probs_dot(self):\n        return self._probs_dot\n\n    @probs_dot.setter\n    def probs_dot(self, dot):\n        self._probs_dot = dot\n        self._probs_dot.movable_dot_items.append(self)\n\n    def hookOnMousePress(self, func):\n        self._mousePressFunc = func\n\n    def unhookOnMousePress(self):\n        self._mousePressFunc = None\n\n    def mousePressEvent(self, event):\n        if self._mousePressFunc:\n            self._mousePressFunc()\n            self._mousePressFunc = None\n        self.tool_tip.show(event.screenPos(), self.get_tooltip_text(), False)\n        self._x = event.pos().x()\n        self.setBrush(QColor(50, 180, 250, 255))\n        self._show_vertical_line_and_point_dot()\n        return super().mousePressEvent(event)\n\n    def mouseMoveEvent(self, event):\n        self.tool_tip.show(event.screenPos(), self.get_tooltip_text(), False)\n        delta_x = event.pos().x() - self._x\n        if self._min_x <= self.x() + delta_x <= self._max_x:\n            self.move(self.x() + delta_x)\n            mod_tooltip_values = [0] + list(self.tooltip_values)\n            if np.round(self.value, 1) in np.round(mod_tooltip_values, 1):\n                index = np.where(np.round(mod_tooltip_values, 1) ==\n                                 np.round(self.value, 1))\n                time.sleep(0.05)\n                self.move_to_val(mod_tooltip_values[index[0][0]])\n        elif self.x() + delta_x < self._min_x:\n            self.move(self._min_x)\n        elif self.x() + delta_x > self._max_x:\n            self.move(self._max_x)\n        self._show_vertical_line_and_point_dot()\n        self.probs_dot.move_to_sum()\n\n    def mouseReleaseEvent(self, event):\n        self.tool_tip.hide()\n        self.setBrush(QColor(170, 220, 255, 255))\n        self.point_dot.setVisible(False)\n        self.vertical_line.setVisible(False)\n        return super().mousePressEvent(event)\n\n    def _show_vertical_line_and_point_dot(self):\n        self.vertical_line.setX(self.x() + self.rect().width() / 2 - self._offset)\n        self.vertical_line.setVisible(True)\n        self.point_dot.move_to_val(self.value)\n        self.point_dot.setVisible(True)\n\n\nclass DiscreteMovableDotItem(MovableDotItem):\n    def get_tooltip_text(self):\n        labels = self._get_tooltip_labels_with_percentages()\n        return self.TOOLTIP_TEMPLATE.format(\n            self.TOOLTIP_STYLE, \"Points: {}\".format(np.round(self.value, 2)),\n            \"\".join(\"{}: {:.0%}<br/>\".format(l, v) for l, v in labels)[:-5])\n\n    def _get_tooltip_labels_with_percentages(self):\n        if not len(self.tooltip_labels):\n            return []\n        for i, val in enumerate(self.tooltip_values):\n            if val > self.value:\n                break\n        # pylint: disable=undefined-loop-variable\n        diff = self.tooltip_values[i] - self.tooltip_values[i - 1]\n        p1 = 0 if diff < 1e-6 else (-self.value + self.tooltip_values[i]) / diff\n        return [(self.tooltip_labels[i - 1].replace(\"<\", \"&lt;\"), abs(p1)),\n                (self.tooltip_labels[i].replace(\"<\", \"&lt;\"), abs(1 - p1))]\n\n\nclass GraphicsColorAnimator(QObject):\n    @pyqtProperty(QColor)\n    def brushColor(self):\n        return self.__brushColor\n\n    @brushColor.setter\n    def brushColor(self, value):\n        self.__brushColor = value\n        for item in self.__items:\n            item.setBrush(value)\n\n    def __init__(self, parent, duration, keyValues):\n        super().__init__(parent)\n        self.__items = []\n        self.__defaultColor = defaultColor = keyValues[0][1]\n        self.__brushColor = defaultColor\n\n        self.__animation = QPropertyAnimation(self, b'brushColor', self)\n        self.__animation.setStartValue(defaultColor)\n        self.__animation.setEndValue(defaultColor)\n        self.__animation.setDuration(duration)\n        self.__animation.setKeyValues(keyValues)\n        self.__animation.setLoopCount(-1)\n\n    def setGraphicsItems(self, items):\n        if self.__animation.state() == QPropertyAnimation.Running:\n            self.__animation.stop()\n        self.__items = items\n        for item in items:\n            item.hookOnMousePress(self.stop)\n\n    def start(self):\n        self.__animation.start()\n\n    def stop(self):\n        if self.__animation.state() != QPropertyAnimation.Running:\n            return\n        self.__animation.stop()\n        for item in self.__items:\n            item.setBrush(self.__defaultColor)\n\n    def clear(self):\n        for item in self.__items:\n            item.unhookOnMousePress()\n        self.__items = []\n\n\n\nclass ContinuousItemMixin:\n    def get_tooltip_text(self):\n        return self.TOOLTIP_TEMPLATE.format(\n            self.TOOLTIP_STYLE, \"Points: {}\".format(np.round(self.value, 2)),\n            \"Value: {}\".format(np.round(self._get_tooltip_label_value(), 1)))\n\n    def _get_tooltip_label_value(self):\n        if not len(self.tooltip_labels):\n            return self.value\n        start = float(self.tooltip_labels[0])\n        stop = float(self.tooltip_labels[-1])\n        delta = (self.tooltip_values[-1] - self.tooltip_values[0])\n        if not delta:\n            return np.nan\n        return start + self.value * (stop - start) / delta\n\n\nclass ContinuousMovableDotItem(MovableDotItem, ContinuousItemMixin):\n    pass\n\n\nclass Continuous2DMovableDotItem(MovableDotItem, ContinuousItemMixin):\n    def __init__(self, radius, scale, offset, min_x, max_x, min_y, max_y):\n        super().__init__(radius, scale, offset, min_x, max_x)\n        self._min_y = min_y\n        self._max_y = max_y\n        self._horizontal_line = None\n\n    @property\n    def horizontal_line(self):\n        return self._horizontal_line\n\n    @horizontal_line.setter\n    def horizontal_line(self, line):\n        line.setVisible(False)\n        self._horizontal_line = line\n\n    def move(self, x):\n        super().move(x)\n        diff_ = np.nan_to_num(self._max_x - self._min_x)\n        k = (x - self._min_x) / diff_ if diff_ else 0\n        self.setY(self._min_y - self.rect().width() / 2 + (self._max_y - self._min_y) * k)\n\n    def mousePressEvent(self, event):\n        self._show_horizontal_line()\n        return super().mousePressEvent(event)\n\n    def mouseMoveEvent(self, event):\n        super().mouseMoveEvent(event)\n        self._show_horizontal_line()\n\n    def mouseReleaseEvent(self, event):\n        self.horizontal_line.setVisible(False)\n        return super().mouseReleaseEvent(event)\n\n    def _show_horizontal_line(self):\n        self.horizontal_line.setY(self.y() + self.rect().width() / 2 -\n                                  abs(self._max_y - self._min_y) / 2)\n        self.horizontal_line.setVisible(True)\n\n\nclass RulerItem(QGraphicsWidget):\n    tick_height = 6\n    tick_width = 0\n    DOT_RADIUS = 12\n    half_tick_height = 3\n    bold_label = True\n    DOT_ITEM_CLS = DotItem\n\n    def __init__(self, name, values, scale, name_offset, offset, labels=None):\n        super().__init__()\n\n        # leading label\n        font = name.document().defaultFont()\n        if self.bold_label:\n            font.setWeight(QFont.Bold)\n        name.setFont(font)\n        name.setPos(name_offset, -10)\n        name.setParentItem(self)\n\n        # prediction marker\n        self.dot = self.DOT_ITEM_CLS(self.DOT_RADIUS, scale, offset, values[0],\n                                     values[-1])\n        self.dot.setParentItem(self)\n\n        # pylint: disable=unused-variable\n        # line\n        line = QGraphicsLineItem(min(values) * scale + offset, 0,\n                                 max(values) * scale + offset, 0,\n                                 self)\n\n        if labels is None:\n            labels = [str(abs(v) if v == -0 else v) for v in values]\n\n        old_x_tick = None\n        shown_items = []\n        w = QGraphicsSimpleTextItem(labels[0]).boundingRect().width()\n        text_finish = values[0] * scale - w + offset - 10\n        for i, (label, value) in enumerate(zip(labels, values)):\n            text = QGraphicsSimpleTextItem(label)\n            x_text = value * scale - text.boundingRect().width() / 2 + offset\n            if text_finish > x_text - 10:\n                y_text, y_tick = self.DOT_RADIUS * 0.7, 0\n                text_finish = values[0] * scale + offset\n            else:\n                y_text = - text.boundingRect().height() - self.DOT_RADIUS * 0.7\n                y_tick = - self.tick_height\n                text_finish = x_text + text.boundingRect().width()\n            text.setPos(x_text, y_text)\n            if not collides(text, shown_items):\n                text.setParentItem(self)\n                shown_items.append(text)\n\n            x_tick = value * scale - self.tick_width / 2 + offset\n            tick = QGraphicsRectItem(\n                x_tick, y_tick, self.tick_width, self.tick_height,\n                self)\n            tick.setBrush(QColor(Qt.black))\n\n            if self.half_tick_height and i:\n                x = x_tick - (x_tick - old_x_tick) / 2\n                half_tick = QGraphicsLineItem(x, - self.half_tick_height, x, 0,\n                                              self)\n            old_x_tick = x_tick\n\n\nclass ProbabilitiesRulerItem(QGraphicsWidget):\n    tick_height = 6\n    DOT_RADIUS = 14\n    y_diff = 4\n\n    def __init__(self, name, values, scale, name_offset, offset, get_points,\n                 title, get_probabilities):\n        super().__init__()\n        self.scale = scale\n        self.offset = offset\n        self.get_points = get_points\n        self.min_val = min(values)\n        self.max_val = max(values)\n\n        # leading labels\n        font = name.document().defaultFont()\n        font.setWeight(QFont.Bold)\n        name_total = QGraphicsTextItem(\"Total\", self)\n        name_total.setFont(font)\n        name_total.setPos(name_offset, -25)\n        name.setFont(font)\n        name.setPos(name_offset, 10)\n        name.setParentItem(self)\n\n        # prediction marker\n        self.dot = ProbabilitiesDotItem(\n            self.DOT_RADIUS, scale, offset, values[0], values[-1],\n            title, get_probabilities)\n        self.dot.setPos(0, (- self.DOT_RADIUS + self.y_diff) / 2)\n        self.dot.setParentItem(self)\n\n        # pylint: disable=unused-variable\n        # two lines\n        t_line = QGraphicsLineItem(self.min_val * scale + offset, 0,\n                                   self.max_val * scale + offset, 0,\n                                   self)\n        p_line = QGraphicsLineItem(self.min_val * scale + offset, self.y_diff,\n                                   self.max_val * scale + offset, self.y_diff,\n                                   self)\n\n        # ticks and labels\n        old_x_tick = values[0] * scale + offset\n        for i, value in enumerate(values[1:]):\n            x_tick = value * scale + offset\n            x = x_tick - (x_tick - old_x_tick) / 2\n            half_tick = QGraphicsLineItem(x, - self.tick_height / 2, x, 0,\n                                          self)\n            old_x_tick = x_tick\n            if i == len(values) - 2:\n                break\n            text = QGraphicsTextItem(str(abs(value) if value == -0 else value),\n                                     self)\n            x_text = value * scale - text.boundingRect().width() / 2 + offset\n            y_text = - text.boundingRect().height() - self.DOT_RADIUS * 0.7\n            text.setPos(x_text, y_text)\n            tick = QGraphicsLineItem(x_tick, -self.tick_height, x_tick, 0,\n                                     self)\n\n        self.prob_items = [\n            (i / 10, QGraphicsTextItem(\" \" + str(i * 10) + \" \"),\n             QGraphicsLineItem(0, 0, 0, 0)) for i in range(1, 10)]\n\n    def rescale(self):\n        shown_items = []\n        for prob, text, tick in self.prob_items:\n            pts = self.get_points(prob)\n            x = pts * self.scale - text.boundingRect().width() / 2 + self.offset\n            text.setPos(x, 10 + self.y_diff)\n            x = pts * self.scale + self.offset\n            tick.setLine(x, 0 + self.y_diff, x, self.tick_height + self.y_diff)\n            text.setParentItem(None)\n            tick.setParentItem(None)\n            text.setVisible(False)\n            tick.setVisible(False)\n            if self.min_val < pts < self.max_val:\n                tick.setParentItem(self)\n                tick.setVisible(True)\n                text.setParentItem(self)\n                if not collides(text, shown_items):\n                    text.setVisible(True)\n                    shown_items.append(text)\n\n\nclass DiscreteFeatureItem(RulerItem):\n    tick_height = 6\n    tick_width = 2\n    half_tick_height = 0\n    bold_label = False\n    DOT_ITEM_CLS = DiscreteMovableDotItem\n\n    def __init__(self, name, variable, values, scale, name_offset, offset):\n        labels = _get_labels(variable)\n        indices = np.argsort(values)\n        labels, values = labels[indices], values[indices]\n        super().__init__(name, values, scale, name_offset, offset, labels)\n        self.dot.tooltip_labels = labels\n        self.dot.tooltip_values = values\n\n\nclass ContinuousFeatureItem(RulerItem):\n    tick_height = 6\n    tick_width = 2\n    half_tick_height = 0\n    bold_label = False\n    DOT_ITEM_CLS = ContinuousMovableDotItem\n\n    def __init__(self, name, variable, data_extremes, values, scale,\n                 name_offset, offset):\n        labels = _get_labels(variable, data_extremes, values)\n        super().__init__(name, values, scale, name_offset, offset, labels)\n        self.dot.tooltip_labels = labels\n        self.dot.tooltip_values = values\n\n\n@singledispatch\ndef _get_labels(*_):\n    return []\n\n\n@_get_labels.register(DiscreteVariable)\ndef _(var: DiscreteVariable, *_):\n    return np.array(var.values)\n\n\n@_get_labels.register(ContinuousVariable)\ndef _(_: ContinuousVariable, data_extremes: List, values: np.ndarray, *__):\n    diff_ = np.nan_to_num(values[-1] - values[0])\n    k = (data_extremes[1] - data_extremes[0]) / diff_ if diff_ else 0\n    return [str(np.round(v * k + data_extremes[0], 1)) for v in values]\n\n\nclass ContinuousFeature2DItem(QGraphicsWidget):\n    tick_height = 6\n    tick_width = 2\n    DOT_RADIUS = 12\n    y_diff = 80\n    n_tck = 4\n\n    def __init__(self, name, _, data_extremes, values, scale, name_offset,\n                 offset):\n        super().__init__()\n        data_start, data_stop = data_extremes[0], data_extremes[1]\n        labels = [str(np.round(data_start + (data_stop - data_start) * i /\n                               (self.n_tck - 1), 1)) for i in range(self.n_tck)]\n\n        # leading label\n        font = name.document().defaultFont()\n        name.setFont(font)\n        name.setPos(name_offset, -10)\n        name.setParentItem(self)\n\n        # labels\n        ascending = data_start < data_stop\n        y_start, y_stop = (self.y_diff, 0) if ascending else (0, self.y_diff)\n        for i in range(self.n_tck):\n            text = QGraphicsSimpleTextItem(labels[i], self)\n            w = text.boundingRect().width()\n            y = y_start + (y_stop - y_start) / (self.n_tck - 1) * i\n            text.setPos(-5 - w, y - 8)\n            tick = QGraphicsLineItem(-2, y, 2, y, self)\n\n        # prediction marker\n        self.dot = Continuous2DMovableDotItem(\n            self.DOT_RADIUS, scale, offset, values[0], values[-1], y_start, y_stop)\n        self.dot.tooltip_labels = labels\n        self.dot.tooltip_values = values\n        self.dot.setParentItem(self)\n        h_line = QGraphicsLineItem(values[0] * scale + offset, self.y_diff / 2,\n                                   values[-1] * scale + offset, self.y_diff / 2,\n                                   self)\n        pen = QPen(Qt.DashLine)\n        pen.setBrush(QColor(Qt.red))\n        h_line.setPen(pen)\n        self.dot.horizontal_line = h_line\n\n        # pylint: disable=unused-variable\n        # line\n        line = QGraphicsLineItem(values[0] * scale + offset, y_start,\n                                 values[-1] * scale + offset, y_stop,\n                                 self)\n\n        # ticks\n        for value in values:\n            diff_ = np.nan_to_num(values[-1] - values[0])\n            k = (value - values[0]) / diff_ if diff_ else 0\n            y_tick = (y_stop - y_start) * k + y_start - self.tick_height / 2\n            x_tick = value * scale - self.tick_width / 2 + offset\n            tick = QGraphicsRectItem(\n                x_tick, y_tick, self.tick_width, self.tick_height,\n                self)\n            tick.setBrush(QColor(Qt.black))\n\n        # rect\n        rect = QGraphicsRectItem(\n            values[0] * scale + offset, -self.y_diff * 0.125,\n            values[-1] * scale + offset, self.y_diff * 1.25,\n            self)\n        pen = QPen(Qt.DotLine)\n        pen.setBrush(QColor(50, 150, 200, 255))\n        rect.setPen(pen)\n        self.setPreferredSize(self.preferredWidth(), self.y_diff * 1.5)\n\n\nclass NomogramItem(QGraphicsWidget):\n    def __init__(self):\n        super().__init__()\n        self._items = []\n        self.setLayout(QGraphicsLinearLayout(Qt.Vertical))\n\n    def add_items(self, items):\n        self._items = items\n        for item in items:\n            self.layout().addItem(item)\n\n\nclass OWNomogram(OWWidget):\n    name = \"Nomogram\"\n    description = \" Nomograms for Visualization of Naive Bayesian\" \\\n                  \" and Logistic Regression Classifiers.\"\n    icon = \"icons/Nomogram.svg\"\n    priority = 2000\n    keywords = []\n\n    class Inputs:\n        classifier = Input(\"Classifier\", Model)\n        data = Input(\"Data\", Table)\n\n    class Outputs:\n        features = Output(\"Features\", AttributeList)\n\n    MAX_N_ATTRS = 1000\n    POINT_SCALE = 0\n    ALIGN_LEFT = 0\n    ALIGN_ZERO = 1\n    ACCEPTABLE = (NaiveBayesModel, LogisticRegressionClassifier)\n    settingsHandler = ClassValuesContextHandler()\n    target_class_index = ContextSetting(0)\n    normalize_probabilities = Setting(False)\n    scale = Setting(1)\n    display_index = Setting(1)\n    n_attributes = Setting(10)\n    sort_index = Setting(SortBy.ABSOLUTE)\n    cont_feature_dim_index = Setting(0)\n\n    graph_name = \"scene\"\n\n    class Error(OWWidget.Error):\n        invalid_classifier = Msg(\"Nomogram accepts only Naive Bayes and \"\n                                 \"Logistic Regression classifiers.\")\n\n    def __init__(self):\n        super().__init__()\n        self.instances = None\n        self.domain = None\n        self.data = None\n        self.classifier = None\n        self.align = OWNomogram.ALIGN_ZERO\n        self.log_odds_ratios = []\n        self.log_reg_coeffs = []\n        self.log_reg_coeffs_orig = []\n        self.log_reg_cont_data_extremes = []\n        self.p = None\n        self.b0 = None\n        self.points = []\n        self.feature_items = {}\n        self.feature_marker_values = []\n        self.scale_marker_values = lambda x: x\n        self.nomogram_main = None\n        self.vertical_line = None\n        self.hidden_vertical_line = None\n        self.old_target_class_index = self.target_class_index\n        self.repaint = False\n\n        self.info.set_input_summary(self.info.NoInput)\n\n        # GUI\n        box = gui.vBox(self.controlArea, \"Target class\")\n        self.class_combo = gui.comboBox(\n            box, self, \"target_class_index\", callback=self._class_combo_changed,\n            contentsLength=12, searchable=True)\n        self.norm_check = gui.checkBox(\n            box, self, \"normalize_probabilities\", \"Normalize probabilities\",\n            hidden=True, callback=self.update_scene,\n            tooltip=\"For multiclass data 1 vs. all probabilities do not\"\n                    \" sum to 1 and therefore could be normalized.\")\n\n        self.scale_radio = gui.radioButtons(\n            self.controlArea, self, \"scale\", [\"Point scale\", \"Log odds ratios\"],\n            box=\"Scale\", callback=self.update_scene)\n\n        box = gui.vBox(self.controlArea, \"Display features\")\n        grid = QGridLayout()\n        radio_group = gui.radioButtonsInBox(\n            box, self, \"display_index\", [], orientation=grid,\n            callback=self.update_scene)\n        radio_all = gui.appendRadioButton(\n            radio_group, \"All\", addToLayout=False)\n        radio_best = gui.appendRadioButton(\n            radio_group, \"Best ranked:\", addToLayout=False)\n        spin_box = gui.hBox(None, margin=0)\n        self.n_spin = gui.spin(\n            spin_box, self, \"n_attributes\", 1, self.MAX_N_ATTRS, label=\" \",\n            controlWidth=60, callback=self._n_spin_changed)\n        grid.addWidget(radio_all, 1, 1)\n        grid.addWidget(radio_best, 2, 1)\n        grid.addWidget(spin_box, 2, 2)\n\n        self.sort_combo = gui.comboBox(\n            box, self, \"sort_index\", label=\"Rank by:\", items=SortBy.items(),\n            orientation=Qt.Horizontal, callback=self.update_scene)\n\n        self.cont_feature_dim_combo = gui.comboBox(\n            box, self, \"cont_feature_dim_index\", label=\"Numeric features: \",\n            items=[\"1D projection\", \"2D curve\"], orientation=Qt.Horizontal,\n            callback=self.update_scene)\n\n        gui.rubber(self.controlArea)\n\n        class _GraphicsView(QGraphicsView):\n            def __init__(self, scene, parent, **kwargs):\n                for k, v in dict(verticalScrollBarPolicy=Qt.ScrollBarAlwaysOff,\n                                 horizontalScrollBarPolicy=Qt.ScrollBarAlwaysOff,\n                                 viewportUpdateMode=QGraphicsView.BoundingRectViewportUpdate,\n                                 renderHints=(QPainter.Antialiasing |\n                                              QPainter.TextAntialiasing |\n                                              QPainter.SmoothPixmapTransform),\n                                 alignment=(Qt.AlignTop |\n                                            Qt.AlignLeft),\n                                 sizePolicy=QSizePolicy(QSizePolicy.MinimumExpanding,\n                                                        QSizePolicy.MinimumExpanding)).items():\n                    kwargs.setdefault(k, v)\n\n                super().__init__(scene, parent, **kwargs)\n\n        class GraphicsView(_GraphicsView):\n            def __init__(self, scene, parent):\n                super().__init__(scene, parent,\n                                 verticalScrollBarPolicy=Qt.ScrollBarAlwaysOn,\n                                 styleSheet='QGraphicsView {background: white}')\n                self.viewport().setMinimumWidth(300)  # XXX: This prevents some tests failing\n                self._is_resizing = False\n\n            w = self\n\n            def resizeEvent(self, resizeEvent):\n                # Recompute main scene on window width change\n                if resizeEvent.size().width() != resizeEvent.oldSize().width():\n                    self._is_resizing = True\n                    self.w.update_scene()\n                    self._is_resizing = False\n                return super().resizeEvent(resizeEvent)\n\n            def is_resizing(self):\n                return self._is_resizing\n\n            def sizeHint(self):\n                return QSize(400, 200)\n\n        class FixedSizeGraphicsView(_GraphicsView):\n            def __init__(self, scene, parent):\n                super().__init__(scene, parent,\n                                 sizePolicy=QSizePolicy(QSizePolicy.MinimumExpanding,\n                                                        QSizePolicy.Minimum))\n\n            def sizeHint(self):\n                return QSize(400, 85)\n\n        scene = self.scene = QGraphicsScene(self)\n\n        top_view = self.top_view = FixedSizeGraphicsView(scene, self)\n        mid_view = self.view = GraphicsView(scene, self)\n        bottom_view = self.bottom_view = FixedSizeGraphicsView(scene, self)\n\n        for view in (top_view, mid_view, bottom_view):\n            self.mainArea.layout().addWidget(view)\n\n        self.dot_animator = GraphicsColorAnimator(\n            self, 3000,\n            [\n                (0.9, DOT_COLOR),\n                (0.925, DOT_COLOR.lighter(115)),\n                (0.95, DOT_COLOR),\n                (0.975, DOT_COLOR.lighter(115)),\n                (1.0, DOT_COLOR)\n            ]\n        )\n\n    def _class_combo_changed(self):\n        with np.errstate(invalid='ignore'):\n            coeffs = [np.nan_to_num(p[self.target_class_index] /\n                                    p[self.old_target_class_index])\n                      for p in self.points]\n        points = [p[self.old_target_class_index] for p in self.points]\n        self.feature_marker_values = [\n            self.get_points_from_coeffs(v, c, p) for (v, c, p) in\n            zip(self.feature_marker_values, coeffs, points)]\n        self.feature_marker_values = np.asarray(self.feature_marker_values)\n        self.update_scene()\n        self.old_target_class_index = self.target_class_index\n\n    def _n_spin_changed(self):\n        self.display_index = 1\n        self.update_scene()\n\n    def update_controls(self):\n        self.class_combo.clear()\n        self.norm_check.setHidden(True)\n        self.cont_feature_dim_combo.setEnabled(True)\n        if self.domain is not None:\n            values = self.domain.class_vars[0].values\n            if values:\n                self.class_combo.addItems(values)\n                self.target_class_index = 0\n            if len(self.domain.attributes) > self.MAX_N_ATTRS:\n                self.display_index = 1\n            if len(self.domain.class_vars[0].values) > 2:\n                self.norm_check.setHidden(False)\n            if not self.domain.has_continuous_attributes():\n                self.cont_feature_dim_combo.setEnabled(False)\n                self.cont_feature_dim_index = 0\n        model = self.sort_combo.model()\n        item = model.item(SortBy.POSITIVE)\n        item.setFlags(item.flags() | Qt.ItemIsEnabled)\n        item = model.item(SortBy.NEGATIVE)\n        item.setFlags(item.flags() | Qt.ItemIsEnabled)\n        self.align = OWNomogram.ALIGN_ZERO\n        if self.classifier and isinstance(self.classifier,\n                                          LogisticRegressionClassifier):\n            self.align = OWNomogram.ALIGN_LEFT\n\n    @Inputs.data\n    def set_data(self, data):\n        self.instances = data\n        summary = len(data) if data else self.info.NoInput\n        details = format_summary_details(data) if data else \"\"\n        self.info.set_input_summary(summary, details)\n        self.feature_marker_values = []\n        self.set_feature_marker_values()\n        self.update_scene()\n\n    @Inputs.classifier\n    def set_classifier(self, classifier):\n        self.closeContext()\n        self.classifier = classifier\n        self.Error.clear()\n        if self.classifier and not isinstance(self.classifier, self.ACCEPTABLE):\n            self.Error.invalid_classifier()\n            self.classifier = None\n        self.domain = self.classifier.domain if self.classifier else None\n        self.data = None\n        self.calculate_log_odds_ratios()\n        self.calculate_log_reg_coefficients()\n        self.update_controls()\n        self.openContext(self.domain.class_var if self.domain is not None\n                         else None)\n        self.points = self.log_odds_ratios or self.log_reg_coeffs\n        self.feature_marker_values = []\n        self.old_target_class_index = self.target_class_index\n        self.update_scene()\n\n    def calculate_log_odds_ratios(self):\n        self.log_odds_ratios = []\n        self.p = None\n        if self.classifier is None or self.domain is None:\n            return\n        if not isinstance(self.classifier, NaiveBayesModel):\n            return\n\n        log_cont_prob = self.classifier.log_cont_prob\n        class_prob = self.classifier.class_prob\n        for i in range(len(self.domain.attributes)):\n            ca = np.exp(log_cont_prob[i]) * class_prob[:, None]\n            _or = (ca / (1 - ca)) / (class_prob / (1 - class_prob))[:, None]\n            self.log_odds_ratios.append(np.log(_or))\n        self.p = class_prob\n\n    def calculate_log_reg_coefficients(self):\n        self.log_reg_coeffs = []\n        self.log_reg_cont_data_extremes = []\n        self.b0 = None\n        if self.classifier is None or self.domain is None:\n            return\n        if not isinstance(self.classifier, LogisticRegressionClassifier):\n            return\n\n        self.domain = self.reconstruct_domain(self.classifier.original_domain,\n                                              self.domain)\n        self.data = self.classifier.original_data.transform(self.domain)\n        attrs, ranges, start = self.domain.attributes, [], 0\n        for attr in attrs:\n            stop = start + len(attr.values) if attr.is_discrete else start + 1\n            ranges.append(slice(start, stop))\n            start = stop\n\n        self.b0 = self.classifier.intercept\n        coeffs = self.classifier.coefficients\n        if len(self.domain.class_var.values) == 2:\n            self.b0 = np.hstack((self.b0 * (-1), self.b0))\n            coeffs = np.vstack((coeffs * (-1), coeffs))\n        self.log_reg_coeffs = [coeffs[:, r] for r in ranges]\n        self.log_reg_coeffs_orig = self.log_reg_coeffs.copy()\n\n        min_values = nanmin(self.data.X, axis=0)\n        max_values = nanmax(self.data.X, axis=0)\n\n        for i, min_t, max_t in zip(range(len(self.log_reg_coeffs)),\n                                   min_values, max_values):\n            if self.log_reg_coeffs[i].shape[1] == 1:\n                coef = self.log_reg_coeffs[i]\n                self.log_reg_coeffs[i] = np.hstack((coef * min_t, coef * max_t))\n                self.log_reg_cont_data_extremes.append(\n                    [sorted([min_t, max_t], reverse=(c < 0)) for c in coef.flat])\n            else:\n                self.log_reg_cont_data_extremes.append([None])\n\n    def update_scene(self):\n        self.clear_scene()\n        if self.domain is None or not len(self.points[0]):\n            self.Outputs.features.send(None)\n            return\n\n        n_attrs = self.n_attributes if self.display_index else int(1e10)\n        attr_inds, attributes = zip(*self.get_ordered_attributes()[:n_attrs])\n        self.Outputs.features.send(AttributeList(attributes))\n\n        name_items = [QGraphicsTextItem(attr.name) for attr in attributes]\n        point_text = QGraphicsTextItem(\"Points\")\n        probs_text = QGraphicsTextItem(\"Probabilities (%)\")\n        all_items = name_items + [point_text, probs_text]\n        name_offset = -max(t.boundingRect().width() for t in all_items) - 10\n        w = self.view.viewport().rect().width()\n        max_width = w + name_offset - 30\n\n        points = [self.points[i][self.target_class_index]\n                  for i in attr_inds]\n        if self.align == OWNomogram.ALIGN_LEFT:\n            points = [p - p.min() for p in points]\n        max_ = np.nan_to_num(max(max(abs(p)) for p in points))\n        d = 100 / max_ if max_ else 1\n        minimums = [p[self.target_class_index].min() for p in self.points]\n        if self.scale == OWNomogram.POINT_SCALE:\n            points = [p * d for p in points]\n\n            if self.align == OWNomogram.ALIGN_LEFT:\n                self.scale_marker_values = lambda x: (x - minimums) * d\n            else:\n                self.scale_marker_values = lambda x: x * d\n        else:\n            if self.align == OWNomogram.ALIGN_LEFT:\n                self.scale_marker_values = lambda x: x - minimums\n            else:\n                self.scale_marker_values = lambda x: x\n\n        point_item, nomogram_head = self.create_main_nomogram(\n            attributes, attr_inds,\n            name_items, points, max_width, point_text, name_offset)\n        probs_item, nomogram_foot = self.create_footer_nomogram(\n            probs_text, d, minimums, max_width, name_offset)\n        for item in self.feature_items.values():\n            item.dot.point_dot = point_item.dot\n            item.dot.probs_dot = probs_item.dot\n            item.dot.vertical_line = self.hidden_vertical_line\n\n        self.dot_animator.setGraphicsItems(\n            [item.dot for item in self.feature_items.values()]\n        )\n        self.dot_animator.start()\n\n        self.nomogram = nomogram = NomogramItem()\n        nomogram.add_items([nomogram_head, self.nomogram_main, nomogram_foot])\n        self.scene.addItem(nomogram)\n\n        self.set_feature_marker_values()\n\n        rect = QRectF(self.scene.itemsBoundingRect().x(),\n                      self.scene.itemsBoundingRect().y(),\n                      self.scene.itemsBoundingRect().width(),\n                      self.nomogram.preferredSize().height()).adjusted(10, 0, 20, 0)\n        self.scene.setSceneRect(rect)\n\n        # Clip top and bottom (60 and 150) parts from the main view\n        self.view.setSceneRect(rect.x(), rect.y() + 80, rect.width() - 10, rect.height() - 160)\n        self.view.viewport().setMaximumHeight(rect.height() - 160)\n        # Clip main part from top/bottom views\n        # below point values are imprecise (less/more than required) but this\n        # is not a problem due to clipped scene content still being drawn\n        self.top_view.setSceneRect(rect.x(), rect.y() + 3, rect.width() - 10, 20)\n        self.bottom_view.setSceneRect(rect.x(), rect.height() - 110, rect.width() - 10, 30)\n\n    @staticmethod\n    def _adjust_scale(attributes, points, max_width, diff, attr_inds,\n                      log_reg_cont_data_extremes, cls_index):\n        if not diff:\n            return max_width\n\n        def offset(name, point):\n            text_ = QGraphicsTextItem(name).boundingRect()\n            return scale * point + text_.width() / 2\n\n        lr = log_reg_cont_data_extremes\n        scale = max_width / diff\n        names = list(chain.from_iterable(\n            [_get_labels(a, lr and lr[i] and lr[i][0] and lr[i][cls_index],\n                         OWNomogram.get_ruler_values(p.min(), p.max(),\n                                                     scale * p.ptp(), False))\n             for i, a, p in zip(attr_inds, attributes, points)]))\n        points = list(chain.from_iterable(points))\n\n        old_scale = scale + 1\n        while old_scale > scale:\n            old_scale = scale\n            offsets = [offset(n, p) for n, p in zip(names, points)]\n            most_right_name = names[np.argmax(offsets)]\n            text = QGraphicsTextItem(most_right_name).boundingRect()\n            scale = (max_width - text.width() / 2) / diff\n        return scale\n\n    def create_main_nomogram(self, attributes, attr_inds, name_items, points,\n                             max_width, point_text, name_offset):\n        cls_index = self.target_class_index\n        min_p = min(p.min() for p in points)\n        max_p = max(p.max() for p in points)\n        values = self.get_ruler_values(min_p, max_p, max_width)\n        min_p, max_p = min(values), max(values)\n        diff_ = np.nan_to_num(max_p - min_p)\n        scale_x = self._adjust_scale(\n            attributes, points, max_width, diff_, attr_inds,\n            self.log_reg_cont_data_extremes, cls_index\n        )\n\n        nomogram_header = NomogramItem()\n        point_item = RulerItem(point_text, values, scale_x, name_offset,\n                               - scale_x * min_p)\n        point_item.setPreferredSize(point_item.preferredWidth(), 35)\n        nomogram_header.add_items([point_item])\n\n        self.nomogram_main = NomogramItem()\n        cont_feature_item_class = ContinuousFeature2DItem if \\\n            self.cont_feature_dim_index else ContinuousFeatureItem\n\n        feature_items = [\n            DiscreteFeatureItem(\n                name_item, attr, point,\n                scale_x, name_offset, - scale_x * min_p)\n            if attr.is_discrete else\n            cont_feature_item_class(\n                name_item, attr, self.log_reg_cont_data_extremes[i][cls_index],\n                self.get_ruler_values(\n                    point.min(), point.max(),\n                    scale_x * point.ptp(), False),\n                scale_x, name_offset, - scale_x * min_p)\n            for i, attr, name_item, point in\n            zip(attr_inds, attributes, name_items, points)]\n\n        self.nomogram_main.add_items(feature_items)\n        self.feature_items = OrderedDict(sorted(zip(attr_inds, feature_items)))\n\n        x = - scale_x * min_p\n        y = self.nomogram_main.layout().preferredHeight() + 10\n        self.vertical_line = QGraphicsLineItem(x, -6, x, y)\n        self.vertical_line.setPen(QPen(Qt.DotLine))\n        self.vertical_line.setParentItem(point_item)\n        self.hidden_vertical_line = QGraphicsLineItem(x, -6, x, y)\n        pen = QPen(Qt.DashLine)\n        pen.setBrush(QColor(Qt.red))\n        self.hidden_vertical_line.setPen(pen)\n        self.hidden_vertical_line.setParentItem(point_item)\n\n        return point_item, nomogram_header\n\n    def get_ordered_attributes(self):\n        \"\"\"Return (in_domain_index, attr) pairs, ordered by method in SortBy combo\"\"\"\n        if self.domain is None or not self.domain.attributes:\n            return []\n\n        attrs = self.domain.attributes\n        sort_by = self.sort_index\n        class_value = self.target_class_index\n\n        if sort_by == SortBy.NO_SORTING:\n            return list(enumerate(attrs))\n\n        elif sort_by == SortBy.NAME:\n\n            def key(x):\n                _, attr = x\n                return attr.name.lower()\n\n        elif sort_by == SortBy.ABSOLUTE:\n\n            def key(x):\n                i, attr = x\n                if attr.is_discrete:\n                    ptp = self.points[i][class_value].ptp()\n                else:\n                    coef = np.abs(self.log_reg_coeffs_orig[i][class_value]).mean()\n                    ptp = coef * np.ptp(self.log_reg_cont_data_extremes[i][class_value])\n                return -ptp\n\n        elif sort_by == SortBy.POSITIVE:\n\n            def key(x):\n                i, attr = x\n                max_value = (self.points[i][class_value].max()\n                             if attr.is_discrete else\n                             np.mean(self.log_reg_cont_data_extremes[i][class_value]))\n                return -max_value\n\n        elif sort_by == SortBy.NEGATIVE:\n\n            def key(x):\n                i, attr = x\n                min_value = (self.points[i][class_value].min()\n                             if attr.is_discrete else\n                             np.mean(self.log_reg_cont_data_extremes[i][class_value]))\n                return min_value\n\n        return sorted(enumerate(attrs), key=key)\n\n\n    def create_footer_nomogram(self, probs_text, d, minimums,\n                               max_width, name_offset):\n        # pylint: disable=invalid-unary-operand-type\n        eps, d_ = 0.05, 1\n        k = - np.log(self.p / (1 - self.p)) if self.p is not None else - self.b0\n        min_sum = k[self.target_class_index] - np.log((1 - eps) / eps)\n        max_sum = k[self.target_class_index] - np.log(eps / (1 - eps))\n        if self.align == OWNomogram.ALIGN_LEFT:\n            max_sum = max_sum - sum(minimums)\n            min_sum = min_sum - sum(minimums)\n            for i in range(len(k)):  # pylint: disable=consider-using-enumerate\n                k[i] = k[i] - sum([min(q) for q in [p[i] for p in self.points]])\n        if self.scale == OWNomogram.POINT_SCALE:\n            min_sum *= d\n            max_sum *= d\n            d_ = d\n\n        values = self.get_ruler_values(min_sum, max_sum, max_width)\n        min_sum, max_sum = min(values), max(values)\n        diff_ = np.nan_to_num(max_sum - min_sum)\n        scale_x = max_width / diff_ if diff_ else max_width\n        cls_var, cls_index = self.domain.class_var, self.target_class_index\n        nomogram_footer = NomogramItem()\n\n        def get_normalized_probabilities(val):\n            if not self.normalize_probabilities:\n                return 1 / (1 + np.exp(k[cls_index] - val / d_))\n            totals = self.__get_totals_for_class_values(minimums)\n            p_sum = np.sum(1 / (1 + np.exp(k - totals / d_)))\n            return 1 / (1 + np.exp(k[cls_index] - val / d_)) / p_sum\n\n        def get_points(prob):\n            if not self.normalize_probabilities:\n                return (k[cls_index] - np.log(1 / prob - 1)) * d_\n            totals = self.__get_totals_for_class_values(minimums)\n            p_sum = np.sum(1 / (1 + np.exp(k - totals / d_)))\n            return (k[cls_index] - np.log(1 / (prob * p_sum) - 1)) * d_\n\n        probs_item = ProbabilitiesRulerItem(\n            probs_text, values, scale_x, name_offset, - scale_x * min_sum,\n            get_points=get_points,\n            title=\"{}='{}'\".format(cls_var.name, cls_var.values[cls_index]),\n            get_probabilities=get_normalized_probabilities)\n        nomogram_footer.add_items([probs_item])\n        return probs_item, nomogram_footer\n\n    def __get_totals_for_class_values(self, minimums):\n        cls_index = self.target_class_index\n        marker_values = self.scale_marker_values(self.feature_marker_values)\n        totals = np.full(len(self.domain.class_var.values), np.nan)\n        totals[cls_index] = marker_values.sum()\n        for i in range(len(self.domain.class_var.values)):\n            if i == cls_index:\n                continue\n            coeffs = [np.nan_to_num(p[i] / p[cls_index]) for p in self.points]\n            points = [p[cls_index] for p in self.points]\n            total = sum([self.get_points_from_coeffs(v, c, p) for (v, c, p)\n                         in zip(self.feature_marker_values, coeffs, points)])\n            if self.align == OWNomogram.ALIGN_LEFT:\n                points = [p - m for m, p in zip(minimums, points)]\n                total -= sum([min(p) for p in [p[i] for p in self.points]])\n            d = 100 / max(max(abs(p)) for p in points)\n            if self.scale == OWNomogram.POINT_SCALE:\n                total *= d\n            totals[i] = total\n        assert not np.any(np.isnan(totals))\n        return totals\n\n    def set_feature_marker_values(self):\n        if not (len(self.points) and len(self.feature_items)):\n            return\n        if not len(self.feature_marker_values):\n            self._init_feature_marker_values()\n        marker_values = self.scale_marker_values(self.feature_marker_values)\n\n        invisible_sum = 0\n        for i, marker in enumerate(marker_values):\n            try:\n                item = self.feature_items[i]\n            except KeyError:\n                invisible_sum += marker\n            else:\n                item.dot.move_to_val(marker)\n\n        item.dot.probs_dot.move_to_sum(invisible_sum)\n\n    def _init_feature_marker_values(self):\n        self.feature_marker_values = []\n        cls_index = self.target_class_index\n        instances = self.instances.transform(self.domain) \\\n            if self.instances else None\n        values = []\n        for i, attr in enumerate(self.domain.attributes):\n            value, feature_val = 0, None\n            if len(self.log_reg_coeffs):\n                if attr.is_discrete:\n                    ind, n = unique(self.data.X[:, i], return_counts=True)\n                    feature_val = np.nan_to_num(ind[np.argmax(n)])\n                else:\n                    feature_val = nanmean(self.data.X[:, i])\n\n            # If data is provided on a separate signal, use the first data\n            # instance to position the points instead of the mean\n            inst_in_dom = instances and attr in instances.domain\n            if inst_in_dom and not np.isnan(instances[0][attr]):\n                feature_val = instances[0][attr]\n\n            if feature_val is not None:\n                value = (self.points[i][cls_index][int(feature_val)]\n                         if attr.is_discrete else\n                         self.log_reg_coeffs_orig[i][cls_index][0] * feature_val)\n            values.append(value)\n        self.feature_marker_values = np.asarray(values)\n\n    def clear_scene(self):\n        self.feature_items = {}\n        self.scale_marker_values = lambda x: x\n        self.nomogram = None\n        self.nomogram_main = None\n        self.vertical_line = None\n        self.hidden_vertical_line = None\n        self.dot_animator.clear()\n        self.scene.clear()\n\n    def send_report(self):\n        self.report_plot()\n\n    @staticmethod\n    def reconstruct_domain(original, preprocessed):\n        # abuse dict to make \"in\" comparisons faster\n        attrs = OrderedDict()\n        for attr in preprocessed.attributes:\n            cv = attr._compute_value.variable._compute_value\n            var = cv.variable if cv else original[attr.name]\n            if var in attrs:    # the reason for OrderedDict\n                continue\n            attrs[var] = None   # we only need keys\n        attrs = list(attrs.keys())\n        return Domain(attrs, original.class_var, original.metas)\n\n    @staticmethod\n    def get_ruler_values(start, stop, max_width, round_to_nearest=True):\n        if max_width == 0:\n            return [0]\n        diff = np.nan_to_num((stop - start) / max_width)\n        if diff <= 0:\n            return [0]\n        decimals = int(np.floor(np.log10(diff)))\n        if diff > 4 * pow(10, decimals):\n            step = 5 * pow(10, decimals + 2)\n        elif diff > 2 * pow(10, decimals):\n            step = 2 * pow(10, decimals + 2)\n        elif diff > 1 * pow(10, decimals):\n            step = 1 * pow(10, decimals + 2)\n        else:\n            step = 5 * pow(10, decimals + 1)\n        round_by = int(- np.floor(np.log10(step)))\n        r = start % step\n        if not round_to_nearest:\n            _range = np.arange(start + step, stop + r, step) - r\n            start, stop = np.floor(start * 100) / 100, np.ceil(stop * 100) / 100\n            return np.round(np.hstack((start, _range, stop)), 2)\n        return np.round(np.arange(start, stop + r + step, step) - r, round_by)\n\n    @staticmethod\n    def get_points_from_coeffs(current_value, coefficients, possible_values):\n        if np.isnan(possible_values).any():\n            return 0\n        # pylint: disable=undefined-loop-variable\n        indices = np.argsort(possible_values)\n        sorted_values = possible_values[indices]\n        sorted_coefficients = coefficients[indices]\n        for i, val in enumerate(sorted_values):\n            if current_value < val:\n                break\n        diff = sorted_values[i] - sorted_values[i - 1]\n        k = 0 if diff < 1e-6 else (sorted_values[i] - current_value) / \\\n                                  (sorted_values[i] - sorted_values[i - 1])\n        return sorted_coefficients[i - 1] * sorted_values[i - 1] * k + \\\n               sorted_coefficients[i] * sorted_values[i] * (1 - k)\n\n    def reset_settings(self):\n        with warnings.catch_warnings():\n            # setting target_class_index will trigger this innocent warning\n            warnings.filterwarnings(\n                \"ignore\", \"combo box 'target_class_index' is empty\")\n            self._reset_settings()\n        self.update_scene()\n\n\nif __name__ == \"__main__\":  # pragma: no cover\n    from Orange.classification import NaiveBayesLearner  #, LogisticRegressionLearner\n    data = Table(\"heart_disease\")\n    clf = NaiveBayesLearner()(data)\n    # clf = LogisticRegressionLearner()(data)\n    WidgetPreview(OWNomogram).run(set_classifier=clf, set_data=data)\n",
            "new_file_content": "import time\nimport warnings\nfrom itertools import chain\nfrom typing import List\nfrom functools import singledispatch\nfrom enum import IntEnum\nfrom collections import OrderedDict\n\nimport numpy as np\n\nfrom AnyQt.QtWidgets import (\n    QGraphicsView, QGraphicsScene, QGraphicsItem, QGraphicsSimpleTextItem,\n    QGraphicsTextItem, QGraphicsLineItem, QGraphicsWidget, QGraphicsRectItem,\n    QGraphicsEllipseItem, QGraphicsLinearLayout, QGridLayout, QLabel, QFrame,\n    QSizePolicy, QDesktopWidget,\n)\nfrom AnyQt.QtGui import QColor, QPainter, QFont, QPen, QBrush\nfrom AnyQt.QtCore import Qt, QRectF, QSize, QPropertyAnimation, QObject, \\\n    pyqtProperty\n\nfrom Orange.data import Table, Domain, DiscreteVariable, ContinuousVariable\nfrom Orange.statistics.util import nanmin, nanmax, nanmean, unique\nfrom Orange.classification import Model\nfrom Orange.classification.naive_bayes import NaiveBayesModel\nfrom Orange.classification.logistic_regression import \\\n    LogisticRegressionClassifier\nfrom Orange.widgets.settings import Setting, ContextSetting, \\\n    ClassValuesContextHandler\nfrom Orange.widgets.utils.widgetpreview import WidgetPreview\nfrom Orange.widgets.utils.state_summary import format_summary_details\nfrom Orange.widgets.widget import OWWidget, Msg, Input, Output, AttributeList\nfrom Orange.widgets import gui\n\n\ndef collides(item, items):\n    return any(item.collidesWithItem(i) for i in items)\n\n\nclass SortBy(IntEnum):\n    NO_SORTING, NAME, ABSOLUTE, POSITIVE, NEGATIVE = 0, 1, 2, 3, 4\n\n    @staticmethod\n    def items():\n        return [\"No sorting\", \"Name\", \"Absolute importance\",\n                \"Positive influence\", \"Negative influence\"]\n\n\nclass MovableToolTip(QLabel):\n    def __init__(self):\n        super().__init__()\n        self.setFrameShape(QFrame.StyledPanel)\n        self.setWindowFlags(Qt.ToolTip)\n        self.hide()\n\n    def show(self, pos, text, change_y=True):\n        self.setText(text)\n        self.adjustSize()\n\n        x, y = pos.x(), (pos.y() + 15 if change_y else self.y())\n        avail = QDesktopWidget().availableGeometry(self)\n        if x + self.width() > avail.right():\n            x -= self.width()\n        if y + self.height() > avail.bottom():\n            y = (pos.y() - 10 - self.height() if change_y else self.y() - self.height())\n        self.move(x, y)\n        super().show()\n\n\nDOT_COLOR = QColor(170, 220, 255, 255)\n\n\nclass DotItem(QGraphicsEllipseItem):\n    TOOLTIP_STYLE = \"\"\"ul {margin-top: 1px; margin-bottom: 1px;}\"\"\"\n    TOOLTIP_TEMPLATE = \"\"\"<html><head><style type=\"text/css\">{}</style>\n    </head><body><b>{}</b><hr/>{}</body></html>\n    \"\"\"\n\n    def __init__(self, radius, scale, offset, min_x, max_x):\n        super().__init__(0, 0, radius, radius)\n        self._min_x = min_x * scale - radius / 2 + offset\n        self._max_x = max_x * scale - radius / 2 + offset\n        self._scale = scale\n        self._offset = offset\n        self.setPos(0, - radius / 2)\n        self.setFlag(QGraphicsItem.ItemIsMovable)\n        self.setBrush(DOT_COLOR)\n        self.setPen(QPen(QBrush(QColor(20, 130, 250, 255)), 2))\n        self.setZValue(100)\n        self.tool_tip = MovableToolTip()\n        self.setAcceptHoverEvents(True)\n\n    @property\n    def value(self):\n        return (self.x() + self.rect().width() / 2 - self._offset) / self._scale\n\n    def move(self, x):\n        self.setX(x)\n\n    def move_to_val(self, val):\n        x = np.clip(self._scale * val - self.rect().width() / 2 + self._offset,\n                    self._min_x, self._max_x)\n        self.move(x)\n\n    def hoverEnterEvent(self, event):\n        self.tool_tip.show(event.screenPos(), self.get_tooltip_text())\n\n    def hoverLeaveEvent(self, event):\n        self.tool_tip.hide()\n\n    def mouseMoveEvent(self, _):\n        # Prevent click-moving of these items\n        return\n\n\nclass ProbabilitiesDotItem(DotItem):\n    def __init__(self, radius, scale, offset, min_x, max_x, title,\n                 get_probabilities):\n        self.title = title\n        self.get_probabilities = get_probabilities\n        self.movable_dot_items = []\n        self._invisible_sum = 0\n        super().__init__(radius, scale, offset, min_x, max_x)\n        self.setBrush(QColor(150, 150, 150, 255))\n        self.setPen(QPen(QBrush(QColor(75, 75, 75, 255)), 2))\n\n    def move_to_sum(self, invisible_sum: float = None):\n        total = sum(item.value for item in self.movable_dot_items)\n\n        if invisible_sum is not None:\n            self._invisible_sum = invisible_sum\n        total += self._invisible_sum\n\n        self.move_to_val(total)\n        self.parentItem().rescale()\n\n    def get_tooltip_text(self):\n        text = \"Total: {} <br/>Probability: {:.0%}\".format(\n            np.round(self.value, 2),\n            np.round(self.get_probabilities(self.value), 2))\n        return self.TOOLTIP_TEMPLATE.format(\n            self.TOOLTIP_STYLE, self.title, text)\n\n\nclass MovableDotItem(DotItem):\n    def __init__(self, radius, scale, offset, min_x, max_x):\n        self.tooltip_labels = []\n        self.tooltip_values = []\n        super().__init__(radius, scale, offset, min_x, max_x)\n        self._x = min_x * scale - radius / 2 + offset\n        self._point_dot = None\n        self._total_dot = None\n        self._probs_dot = None\n        self._vertical_line = None\n        self._mousePressFunc = None\n\n    @property\n    def vertical_line(self):\n        return self._vertical_line\n\n    @vertical_line.setter\n    def vertical_line(self, line):\n        line.setVisible(False)\n        self._vertical_line = line\n\n    @property\n    def point_dot(self):\n        return self._point_dot\n\n    @point_dot.setter\n    def point_dot(self, dot):\n        dot.setVisible(False)\n        self._point_dot = dot\n\n    @property\n    def total_dot(self):\n        return self._total_dot\n\n    @total_dot.setter\n    def total_dot(self, dot):\n        self._total_dot = dot\n        self._total_dot.movable_dot_items.append(self)\n\n    @property\n    def probs_dot(self):\n        return self._probs_dot\n\n    @probs_dot.setter\n    def probs_dot(self, dot):\n        self._probs_dot = dot\n        self._probs_dot.movable_dot_items.append(self)\n\n    def hookOnMousePress(self, func):\n        self._mousePressFunc = func\n\n    def unhookOnMousePress(self):\n        self._mousePressFunc = None\n\n    def mousePressEvent(self, event):\n        if self._mousePressFunc:\n            self._mousePressFunc()\n            self._mousePressFunc = None\n        self.tool_tip.show(event.screenPos(), self.get_tooltip_text(), False)\n        self._x = event.pos().x()\n        self.setBrush(QColor(50, 180, 250, 255))\n        self._show_vertical_line_and_point_dot()\n        return super().mousePressEvent(event)\n\n    def mouseMoveEvent(self, event):\n        self.tool_tip.show(event.screenPos(), self.get_tooltip_text(), False)\n        delta_x = event.pos().x() - self._x\n        if self._min_x <= self.x() + delta_x <= self._max_x:\n            self.move(self.x() + delta_x)\n            mod_tooltip_values = [0] + list(self.tooltip_values)\n            if np.round(self.value, 1) in np.round(mod_tooltip_values, 1):\n                index = np.where(np.round(mod_tooltip_values, 1) ==\n                                 np.round(self.value, 1))\n                time.sleep(0.05)\n                self.move_to_val(mod_tooltip_values[index[0][0]])\n        elif self.x() + delta_x < self._min_x:\n            self.move(self._min_x)\n        elif self.x() + delta_x > self._max_x:\n            self.move(self._max_x)\n        self._show_vertical_line_and_point_dot()\n        self.probs_dot.move_to_sum()\n\n    def mouseReleaseEvent(self, event):\n        self.tool_tip.hide()\n        self.setBrush(QColor(170, 220, 255, 255))\n        self.point_dot.setVisible(False)\n        self.vertical_line.setVisible(False)\n        return super().mousePressEvent(event)\n\n    def _show_vertical_line_and_point_dot(self):\n        self.vertical_line.setX(self.x() + self.rect().width() / 2 - self._offset)\n        self.vertical_line.setVisible(True)\n        self.point_dot.move_to_val(self.value)\n        self.point_dot.setVisible(True)\n\n\nclass DiscreteMovableDotItem(MovableDotItem):\n    def get_tooltip_text(self):\n        labels = self._get_tooltip_labels_with_percentages()\n        return self.TOOLTIP_TEMPLATE.format(\n            self.TOOLTIP_STYLE, \"Points: {}\".format(np.round(self.value, 2)),\n            \"\".join(\"{}: {:.0%}<br/>\".format(l, v) for l, v in labels)[:-5])\n\n    def _get_tooltip_labels_with_percentages(self):\n        if not len(self.tooltip_labels):\n            return []\n        for i, val in enumerate(self.tooltip_values):\n            if val > self.value:\n                break\n        # pylint: disable=undefined-loop-variable\n        diff = self.tooltip_values[i] - self.tooltip_values[i - 1]\n        p1 = 0 if diff < 1e-6 else (-self.value + self.tooltip_values[i]) / diff\n        return [(self.tooltip_labels[i - 1].replace(\"<\", \"&lt;\"), abs(p1)),\n                (self.tooltip_labels[i].replace(\"<\", \"&lt;\"), abs(1 - p1))]\n\n\nclass GraphicsColorAnimator(QObject):\n    @pyqtProperty(QColor)\n    def brushColor(self):\n        return self.__brushColor\n\n    @brushColor.setter\n    def brushColor(self, value):\n        self.__brushColor = value\n        for item in self.__items:\n            item.setBrush(value)\n\n    def __init__(self, parent, duration, keyValues):\n        super().__init__(parent)\n        self.__items = []\n        self.__defaultColor = defaultColor = keyValues[0][1]\n        self.__brushColor = defaultColor\n\n        self.__animation = QPropertyAnimation(self, b'brushColor', self)\n        self.__animation.setStartValue(defaultColor)\n        self.__animation.setEndValue(defaultColor)\n        self.__animation.setDuration(duration)\n        self.__animation.setKeyValues(keyValues)\n        self.__animation.setLoopCount(-1)\n\n    def setGraphicsItems(self, items):\n        if self.__animation.state() == QPropertyAnimation.Running:\n            self.__animation.stop()\n        self.__items = items\n        for item in items:\n            item.hookOnMousePress(self.stop)\n\n    def start(self):\n        self.__animation.start()\n\n    def stop(self):\n        if self.__animation.state() != QPropertyAnimation.Running:\n            return\n        self.__animation.stop()\n        for item in self.__items:\n            item.setBrush(self.__defaultColor)\n\n    def clear(self):\n        for item in self.__items:\n            item.unhookOnMousePress()\n        self.__items = []\n\n\n\nclass ContinuousItemMixin:\n    def get_tooltip_text(self):\n        return self.TOOLTIP_TEMPLATE.format(\n            self.TOOLTIP_STYLE, \"Points: {}\".format(np.round(self.value, 2)),\n            \"Value: {}\".format(np.round(self._get_tooltip_label_value(), 1)))\n\n    def _get_tooltip_label_value(self):\n        if not len(self.tooltip_labels):\n            return self.value\n        start = float(self.tooltip_labels[0])\n        stop = float(self.tooltip_labels[-1])\n        delta = (self.tooltip_values[-1] - self.tooltip_values[0])\n        if not delta:\n            return np.nan\n        return start + self.value * (stop - start) / delta\n\n\nclass ContinuousMovableDotItem(MovableDotItem, ContinuousItemMixin):\n    pass\n\n\nclass Continuous2DMovableDotItem(MovableDotItem, ContinuousItemMixin):\n    def __init__(self, radius, scale, offset, min_x, max_x, min_y, max_y):\n        super().__init__(radius, scale, offset, min_x, max_x)\n        self._min_y = min_y\n        self._max_y = max_y\n        self._horizontal_line = None\n\n    @property\n    def horizontal_line(self):\n        return self._horizontal_line\n\n    @horizontal_line.setter\n    def horizontal_line(self, line):\n        line.setVisible(False)\n        self._horizontal_line = line\n\n    def move(self, x):\n        super().move(x)\n        diff_ = np.nan_to_num(self._max_x - self._min_x)\n        k = (x - self._min_x) / diff_ if diff_ else 0\n        self.setY(self._min_y - self.rect().width() / 2 + (self._max_y - self._min_y) * k)\n\n    def mousePressEvent(self, event):\n        self._show_horizontal_line()\n        return super().mousePressEvent(event)\n\n    def mouseMoveEvent(self, event):\n        super().mouseMoveEvent(event)\n        self._show_horizontal_line()\n\n    def mouseReleaseEvent(self, event):\n        self.horizontal_line.setVisible(False)\n        return super().mouseReleaseEvent(event)\n\n    def _show_horizontal_line(self):\n        self.horizontal_line.setY(self.y() + self.rect().width() / 2 -\n                                  abs(self._max_y - self._min_y) / 2)\n        self.horizontal_line.setVisible(True)\n\n\nclass RulerItem(QGraphicsWidget):\n    tick_height = 6\n    tick_width = 0\n    DOT_RADIUS = 12\n    half_tick_height = 3\n    bold_label = True\n    DOT_ITEM_CLS = DotItem\n\n    def __init__(self, name, values, scale, name_offset, offset, labels=None):\n        super().__init__()\n\n        # leading label\n        font = name.document().defaultFont()\n        if self.bold_label:\n            font.setWeight(QFont.Bold)\n        name.setFont(font)\n        name.setPos(name_offset, -10)\n        name.setParentItem(self)\n\n        # prediction marker\n        self.dot = self.DOT_ITEM_CLS(self.DOT_RADIUS, scale, offset, values[0],\n                                     values[-1])\n        self.dot.setParentItem(self)\n\n        # pylint: disable=unused-variable\n        # line\n        line = QGraphicsLineItem(min(values) * scale + offset, 0,\n                                 max(values) * scale + offset, 0,\n                                 self)\n\n        if labels is None:\n            labels = [str(abs(v) if v == -0 else v) for v in values]\n\n        old_x_tick = None\n        shown_items = []\n        w = QGraphicsSimpleTextItem(labels[0]).boundingRect().width()\n        text_finish = values[0] * scale - w + offset - 10\n        for i, (label, value) in enumerate(zip(labels, values)):\n            text = QGraphicsSimpleTextItem(label)\n            x_text = value * scale - text.boundingRect().width() / 2 + offset\n            if text_finish > x_text - 10:\n                y_text, y_tick = self.DOT_RADIUS * 0.7, 0\n                text_finish = values[0] * scale + offset\n            else:\n                y_text = - text.boundingRect().height() - self.DOT_RADIUS * 0.7\n                y_tick = - self.tick_height\n                text_finish = x_text + text.boundingRect().width()\n            text.setPos(x_text, y_text)\n            if not collides(text, shown_items):\n                text.setParentItem(self)\n                shown_items.append(text)\n\n            x_tick = value * scale - self.tick_width / 2 + offset\n            tick = QGraphicsRectItem(\n                x_tick, y_tick, self.tick_width, self.tick_height,\n                self)\n            tick.setBrush(QColor(Qt.black))\n\n            if self.half_tick_height and i:\n                x = x_tick - (x_tick - old_x_tick) / 2\n                half_tick = QGraphicsLineItem(x, - self.half_tick_height, x, 0,\n                                              self)\n            old_x_tick = x_tick\n\n\nclass ProbabilitiesRulerItem(QGraphicsWidget):\n    tick_height = 6\n    DOT_RADIUS = 14\n    y_diff = 4\n\n    def __init__(self, name, values, scale, name_offset, offset, get_points,\n                 title, get_probabilities):\n        super().__init__()\n        self.scale = scale\n        self.offset = offset\n        self.get_points = get_points\n        self.min_val = min(values)\n        self.max_val = max(values)\n\n        # leading labels\n        font = name.document().defaultFont()\n        font.setWeight(QFont.Bold)\n        name_total = QGraphicsTextItem(\"Total\", self)\n        name_total.setFont(font)\n        name_total.setPos(name_offset, -25)\n        name.setFont(font)\n        name.setPos(name_offset, 10)\n        name.setParentItem(self)\n\n        # prediction marker\n        self.dot = ProbabilitiesDotItem(\n            self.DOT_RADIUS, scale, offset, values[0], values[-1],\n            title, get_probabilities)\n        self.dot.setPos(0, (- self.DOT_RADIUS + self.y_diff) / 2)\n        self.dot.setParentItem(self)\n\n        # pylint: disable=unused-variable\n        # two lines\n        t_line = QGraphicsLineItem(self.min_val * scale + offset, 0,\n                                   self.max_val * scale + offset, 0,\n                                   self)\n        p_line = QGraphicsLineItem(self.min_val * scale + offset, self.y_diff,\n                                   self.max_val * scale + offset, self.y_diff,\n                                   self)\n\n        # ticks and labels\n        old_x_tick = values[0] * scale + offset\n        for i, value in enumerate(values[1:]):\n            x_tick = value * scale + offset\n            x = x_tick - (x_tick - old_x_tick) / 2\n            half_tick = QGraphicsLineItem(x, - self.tick_height / 2, x, 0,\n                                          self)\n            old_x_tick = x_tick\n            if i == len(values) - 2:\n                break\n            text = QGraphicsTextItem(str(abs(value) if value == -0 else value),\n                                     self)\n            x_text = value * scale - text.boundingRect().width() / 2 + offset\n            y_text = - text.boundingRect().height() - self.DOT_RADIUS * 0.7\n            text.setPos(x_text, y_text)\n            tick = QGraphicsLineItem(x_tick, -self.tick_height, x_tick, 0,\n                                     self)\n\n        self.prob_items = [\n            (i / 10, QGraphicsTextItem(\" \" + str(i * 10) + \" \"),\n             QGraphicsLineItem(0, 0, 0, 0)) for i in range(1, 10)]\n\n    def rescale(self):\n        shown_items = []\n        for prob, text, tick in self.prob_items:\n            pts = self.get_points(prob)\n            x = pts * self.scale - text.boundingRect().width() / 2 + self.offset\n            text.setPos(x, 10 + self.y_diff)\n            x = pts * self.scale + self.offset\n            tick.setLine(x, 0 + self.y_diff, x, self.tick_height + self.y_diff)\n            text.setParentItem(None)\n            tick.setParentItem(None)\n            text.setVisible(False)\n            tick.setVisible(False)\n            if self.min_val < pts < self.max_val:\n                tick.setParentItem(self)\n                tick.setVisible(True)\n                text.setParentItem(self)\n                if not collides(text, shown_items):\n                    text.setVisible(True)\n                    shown_items.append(text)\n\n\nclass DiscreteFeatureItem(RulerItem):\n    tick_height = 6\n    tick_width = 2\n    half_tick_height = 0\n    bold_label = False\n    DOT_ITEM_CLS = DiscreteMovableDotItem\n\n    def __init__(self, name, variable, values, scale, name_offset, offset):\n        labels = _get_labels(variable)\n        indices = np.argsort(values)\n        labels, values = labels[indices], values[indices]\n        super().__init__(name, values, scale, name_offset, offset, labels)\n        self.dot.tooltip_labels = labels\n        self.dot.tooltip_values = values\n\n\nclass ContinuousFeatureItem(RulerItem):\n    tick_height = 6\n    tick_width = 2\n    half_tick_height = 0\n    bold_label = False\n    DOT_ITEM_CLS = ContinuousMovableDotItem\n\n    def __init__(self, name, variable, data_extremes, values, scale,\n                 name_offset, offset):\n        labels = _get_labels(variable, data_extremes, values)\n        super().__init__(name, values, scale, name_offset, offset, labels)\n        self.dot.tooltip_labels = labels\n        self.dot.tooltip_values = values\n\n\n@singledispatch\ndef _get_labels(*_):\n    return []\n\n\n@_get_labels.register(DiscreteVariable)\ndef _(var: DiscreteVariable, *_):\n    return np.array(var.values)\n\n\n@_get_labels.register(ContinuousVariable)\ndef _(_: ContinuousVariable, data_extremes: List, values: np.ndarray, *__):\n    diff_ = np.nan_to_num(values[-1] - values[0])\n    k = (data_extremes[1] - data_extremes[0]) / diff_ if diff_ else 0\n    return [str(np.round(v * k + data_extremes[0], 1)) for v in values]\n\n\nclass ContinuousFeature2DItem(QGraphicsWidget):\n    tick_height = 6\n    tick_width = 2\n    DOT_RADIUS = 12\n    y_diff = 80\n    n_tck = 4\n\n    def __init__(self, name, _, data_extremes, values, scale, name_offset,\n                 offset):\n        super().__init__()\n        data_start, data_stop = data_extremes[0], data_extremes[1]\n        labels = [str(np.round(data_start + (data_stop - data_start) * i /\n                               (self.n_tck - 1), 1)) for i in range(self.n_tck)]\n\n        # leading label\n        font = name.document().defaultFont()\n        name.setFont(font)\n        name.setPos(name_offset, -10)\n        name.setParentItem(self)\n\n        # labels\n        ascending = data_start < data_stop\n        y_start, y_stop = (self.y_diff, 0) if ascending else (0, self.y_diff)\n        for i in range(self.n_tck):\n            text = QGraphicsSimpleTextItem(labels[i], self)\n            w = text.boundingRect().width()\n            y = y_start + (y_stop - y_start) / (self.n_tck - 1) * i\n            text.setPos(-5 - w, y - 8)\n            tick = QGraphicsLineItem(-2, y, 2, y, self)\n\n        # prediction marker\n        self.dot = Continuous2DMovableDotItem(\n            self.DOT_RADIUS, scale, offset, values[0], values[-1], y_start, y_stop)\n        self.dot.tooltip_labels = labels\n        self.dot.tooltip_values = values\n        self.dot.setParentItem(self)\n        h_line = QGraphicsLineItem(values[0] * scale + offset, self.y_diff / 2,\n                                   values[-1] * scale + offset, self.y_diff / 2,\n                                   self)\n        pen = QPen(Qt.DashLine)\n        pen.setBrush(QColor(Qt.red))\n        h_line.setPen(pen)\n        self.dot.horizontal_line = h_line\n\n        # pylint: disable=unused-variable\n        # line\n        line = QGraphicsLineItem(values[0] * scale + offset, y_start,\n                                 values[-1] * scale + offset, y_stop,\n                                 self)\n\n        # ticks\n        for value in values:\n            diff_ = np.nan_to_num(values[-1] - values[0])\n            k = (value - values[0]) / diff_ if diff_ else 0\n            y_tick = (y_stop - y_start) * k + y_start - self.tick_height / 2\n            x_tick = value * scale - self.tick_width / 2 + offset\n            tick = QGraphicsRectItem(\n                x_tick, y_tick, self.tick_width, self.tick_height,\n                self)\n            tick.setBrush(QColor(Qt.black))\n\n        # rect\n        rect = QGraphicsRectItem(\n            values[0] * scale + offset, -self.y_diff * 0.125,\n            values[-1] * scale + offset, self.y_diff * 1.25,\n            self)\n        pen = QPen(Qt.DotLine)\n        pen.setBrush(QColor(50, 150, 200, 255))\n        rect.setPen(pen)\n        self.setPreferredSize(self.preferredWidth(), self.y_diff * 1.5)\n\n\nclass NomogramItem(QGraphicsWidget):\n    def __init__(self):\n        super().__init__()\n        self._items = []\n        self.setLayout(QGraphicsLinearLayout(Qt.Vertical))\n\n    def add_items(self, items):\n        self._items = items\n        for item in items:\n            self.layout().addItem(item)\n\n\nclass OWNomogram(OWWidget):\n    name = \"Nomogram\"\n    description = \" Nomograms for Visualization of Naive Bayesian\" \\\n                  \" and Logistic Regression Classifiers.\"\n    icon = \"icons/Nomogram.svg\"\n    priority = 2000\n    keywords = []\n\n    class Inputs:\n        classifier = Input(\"Classifier\", Model)\n        data = Input(\"Data\", Table)\n\n    class Outputs:\n        features = Output(\"Features\", AttributeList)\n\n    MAX_N_ATTRS = 1000\n    POINT_SCALE = 0\n    ALIGN_LEFT = 0\n    ALIGN_ZERO = 1\n    ACCEPTABLE = (NaiveBayesModel, LogisticRegressionClassifier)\n    settingsHandler = ClassValuesContextHandler()\n    target_class_index = ContextSetting(0)\n    normalize_probabilities = Setting(False)\n    scale = Setting(1)\n    display_index = Setting(1)\n    n_attributes = Setting(10)\n    sort_index = Setting(SortBy.ABSOLUTE)\n    cont_feature_dim_index = Setting(0)\n\n    graph_name = \"scene\"\n\n    class Error(OWWidget.Error):\n        invalid_classifier = Msg(\"Nomogram accepts only Naive Bayes and \"\n                                 \"Logistic Regression classifiers.\")\n\n    def __init__(self):\n        super().__init__()\n        self.instances = None\n        self.domain = None\n        self.data = None\n        self.classifier = None\n        self.align = OWNomogram.ALIGN_ZERO\n        self.log_odds_ratios = []\n        self.log_reg_coeffs = []\n        self.log_reg_coeffs_orig = []\n        self.log_reg_cont_data_extremes = []\n        self.p = None\n        self.b0 = None\n        self.points = []\n        self.feature_items = {}\n        self.feature_marker_values = []\n        self.scale_marker_values = lambda x: x\n        self.nomogram_main = None\n        self.vertical_line = None\n        self.hidden_vertical_line = None\n        self.old_target_class_index = self.target_class_index\n        self.repaint = False\n\n        self.info.set_input_summary(self.info.NoInput)\n\n        # GUI\n        box = gui.vBox(self.controlArea, \"Target class\")\n        self.class_combo = gui.comboBox(\n            box, self, \"target_class_index\", callback=self._class_combo_changed,\n            contentsLength=12, searchable=True)\n        self.norm_check = gui.checkBox(\n            box, self, \"normalize_probabilities\", \"Normalize probabilities\",\n            hidden=True, callback=self.update_scene,\n            tooltip=\"For multiclass data 1 vs. all probabilities do not\"\n                    \" sum to 1 and therefore could be normalized.\")\n\n        self.scale_radio = gui.radioButtons(\n            self.controlArea, self, \"scale\", [\"Point scale\", \"Log odds ratios\"],\n            box=\"Scale\", callback=self.update_scene)\n\n        box = gui.vBox(self.controlArea, \"Display features\")\n        grid = QGridLayout()\n        radio_group = gui.radioButtonsInBox(\n            box, self, \"display_index\", [], orientation=grid,\n            callback=self.update_scene)\n        radio_all = gui.appendRadioButton(\n            radio_group, \"All\", addToLayout=False)\n        radio_best = gui.appendRadioButton(\n            radio_group, \"Best ranked:\", addToLayout=False)\n        spin_box = gui.hBox(None, margin=0)\n        self.n_spin = gui.spin(\n            spin_box, self, \"n_attributes\", 1, self.MAX_N_ATTRS, label=\" \",\n            controlWidth=60, callback=self._n_spin_changed)\n        grid.addWidget(radio_all, 1, 1)\n        grid.addWidget(radio_best, 2, 1)\n        grid.addWidget(spin_box, 2, 2)\n\n        self.sort_combo = gui.comboBox(\n            box, self, \"sort_index\", label=\"Rank by:\", items=SortBy.items(),\n            orientation=Qt.Horizontal, callback=self.update_scene)\n\n        self.cont_feature_dim_combo = gui.comboBox(\n            box, self, \"cont_feature_dim_index\", label=\"Numeric features: \",\n            items=[\"1D projection\", \"2D curve\"], orientation=Qt.Horizontal,\n            callback=self.update_scene)\n\n        gui.rubber(self.controlArea)\n\n        class _GraphicsView(QGraphicsView):\n            def __init__(self, scene, parent, **kwargs):\n                for k, v in dict(verticalScrollBarPolicy=Qt.ScrollBarAlwaysOff,\n                                 horizontalScrollBarPolicy=Qt.ScrollBarAlwaysOff,\n                                 viewportUpdateMode=QGraphicsView.BoundingRectViewportUpdate,\n                                 renderHints=(QPainter.Antialiasing |\n                                              QPainter.TextAntialiasing |\n                                              QPainter.SmoothPixmapTransform),\n                                 alignment=(Qt.AlignTop |\n                                            Qt.AlignLeft),\n                                 sizePolicy=QSizePolicy(QSizePolicy.MinimumExpanding,\n                                                        QSizePolicy.MinimumExpanding)).items():\n                    kwargs.setdefault(k, v)\n\n                super().__init__(scene, parent, **kwargs)\n\n        class GraphicsView(_GraphicsView):\n            def __init__(self, scene, parent):\n                super().__init__(scene, parent,\n                                 verticalScrollBarPolicy=Qt.ScrollBarAlwaysOn,\n                                 styleSheet='QGraphicsView {background: white}')\n                self.viewport().setMinimumWidth(300)  # XXX: This prevents some tests failing\n                self._is_resizing = False\n\n            w = self\n\n            def resizeEvent(self, resizeEvent):\n                # Recompute main scene on window width change\n                if resizeEvent.size().width() != resizeEvent.oldSize().width():\n                    self._is_resizing = True\n                    self.w.update_scene()\n                    self._is_resizing = False\n                return super().resizeEvent(resizeEvent)\n\n            def is_resizing(self):\n                return self._is_resizing\n\n            def sizeHint(self):\n                return QSize(400, 200)\n\n        class FixedSizeGraphicsView(_GraphicsView):\n            def __init__(self, scene, parent):\n                super().__init__(scene, parent,\n                                 sizePolicy=QSizePolicy(QSizePolicy.MinimumExpanding,\n                                                        QSizePolicy.Minimum))\n\n            def sizeHint(self):\n                return QSize(400, 85)\n\n        scene = self.scene = QGraphicsScene(self)\n\n        top_view = self.top_view = FixedSizeGraphicsView(scene, self)\n        mid_view = self.view = GraphicsView(scene, self)\n        bottom_view = self.bottom_view = FixedSizeGraphicsView(scene, self)\n\n        for view in (top_view, mid_view, bottom_view):\n            self.mainArea.layout().addWidget(view)\n\n        self.dot_animator = GraphicsColorAnimator(\n            self, 3000,\n            [\n                (0.9, DOT_COLOR),\n                (0.925, DOT_COLOR.lighter(115)),\n                (0.95, DOT_COLOR),\n                (0.975, DOT_COLOR.lighter(115)),\n                (1.0, DOT_COLOR)\n            ]\n        )\n\n    def _class_combo_changed(self):\n        with np.errstate(invalid='ignore'):\n            coeffs = [np.nan_to_num(p[self.target_class_index] /\n                                    p[self.old_target_class_index])\n                      for p in self.points]\n        points = [p[self.old_target_class_index] for p in self.points]\n        self.feature_marker_values = [\n            self.get_points_from_coeffs(v, c, p) for (v, c, p) in\n            zip(self.feature_marker_values, coeffs, points)]\n        self.feature_marker_values = np.asarray(self.feature_marker_values)\n        self.update_scene()\n        self.old_target_class_index = self.target_class_index\n\n    def _n_spin_changed(self):\n        self.display_index = 1\n        self.update_scene()\n\n    def update_controls(self):\n        self.class_combo.clear()\n        self.norm_check.setHidden(True)\n        self.cont_feature_dim_combo.setEnabled(True)\n        if self.domain is not None:\n            values = self.domain.class_vars[0].values\n            if values:\n                self.class_combo.addItems(values)\n                self.target_class_index = 0\n            if len(self.domain.attributes) > self.MAX_N_ATTRS:\n                self.display_index = 1\n            if len(self.domain.class_vars[0].values) > 2:\n                self.norm_check.setHidden(False)\n            if not self.domain.has_continuous_attributes():\n                self.cont_feature_dim_combo.setEnabled(False)\n                self.cont_feature_dim_index = 0\n        model = self.sort_combo.model()\n        item = model.item(SortBy.POSITIVE)\n        item.setFlags(item.flags() | Qt.ItemIsEnabled)\n        item = model.item(SortBy.NEGATIVE)\n        item.setFlags(item.flags() | Qt.ItemIsEnabled)\n        self.align = OWNomogram.ALIGN_ZERO\n        if self.classifier and isinstance(self.classifier,\n                                          LogisticRegressionClassifier):\n            self.align = OWNomogram.ALIGN_LEFT\n\n    @Inputs.data\n    def set_data(self, data):\n        self.instances = data\n        summary = len(data) if data else self.info.NoInput\n        details = format_summary_details(data) if data else \"\"\n        self.info.set_input_summary(summary, details)\n        self.feature_marker_values = []\n        self.set_feature_marker_values()\n        self.update_scene()\n\n    @Inputs.classifier\n    def set_classifier(self, classifier):\n        self.closeContext()\n        self.classifier = classifier\n        self.Error.clear()\n        if self.classifier and not isinstance(self.classifier, self.ACCEPTABLE):\n            self.Error.invalid_classifier()\n            self.classifier = None\n        self.domain = self.classifier.domain if self.classifier else None\n        self.data = None\n        self.calculate_log_odds_ratios()\n        self.calculate_log_reg_coefficients()\n        self.update_controls()\n        self.openContext(self.domain.class_var if self.domain is not None\n                         else None)\n        self.points = self.log_odds_ratios or self.log_reg_coeffs\n        self.feature_marker_values = []\n        self.old_target_class_index = self.target_class_index\n        self.update_scene()\n\n    def calculate_log_odds_ratios(self):\n        self.log_odds_ratios = []\n        self.p = None\n        if self.classifier is None or self.domain is None:\n            return\n        if not isinstance(self.classifier, NaiveBayesModel):\n            return\n\n        log_cont_prob = self.classifier.log_cont_prob\n        class_prob = self.classifier.class_prob\n        for i in range(len(self.domain.attributes)):\n            ca = np.exp(log_cont_prob[i]) * class_prob[:, None]\n            _or = (ca / (1 - ca)) / (class_prob / (1 - class_prob))[:, None]\n            self.log_odds_ratios.append(np.log(_or))\n        self.p = class_prob\n\n    def calculate_log_reg_coefficients(self):\n        self.log_reg_coeffs = []\n        self.log_reg_cont_data_extremes = []\n        self.b0 = None\n        if self.classifier is None or self.domain is None:\n            return\n        if not isinstance(self.classifier, LogisticRegressionClassifier):\n            return\n\n        self.domain = self.reconstruct_domain(self.classifier.original_domain,\n                                              self.domain)\n        self.data = self.classifier.original_data.transform(self.domain)\n        attrs, ranges, start = self.domain.attributes, [], 0\n        for attr in attrs:\n            stop = start + len(attr.values) if attr.is_discrete else start + 1\n            ranges.append(slice(start, stop))\n            start = stop\n\n        self.b0 = self.classifier.intercept\n        coeffs = self.classifier.coefficients\n        if len(self.domain.class_var.values) == 2:\n            self.b0 = np.hstack((self.b0 * (-1), self.b0))\n            coeffs = np.vstack((coeffs * (-1), coeffs))\n        self.log_reg_coeffs = [coeffs[:, r] for r in ranges]\n        self.log_reg_coeffs_orig = self.log_reg_coeffs.copy()\n\n        min_values = nanmin(self.data.X, axis=0)\n        max_values = nanmax(self.data.X, axis=0)\n\n        for i, min_t, max_t in zip(range(len(self.log_reg_coeffs)),\n                                   min_values, max_values):\n            if self.log_reg_coeffs[i].shape[1] == 1:\n                coef = self.log_reg_coeffs[i]\n                self.log_reg_coeffs[i] = np.hstack((coef * min_t, coef * max_t))\n                self.log_reg_cont_data_extremes.append(\n                    [sorted([min_t, max_t], reverse=(c < 0)) for c in coef.flat])\n            else:\n                self.log_reg_cont_data_extremes.append([None])\n\n    def update_scene(self):\n        self.clear_scene()\n        if self.domain is None or not len(self.points[0]):\n            self.Outputs.features.send(None)\n            return\n\n        n_attrs = self.n_attributes if self.display_index else int(1e10)\n        attr_inds, attributes = zip(*self.get_ordered_attributes()[:n_attrs])\n        self.Outputs.features.send(AttributeList(attributes))\n\n        name_items = [QGraphicsTextItem(attr.name) for attr in attributes]\n        point_text = QGraphicsTextItem(\"Points\")\n        probs_text = QGraphicsTextItem(\"Probabilities (%)\")\n        all_items = name_items + [point_text, probs_text]\n        name_offset = -max(t.boundingRect().width() for t in all_items) - 10\n        w = self.view.viewport().rect().width()\n        max_width = w + name_offset - 30\n\n        points = [self.points[i][self.target_class_index]\n                  for i in attr_inds]\n        if self.align == OWNomogram.ALIGN_LEFT:\n            points = [p - p.min() for p in points]\n        max_ = np.nan_to_num(max(max(abs(p)) for p in points))\n        d = 100 / max_ if max_ else 1\n        minimums = [p[self.target_class_index].min() for p in self.points]\n        if self.scale == OWNomogram.POINT_SCALE:\n            points = [p * d for p in points]\n\n            if self.align == OWNomogram.ALIGN_LEFT:\n                self.scale_marker_values = lambda x: (x - minimums) * d\n            else:\n                self.scale_marker_values = lambda x: x * d\n        else:\n            if self.align == OWNomogram.ALIGN_LEFT:\n                self.scale_marker_values = lambda x: x - minimums\n            else:\n                self.scale_marker_values = lambda x: x\n\n        point_item, nomogram_head = self.create_main_nomogram(\n            attributes, attr_inds,\n            name_items, points, max_width, point_text, name_offset)\n        probs_item, nomogram_foot = self.create_footer_nomogram(\n            probs_text, d, minimums, max_width, name_offset)\n        for item in self.feature_items.values():\n            item.dot.point_dot = point_item.dot\n            item.dot.probs_dot = probs_item.dot\n            item.dot.vertical_line = self.hidden_vertical_line\n\n        self.dot_animator.setGraphicsItems(\n            [item.dot for item in self.feature_items.values()]\n        )\n        self.dot_animator.start()\n\n        self.nomogram = nomogram = NomogramItem()\n        nomogram.add_items([nomogram_head, self.nomogram_main, nomogram_foot])\n        self.scene.addItem(nomogram)\n\n        self.set_feature_marker_values()\n\n        rect = QRectF(self.scene.itemsBoundingRect().x(),\n                      self.scene.itemsBoundingRect().y(),\n                      self.scene.itemsBoundingRect().width(),\n                      self.nomogram.preferredSize().height()).adjusted(10, 0, 20, 0)\n        self.scene.setSceneRect(rect)\n\n        # Clip top and bottom (60 and 150) parts from the main view\n        self.view.setSceneRect(rect.x(), rect.y() + 80, rect.width() - 10, rect.height() - 160)\n        self.view.viewport().setMaximumHeight(rect.height() - 160)\n        # Clip main part from top/bottom views\n        # below point values are imprecise (less/more than required) but this\n        # is not a problem due to clipped scene content still being drawn\n        self.top_view.setSceneRect(rect.x(), rect.y() + 3, rect.width() - 10, 20)\n        self.bottom_view.setSceneRect(rect.x(), rect.height() - 110, rect.width() - 10, 30)\n\n    @staticmethod\n    def _adjust_scale(attributes, points, max_width, diff, attr_inds,\n                      log_reg_cont_data_extremes, cls_index):\n        if not diff:\n            return max_width\n\n        def offset(name, point):\n            text_ = QGraphicsTextItem(name).boundingRect()\n            return scale * point + text_.width() / 2\n\n        lr = log_reg_cont_data_extremes\n        scale = max_width / diff\n        names = list(chain.from_iterable(\n            [_get_labels(a, lr and lr[i] and lr[i][0] and lr[i][cls_index],\n                         OWNomogram.get_ruler_values(p.min(), p.max(),\n                                                     scale * p.ptp(), False))\n             for i, a, p in zip(attr_inds, attributes, points)]))\n        points = list(chain.from_iterable(points))\n\n        old_scale = scale + 1\n        while old_scale > scale:\n            old_scale = scale\n            offsets = [offset(n, p) for n, p in zip(names, points)]\n            most_right_name = names[np.argmax(offsets)]\n            text = QGraphicsTextItem(most_right_name).boundingRect()\n            scale = (max_width - text.width() / 2) / diff\n        return scale\n\n    def create_main_nomogram(self, attributes, attr_inds, name_items, points,\n                             max_width, point_text, name_offset):\n        cls_index = self.target_class_index\n        min_p = min(p.min() for p in points)\n        max_p = max(p.max() for p in points)\n        values = self.get_ruler_values(min_p, max_p, max_width)\n        min_p, max_p = min(values), max(values)\n        diff_ = np.nan_to_num(max_p - min_p)\n        scale_x = self._adjust_scale(\n            attributes, points, max_width, diff_, attr_inds,\n            self.log_reg_cont_data_extremes, cls_index\n        )\n\n        nomogram_header = NomogramItem()\n        point_item = RulerItem(point_text, values, scale_x, name_offset,\n                               - scale_x * min_p)\n        point_item.setPreferredSize(point_item.preferredWidth(), 35)\n        nomogram_header.add_items([point_item])\n\n        self.nomogram_main = NomogramItem()\n        cont_feature_item_class = ContinuousFeature2DItem if \\\n            self.cont_feature_dim_index else ContinuousFeatureItem\n\n        feature_items = [\n            DiscreteFeatureItem(\n                name_item, attr, point,\n                scale_x, name_offset, - scale_x * min_p)\n            if attr.is_discrete else\n            cont_feature_item_class(\n                name_item, attr, self.log_reg_cont_data_extremes[i][cls_index],\n                self.get_ruler_values(\n                    point.min(), point.max(),\n                    scale_x * point.ptp(), False),\n                scale_x, name_offset, - scale_x * min_p)\n            for i, attr, name_item, point in\n            zip(attr_inds, attributes, name_items, points)]\n\n        self.nomogram_main.add_items(feature_items)\n        self.feature_items = OrderedDict(sorted(zip(attr_inds, feature_items)))\n\n        x = - scale_x * min_p\n        y = self.nomogram_main.layout().preferredHeight() + 10\n        self.vertical_line = QGraphicsLineItem(x, -6, x, y)\n        self.vertical_line.setPen(QPen(Qt.DotLine))\n        self.vertical_line.setParentItem(point_item)\n        self.hidden_vertical_line = QGraphicsLineItem(x, -6, x, y)\n        pen = QPen(Qt.DashLine)\n        pen.setBrush(QColor(Qt.red))\n        self.hidden_vertical_line.setPen(pen)\n        self.hidden_vertical_line.setParentItem(point_item)\n\n        return point_item, nomogram_header\n\n    def get_ordered_attributes(self):\n        \"\"\"Return (in_domain_index, attr) pairs, ordered by method in SortBy combo\"\"\"\n        if self.domain is None or not self.domain.attributes:\n            return []\n\n        attrs = self.domain.attributes\n        sort_by = self.sort_index\n        class_value = self.target_class_index\n\n        if sort_by == SortBy.NO_SORTING:\n            return list(enumerate(attrs))\n\n        elif sort_by == SortBy.NAME:\n\n            def key(x):\n                _, attr = x\n                return attr.name.lower()\n\n        elif sort_by == SortBy.ABSOLUTE:\n\n            def key(x):\n                i, attr = x\n                if attr.is_discrete:\n                    ptp = self.points[i][class_value].ptp()\n                else:\n                    coef = np.abs(self.log_reg_coeffs_orig[i][class_value]).mean()\n                    ptp = coef * np.ptp(self.log_reg_cont_data_extremes[i][class_value])\n                return -ptp\n\n        elif sort_by == SortBy.POSITIVE:\n\n            def key(x):\n                i, attr = x\n                max_value = (self.points[i][class_value].max()\n                             if attr.is_discrete else\n                             np.mean(self.log_reg_cont_data_extremes[i][class_value]))\n                return -max_value\n\n        elif sort_by == SortBy.NEGATIVE:\n\n            def key(x):\n                i, attr = x\n                min_value = (self.points[i][class_value].min()\n                             if attr.is_discrete else\n                             np.mean(self.log_reg_cont_data_extremes[i][class_value]))\n                return min_value\n\n        return sorted(enumerate(attrs), key=key)\n\n\n    def create_footer_nomogram(self, probs_text, d, minimums,\n                               max_width, name_offset):\n        # pylint: disable=invalid-unary-operand-type\n        eps, d_ = 0.05, 1\n        k = - np.log(self.p / (1 - self.p)) if self.p is not None else - self.b0\n        min_sum = k[self.target_class_index] - np.log((1 - eps) / eps)\n        max_sum = k[self.target_class_index] - np.log(eps / (1 - eps))\n        if self.align == OWNomogram.ALIGN_LEFT:\n            max_sum = max_sum - sum(minimums)\n            min_sum = min_sum - sum(minimums)\n            for i in range(len(k)):  # pylint: disable=consider-using-enumerate\n                k[i] = k[i] - sum([min(q) for q in [p[i] for p in self.points]])\n        if self.scale == OWNomogram.POINT_SCALE:\n            min_sum *= d\n            max_sum *= d\n            d_ = d\n\n        values = self.get_ruler_values(min_sum, max_sum, max_width)\n        min_sum, max_sum = min(values), max(values)\n        diff_ = np.nan_to_num(max_sum - min_sum)\n        scale_x = max_width / diff_ if diff_ else max_width\n        cls_var, cls_index = self.domain.class_var, self.target_class_index\n        nomogram_footer = NomogramItem()\n\n        def get_normalized_probabilities(val):\n            if not self.normalize_probabilities:\n                return 1 / (1 + np.exp(k[cls_index] - val / d_))\n            totals = self.__get_totals_for_class_values(minimums)\n            p_sum = np.sum(1 / (1 + np.exp(k - totals / d_)))\n            return 1 / (1 + np.exp(k[cls_index] - val / d_)) / p_sum\n\n        def get_points(prob):\n            if not self.normalize_probabilities:\n                return (k[cls_index] - np.log(1 / prob - 1)) * d_\n            totals = self.__get_totals_for_class_values(minimums)\n            p_sum = np.sum(1 / (1 + np.exp(k - totals / d_)))\n            return (k[cls_index] - np.log(1 / (prob * p_sum) - 1)) * d_\n\n        probs_item = ProbabilitiesRulerItem(\n            probs_text, values, scale_x, name_offset, - scale_x * min_sum,\n            get_points=get_points,\n            title=\"{}='{}'\".format(cls_var.name, cls_var.values[cls_index]),\n            get_probabilities=get_normalized_probabilities)\n        nomogram_footer.add_items([probs_item])\n        return probs_item, nomogram_footer\n\n    def __get_totals_for_class_values(self, minimums):\n        cls_index = self.target_class_index\n        marker_values = self.scale_marker_values(self.feature_marker_values)\n        totals = np.full(len(self.domain.class_var.values), np.nan)\n        totals[cls_index] = marker_values.sum()\n        for i in range(len(self.domain.class_var.values)):\n            if i == cls_index:\n                continue\n            coeffs = [np.nan_to_num(p[i] / p[cls_index]) for p in self.points]\n            points = [p[cls_index] for p in self.points]\n            total = sum([self.get_points_from_coeffs(v, c, p) for (v, c, p)\n                         in zip(self.feature_marker_values, coeffs, points)])\n            if self.align == OWNomogram.ALIGN_LEFT:\n                points = [p - m for m, p in zip(minimums, points)]\n                total -= sum([min(p) for p in [p[i] for p in self.points]])\n            d = 100 / max(max(abs(p)) for p in points)\n            if self.scale == OWNomogram.POINT_SCALE:\n                total *= d\n            totals[i] = total\n        assert not np.any(np.isnan(totals))\n        return totals\n\n    def set_feature_marker_values(self):\n        if not (len(self.points) and len(self.feature_items)):\n            return\n        if not len(self.feature_marker_values):\n            self._init_feature_marker_values()\n        marker_values = self.scale_marker_values(self.feature_marker_values)\n\n        invisible_sum = 0\n        for i, marker in enumerate(marker_values):\n            try:\n                item = self.feature_items[i]\n            except KeyError:\n                invisible_sum += marker\n            else:\n                item.dot.move_to_val(marker)\n\n        item.dot.probs_dot.move_to_sum(invisible_sum)\n\n    def _init_feature_marker_values(self):\n        self.feature_marker_values = []\n        cls_index = self.target_class_index\n        instances = self.instances.transform(self.domain) \\\n            if self.instances else None\n        values = []\n        for i, attr in enumerate(self.domain.attributes):\n            value, feature_val = 0, None\n            if len(self.log_reg_coeffs):\n                if attr.is_discrete:\n                    ind, n = unique(self.data.X[:, i], return_counts=True)\n                    feature_val = np.nan_to_num(ind[np.argmax(n)])\n                else:\n                    feature_val = nanmean(self.data.X[:, i])\n\n            # If data is provided on a separate signal, use the first data\n            # instance to position the points instead of the mean\n            inst_in_dom = instances and attr in instances.domain\n            if inst_in_dom and not np.isnan(instances[0][attr]):\n                feature_val = instances[0][attr]\n\n            if feature_val is not None:\n                value = (self.points[i][cls_index][int(feature_val)]\n                         if attr.is_discrete else\n                         self.log_reg_coeffs_orig[i][cls_index][0] * feature_val)\n            values.append(value)\n        self.feature_marker_values = np.asarray(values)\n\n    def clear_scene(self):\n        self.feature_items = {}\n        self.scale_marker_values = lambda x: x\n        self.nomogram = None\n        self.nomogram_main = None\n        self.vertical_line = None\n        self.hidden_vertical_line = None\n        self.dot_animator.clear()\n        self.scene.clear()\n\n    def send_report(self):\n        self.report_plot()\n\n    @staticmethod\n    def reconstruct_domain(original, preprocessed):\n        # abuse dict to make \"in\" comparisons faster\n        attrs = OrderedDict()\n        for attr in preprocessed.attributes:\n            cv = attr._compute_value.variable._compute_value\n            var = cv.variable if cv else original[attr.name]\n            var = original[var.name] if var.name in original else attr\n            if var in attrs:    # the reason for OrderedDict\n                continue\n            attrs[var] = None   # we only need keys\n        attrs = list(attrs.keys())\n        return Domain(attrs, original.class_var, original.metas)\n\n    @staticmethod\n    def get_ruler_values(start, stop, max_width, round_to_nearest=True):\n        if max_width == 0:\n            return [0]\n        diff = np.nan_to_num((stop - start) / max_width)\n        if diff <= 0:\n            return [0]\n        decimals = int(np.floor(np.log10(diff)))\n        if diff > 4 * pow(10, decimals):\n            step = 5 * pow(10, decimals + 2)\n        elif diff > 2 * pow(10, decimals):\n            step = 2 * pow(10, decimals + 2)\n        elif diff > 1 * pow(10, decimals):\n            step = 1 * pow(10, decimals + 2)\n        else:\n            step = 5 * pow(10, decimals + 1)\n        round_by = int(- np.floor(np.log10(step)))\n        r = start % step\n        if not round_to_nearest:\n            _range = np.arange(start + step, stop + r, step) - r\n            start, stop = np.floor(start * 100) / 100, np.ceil(stop * 100) / 100\n            return np.round(np.hstack((start, _range, stop)), 2)\n        return np.round(np.arange(start, stop + r + step, step) - r, round_by)\n\n    @staticmethod\n    def get_points_from_coeffs(current_value, coefficients, possible_values):\n        if np.isnan(possible_values).any():\n            return 0\n        # pylint: disable=undefined-loop-variable\n        indices = np.argsort(possible_values)\n        sorted_values = possible_values[indices]\n        sorted_coefficients = coefficients[indices]\n        for i, val in enumerate(sorted_values):\n            if current_value < val:\n                break\n        diff = sorted_values[i] - sorted_values[i - 1]\n        k = 0 if diff < 1e-6 else (sorted_values[i] - current_value) / \\\n                                  (sorted_values[i] - sorted_values[i - 1])\n        return sorted_coefficients[i - 1] * sorted_values[i - 1] * k + \\\n               sorted_coefficients[i] * sorted_values[i] * (1 - k)\n\n    def reset_settings(self):\n        with warnings.catch_warnings():\n            # setting target_class_index will trigger this innocent warning\n            warnings.filterwarnings(\n                \"ignore\", \"combo box 'target_class_index' is empty\")\n            self._reset_settings()\n        self.update_scene()\n\n\nif __name__ == \"__main__\":  # pragma: no cover\n    from Orange.classification import NaiveBayesLearner  #, LogisticRegressionLearner\n    data = Table(\"heart_disease\")\n    clf = NaiveBayesLearner()(data)\n    # clf = LogisticRegressionLearner()(data)\n    WidgetPreview(OWNomogram).run(set_classifier=clf, set_data=data)\n",
            "header": {
                "file": {
                    "path": "Orange/widgets/visualize/ownomogram.py"
                },
                "misc_line": null
            },
            "index_line": {
                "old_commit_hash": "b1a34112d",
                "new_commit_hash": "faaa631d6",
                "mode": "100644"
            },
            "is_binary_file": false,
            "binary_line": null,
            "minus_file": {
                "path": "a/Orange/widgets/visualize/ownomogram.py"
            },
            "plus_file": {
                "path": "b/Orange/widgets/visualize/ownomogram.py"
            },
            "hunks": [
                {
                    "descriptor": {
                        "old_range": {
                            "start": 1285,
                            "length": 6
                        },
                        "new_range": {
                            "start": 1285,
                            "length": 7
                        },
                        "section": "class OWNomogram(OWWidget):"
                    },
                    "line_group": {
                        "all_lines": [
                            {
                                "content": "        for attr in preprocessed.attributes:",
                                "type": "context"
                            },
                            {
                                "content": "            cv = attr._compute_value.variable._compute_value",
                                "type": "context"
                            },
                            {
                                "content": "            var = cv.variable if cv else original[attr.name]",
                                "type": "context"
                            },
                            {
                                "content": "            var = original[var.name] if var.name in original else attr",
                                "type": "added"
                            },
                            {
                                "content": "            if var in attrs:    # the reason for OrderedDict",
                                "type": "context"
                            },
                            {
                                "content": "                continue",
                                "type": "context"
                            },
                            {
                                "content": "            attrs[var] = None   # we only need keys",
                                "type": "context"
                            }
                        ]
                    },
                    "modified_entities": [
                        {
                            "file_name": "Orange/widgets/visualize/ownomogram.py",
                            "type": "method",
                            "name": "OWNomogram.reconstruct_domain",
                            "content": "class OWNomogram(OWWidget):\n    name = \"Nomogram\"\n    description = \" Nomograms for Visualization of Naive Bayesian\" \\\n                  \" and Logistic Regression Classifiers.\"\n    icon = \"icons/Nomogram.svg\"\n    priority = 2000\n    keywords = []\n\n    class Inputs:\n        classifier = Input(\"Classifier\", Model)\n        data = Input(\"Data\", Table)\n\n    class Outputs:\n        features = Output(\"Features\", AttributeList)\n\n    MAX_N_ATTRS = 1000\n    POINT_SCALE = 0\n    ALIGN_LEFT = 0\n    ALIGN_ZERO = 1\n    ACCEPTABLE = (NaiveBayesModel, LogisticRegressionClassifier)\n    settingsHandler = ClassValuesContextHandler()\n    target_class_index = ContextSetting(0)\n    normalize_probabilities = Setting(False)\n    scale = Setting(1)\n    display_index = Setting(1)\n    n_attributes = Setting(10)\n    sort_index = Setting(SortBy.ABSOLUTE)\n    cont_feature_dim_index = Setting(0)\n\n    graph_name = \"scene\"\n\n    class Error(OWWidget.Error):\n        invalid_classifier = Msg(\"Nomogram accepts only Naive Bayes and \"\n                                 \"Logistic Regression classifiers.\")\n\n    def __init__(self):\n        super().__init__()\n        self.instances = None\n        self.domain = None\n        self.data = None\n        self.classifier = None\n        self.align = OWNomogram.ALIGN_ZERO\n        self.log_odds_ratios = []\n        self.log_reg_coeffs = []\n        self.log_reg_coeffs_orig = []\n        self.log_reg_cont_data_extremes = []\n        self.p = None\n        self.b0 = None\n        self.points = []\n        self.feature_items = {}\n        self.feature_marker_values = []\n        self.scale_marker_values = lambda x: x\n        self.nomogram_main = None\n        self.vertical_line = None\n        self.hidden_vertical_line = None\n        self.old_target_class_index = self.target_class_index\n        self.repaint = False\n\n        self.info.set_input_summary(self.info.NoInput)\n\n        # GUI\n        box = gui.vBox(self.controlArea, \"Target class\")\n        self.class_combo = gui.comboBox(\n            box, self, \"target_class_index\", callback=self._class_combo_changed,\n            contentsLength=12, searchable=True)\n        self.norm_check = gui.checkBox(\n            box, self, \"normalize_probabilities\", \"Normalize probabilities\",\n            hidden=True, callback=self.update_scene,\n            tooltip=\"For multiclass data 1 vs. all probabilities do not\"\n                    \" sum to 1 and therefore could be normalized.\")\n\n        self.scale_radio = gui.radioButtons(\n            self.controlArea, self, \"scale\", [\"Point scale\", \"Log odds ratios\"],\n            box=\"Scale\", callback=self.update_scene)\n\n        box = gui.vBox(self.controlArea, \"Display features\")\n        grid = QGridLayout()\n        radio_group = gui.radioButtonsInBox(\n            box, self, \"display_index\", [], orientation=grid,\n            callback=self.update_scene)\n        radio_all = gui.appendRadioButton(\n            radio_group, \"All\", addToLayout=False)\n        radio_best = gui.appendRadioButton(\n            radio_group, \"Best ranked:\", addToLayout=False)\n        spin_box = gui.hBox(None, margin=0)\n        self.n_spin = gui.spin(\n            spin_box, self, \"n_attributes\", 1, self.MAX_N_ATTRS, label=\" \",\n            controlWidth=60, callback=self._n_spin_changed)\n        grid.addWidget(radio_all, 1, 1)\n        grid.addWidget(radio_best, 2, 1)\n        grid.addWidget(spin_box, 2, 2)\n\n        self.sort_combo = gui.comboBox(\n            box, self, \"sort_index\", label=\"Rank by:\", items=SortBy.items(),\n            orientation=Qt.Horizontal, callback=self.update_scene)\n\n        self.cont_feature_dim_combo = gui.comboBox(\n            box, self, \"cont_feature_dim_index\", label=\"Numeric features: \",\n            items=[\"1D projection\", \"2D curve\"], orientation=Qt.Horizontal,\n            callback=self.update_scene)\n\n        gui.rubber(self.controlArea)\n\n        class _GraphicsView(QGraphicsView):\n            def __init__(self, scene, parent, **kwargs):\n                for k, v in dict(verticalScrollBarPolicy=Qt.ScrollBarAlwaysOff,\n                                 horizontalScrollBarPolicy=Qt.ScrollBarAlwaysOff,\n                                 viewportUpdateMode=QGraphicsView.BoundingRectViewportUpdate,\n                                 renderHints=(QPainter.Antialiasing |\n                                              QPainter.TextAntialiasing |\n                                              QPainter.SmoothPixmapTransform),\n                                 alignment=(Qt.AlignTop |\n                                            Qt.AlignLeft),\n                                 sizePolicy=QSizePolicy(QSizePolicy.MinimumExpanding,\n                                                        QSizePolicy.MinimumExpanding)).items():\n                    kwargs.setdefault(k, v)\n\n                super().__init__(scene, parent, **kwargs)\n\n        class GraphicsView(_GraphicsView):\n            def __init__(self, scene, parent):\n                super().__init__(scene, parent,\n                                 verticalScrollBarPolicy=Qt.ScrollBarAlwaysOn,\n                                 styleSheet='QGraphicsView {background: white}')\n                self.viewport().setMinimumWidth(300)  # XXX: This prevents some tests failing\n                self._is_resizing = False\n\n            w = self\n\n            def resizeEvent(self, resizeEvent):\n                # Recompute main scene on window width change\n                if resizeEvent.size().width() != resizeEvent.oldSize().width():\n                    self._is_resizing = True\n                    self.w.update_scene()\n                    self._is_resizing = False\n                return super().resizeEvent(resizeEvent)\n\n            def is_resizing(self):\n                return self._is_resizing\n\n            def sizeHint(self):\n                return QSize(400, 200)\n\n        class FixedSizeGraphicsView(_GraphicsView):\n            def __init__(self, scene, parent):\n                super().__init__(scene, parent,\n                                 sizePolicy=QSizePolicy(QSizePolicy.MinimumExpanding,\n                                                        QSizePolicy.Minimum))\n\n            def sizeHint(self):\n                return QSize(400, 85)\n\n        scene = self.scene = QGraphicsScene(self)\n\n        top_view = self.top_view = FixedSizeGraphicsView(scene, self)\n        mid_view = self.view = GraphicsView(scene, self)\n        bottom_view = self.bottom_view = FixedSizeGraphicsView(scene, self)\n\n        for view in (top_view, mid_view, bottom_view):\n            self.mainArea.layout().addWidget(view)\n\n        self.dot_animator = GraphicsColorAnimator(\n            self, 3000,\n            [\n                (0.9, DOT_COLOR),\n                (0.925, DOT_COLOR.lighter(115)),\n                (0.95, DOT_COLOR),\n                (0.975, DOT_COLOR.lighter(115)),\n                (1.0, DOT_COLOR)\n            ]\n        )\n\n    def _class_combo_changed(self):\n        with np.errstate(invalid='ignore'):\n            coeffs = [np.nan_to_num(p[self.target_class_index] /\n                                    p[self.old_target_class_index])\n                      for p in self.points]\n        points = [p[self.old_target_class_index] for p in self.points]\n        self.feature_marker_values = [\n            self.get_points_from_coeffs(v, c, p) for (v, c, p) in\n            zip(self.feature_marker_values, coeffs, points)]\n        self.feature_marker_values = np.asarray(self.feature_marker_values)\n        self.update_scene()\n        self.old_target_class_index = self.target_class_index\n\n    def _n_spin_changed(self):\n        self.display_index = 1\n        self.update_scene()\n\n    def update_controls(self):\n        self.class_combo.clear()\n        self.norm_check.setHidden(True)\n        self.cont_feature_dim_combo.setEnabled(True)\n        if self.domain is not None:\n            values = self.domain.class_vars[0].values\n            if values:\n                self.class_combo.addItems(values)\n                self.target_class_index = 0\n            if len(self.domain.attributes) > self.MAX_N_ATTRS:\n                self.display_index = 1\n            if len(self.domain.class_vars[0].values) > 2:\n                self.norm_check.setHidden(False)\n            if not self.domain.has_continuous_attributes():\n                self.cont_feature_dim_combo.setEnabled(False)\n                self.cont_feature_dim_index = 0\n        model = self.sort_combo.model()\n        item = model.item(SortBy.POSITIVE)\n        item.setFlags(item.flags() | Qt.ItemIsEnabled)\n        item = model.item(SortBy.NEGATIVE)\n        item.setFlags(item.flags() | Qt.ItemIsEnabled)\n        self.align = OWNomogram.ALIGN_ZERO\n        if self.classifier and isinstance(self.classifier,\n                                          LogisticRegressionClassifier):\n            self.align = OWNomogram.ALIGN_LEFT\n\n    @Inputs.data\n    def set_data(self, data):\n        self.instances = data\n        summary = len(data) if data else self.info.NoInput\n        details = format_summary_details(data) if data else \"\"\n        self.info.set_input_summary(summary, details)\n        self.feature_marker_values = []\n        self.set_feature_marker_values()\n        self.update_scene()\n\n    @Inputs.classifier\n    def set_classifier(self, classifier):\n        self.closeContext()\n        self.classifier = classifier\n        self.Error.clear()\n        if self.classifier and not isinstance(self.classifier, self.ACCEPTABLE):\n            self.Error.invalid_classifier()\n            self.classifier = None\n        self.domain = self.classifier.domain if self.classifier else None\n        self.data = None\n        self.calculate_log_odds_ratios()\n        self.calculate_log_reg_coefficients()\n        self.update_controls()\n        self.openContext(self.domain.class_var if self.domain is not None\n                         else None)\n        self.points = self.log_odds_ratios or self.log_reg_coeffs\n        self.feature_marker_values = []\n        self.old_target_class_index = self.target_class_index\n        self.update_scene()\n\n    def calculate_log_odds_ratios(self):\n        self.log_odds_ratios = []\n        self.p = None\n        if self.classifier is None or self.domain is None:\n            return\n        if not isinstance(self.classifier, NaiveBayesModel):\n            return\n\n        log_cont_prob = self.classifier.log_cont_prob\n        class_prob = self.classifier.class_prob\n        for i in range(len(self.domain.attributes)):\n            ca = np.exp(log_cont_prob[i]) * class_prob[:, None]\n            _or = (ca / (1 - ca)) / (class_prob / (1 - class_prob))[:, None]\n            self.log_odds_ratios.append(np.log(_or))\n        self.p = class_prob\n\n    def calculate_log_reg_coefficients(self):\n        self.log_reg_coeffs = []\n        self.log_reg_cont_data_extremes = []\n        self.b0 = None\n        if self.classifier is None or self.domain is None:\n            return\n        if not isinstance(self.classifier, LogisticRegressionClassifier):\n            return\n\n        self.domain = self.reconstruct_domain(self.classifier.original_domain,\n                                              self.domain)\n        self.data = self.classifier.original_data.transform(self.domain)\n        attrs, ranges, start = self.domain.attributes, [], 0\n        for attr in attrs:\n            stop = start + len(attr.values) if attr.is_discrete else start + 1\n            ranges.append(slice(start, stop))\n            start = stop\n\n        self.b0 = self.classifier.intercept\n        coeffs = self.classifier.coefficients\n        if len(self.domain.class_var.values) == 2:\n            self.b0 = np.hstack((self.b0 * (-1), self.b0))\n            coeffs = np.vstack((coeffs * (-1), coeffs))\n        self.log_reg_coeffs = [coeffs[:, r] for r in ranges]\n        self.log_reg_coeffs_orig = self.log_reg_coeffs.copy()\n\n        min_values = nanmin(self.data.X, axis=0)\n        max_values = nanmax(self.data.X, axis=0)\n\n        for i, min_t, max_t in zip(range(len(self.log_reg_coeffs)),\n                                   min_values, max_values):\n            if self.log_reg_coeffs[i].shape[1] == 1:\n                coef = self.log_reg_coeffs[i]\n                self.log_reg_coeffs[i] = np.hstack((coef * min_t, coef * max_t))\n                self.log_reg_cont_data_extremes.append(\n                    [sorted([min_t, max_t], reverse=(c < 0)) for c in coef.flat])\n            else:\n                self.log_reg_cont_data_extremes.append([None])\n\n    def update_scene(self):\n        self.clear_scene()\n        if self.domain is None or not len(self.points[0]):\n            self.Outputs.features.send(None)\n            return\n\n        n_attrs = self.n_attributes if self.display_index else int(1e10)\n        attr_inds, attributes = zip(*self.get_ordered_attributes()[:n_attrs])\n        self.Outputs.features.send(AttributeList(attributes))\n\n        name_items = [QGraphicsTextItem(attr.name) for attr in attributes]\n        point_text = QGraphicsTextItem(\"Points\")\n        probs_text = QGraphicsTextItem(\"Probabilities (%)\")\n        all_items = name_items + [point_text, probs_text]\n        name_offset = -max(t.boundingRect().width() for t in all_items) - 10\n        w = self.view.viewport().rect().width()\n        max_width = w + name_offset - 30\n\n        points = [self.points[i][self.target_class_index]\n                  for i in attr_inds]\n        if self.align == OWNomogram.ALIGN_LEFT:\n            points = [p - p.min() for p in points]\n        max_ = np.nan_to_num(max(max(abs(p)) for p in points))\n        d = 100 / max_ if max_ else 1\n        minimums = [p[self.target_class_index].min() for p in self.points]\n        if self.scale == OWNomogram.POINT_SCALE:\n            points = [p * d for p in points]\n\n            if self.align == OWNomogram.ALIGN_LEFT:\n                self.scale_marker_values = lambda x: (x - minimums) * d\n            else:\n                self.scale_marker_values = lambda x: x * d\n        else:\n            if self.align == OWNomogram.ALIGN_LEFT:\n                self.scale_marker_values = lambda x: x - minimums\n            else:\n                self.scale_marker_values = lambda x: x\n\n        point_item, nomogram_head = self.create_main_nomogram(\n            attributes, attr_inds,\n            name_items, points, max_width, point_text, name_offset)\n        probs_item, nomogram_foot = self.create_footer_nomogram(\n            probs_text, d, minimums, max_width, name_offset)\n        for item in self.feature_items.values():\n            item.dot.point_dot = point_item.dot\n            item.dot.probs_dot = probs_item.dot\n            item.dot.vertical_line = self.hidden_vertical_line\n\n        self.dot_animator.setGraphicsItems(\n            [item.dot for item in self.feature_items.values()]\n        )\n        self.dot_animator.start()\n\n        self.nomogram = nomogram = NomogramItem()\n        nomogram.add_items([nomogram_head, self.nomogram_main, nomogram_foot])\n        self.scene.addItem(nomogram)\n\n        self.set_feature_marker_values()\n\n        rect = QRectF(self.scene.itemsBoundingRect().x(),\n                      self.scene.itemsBoundingRect().y(),\n                      self.scene.itemsBoundingRect().width(),\n                      self.nomogram.preferredSize().height()).adjusted(10, 0, 20, 0)\n        self.scene.setSceneRect(rect)\n\n        # Clip top and bottom (60 and 150) parts from the main view\n        self.view.setSceneRect(rect.x(), rect.y() + 80, rect.width() - 10, rect.height() - 160)\n        self.view.viewport().setMaximumHeight(rect.height() - 160)\n        # Clip main part from top/bottom views\n        # below point values are imprecise (less/more than required) but this\n        # is not a problem due to clipped scene content still being drawn\n        self.top_view.setSceneRect(rect.x(), rect.y() + 3, rect.width() - 10, 20)\n        self.bottom_view.setSceneRect(rect.x(), rect.height() - 110, rect.width() - 10, 30)\n\n    @staticmethod\n    def _adjust_scale(attributes, points, max_width, diff, attr_inds,\n                      log_reg_cont_data_extremes, cls_index):\n        if not diff:\n            return max_width\n\n        def offset(name, point):\n            text_ = QGraphicsTextItem(name).boundingRect()\n            return scale * point + text_.width() / 2\n\n        lr = log_reg_cont_data_extremes\n        scale = max_width / diff\n        names = list(chain.from_iterable(\n            [_get_labels(a, lr and lr[i] and lr[i][0] and lr[i][cls_index],\n                         OWNomogram.get_ruler_values(p.min(), p.max(),\n                                                     scale * p.ptp(), False))\n             for i, a, p in zip(attr_inds, attributes, points)]))\n        points = list(chain.from_iterable(points))\n\n        old_scale = scale + 1\n        while old_scale > scale:\n            old_scale = scale\n            offsets = [offset(n, p) for n, p in zip(names, points)]\n            most_right_name = names[np.argmax(offsets)]\n            text = QGraphicsTextItem(most_right_name).boundingRect()\n            scale = (max_width - text.width() / 2) / diff\n        return scale\n\n    def create_main_nomogram(self, attributes, attr_inds, name_items, points,\n                             max_width, point_text, name_offset):\n        cls_index = self.target_class_index\n        min_p = min(p.min() for p in points)\n        max_p = max(p.max() for p in points)\n        values = self.get_ruler_values(min_p, max_p, max_width)\n        min_p, max_p = min(values), max(values)\n        diff_ = np.nan_to_num(max_p - min_p)\n        scale_x = self._adjust_scale(\n            attributes, points, max_width, diff_, attr_inds,\n            self.log_reg_cont_data_extremes, cls_index\n        )\n\n        nomogram_header = NomogramItem()\n        point_item = RulerItem(point_text, values, scale_x, name_offset,\n                               - scale_x * min_p)\n        point_item.setPreferredSize(point_item.preferredWidth(), 35)\n        nomogram_header.add_items([point_item])\n\n        self.nomogram_main = NomogramItem()\n        cont_feature_item_class = ContinuousFeature2DItem if \\\n            self.cont_feature_dim_index else ContinuousFeatureItem\n\n        feature_items = [\n            DiscreteFeatureItem(\n                name_item, attr, point,\n                scale_x, name_offset, - scale_x * min_p)\n            if attr.is_discrete else\n            cont_feature_item_class(\n                name_item, attr, self.log_reg_cont_data_extremes[i][cls_index],\n                self.get_ruler_values(\n                    point.min(), point.max(),\n                    scale_x * point.ptp(), False),\n                scale_x, name_offset, - scale_x * min_p)\n            for i, attr, name_item, point in\n            zip(attr_inds, attributes, name_items, points)]\n\n        self.nomogram_main.add_items(feature_items)\n        self.feature_items = OrderedDict(sorted(zip(attr_inds, feature_items)))\n\n        x = - scale_x * min_p\n        y = self.nomogram_main.layout().preferredHeight() + 10\n        self.vertical_line = QGraphicsLineItem(x, -6, x, y)\n        self.vertical_line.setPen(QPen(Qt.DotLine))\n        self.vertical_line.setParentItem(point_item)\n        self.hidden_vertical_line = QGraphicsLineItem(x, -6, x, y)\n        pen = QPen(Qt.DashLine)\n        pen.setBrush(QColor(Qt.red))\n        self.hidden_vertical_line.setPen(pen)\n        self.hidden_vertical_line.setParentItem(point_item)\n\n        return point_item, nomogram_header\n\n    def get_ordered_attributes(self):\n        \"\"\"Return (in_domain_index, attr) pairs, ordered by method in SortBy combo\"\"\"\n        if self.domain is None or not self.domain.attributes:\n            return []\n\n        attrs = self.domain.attributes\n        sort_by = self.sort_index\n        class_value = self.target_class_index\n\n        if sort_by == SortBy.NO_SORTING:\n            return list(enumerate(attrs))\n\n        elif sort_by == SortBy.NAME:\n\n            def key(x):\n                _, attr = x\n                return attr.name.lower()\n\n        elif sort_by == SortBy.ABSOLUTE:\n\n            def key(x):\n                i, attr = x\n                if attr.is_discrete:\n                    ptp = self.points[i][class_value].ptp()\n                else:\n                    coef = np.abs(self.log_reg_coeffs_orig[i][class_value]).mean()\n                    ptp = coef * np.ptp(self.log_reg_cont_data_extremes[i][class_value])\n                return -ptp\n\n        elif sort_by == SortBy.POSITIVE:\n\n            def key(x):\n                i, attr = x\n                max_value = (self.points[i][class_value].max()\n                             if attr.is_discrete else\n                             np.mean(self.log_reg_cont_data_extremes[i][class_value]))\n                return -max_value\n\n        elif sort_by == SortBy.NEGATIVE:\n\n            def key(x):\n                i, attr = x\n                min_value = (self.points[i][class_value].min()\n                             if attr.is_discrete else\n                             np.mean(self.log_reg_cont_data_extremes[i][class_value]))\n                return min_value\n\n        return sorted(enumerate(attrs), key=key)\n\n\n    def create_footer_nomogram(self, probs_text, d, minimums,\n                               max_width, name_offset):\n        # pylint: disable=invalid-unary-operand-type\n        eps, d_ = 0.05, 1\n        k = - np.log(self.p / (1 - self.p)) if self.p is not None else - self.b0\n        min_sum = k[self.target_class_index] - np.log((1 - eps) / eps)\n        max_sum = k[self.target_class_index] - np.log(eps / (1 - eps))\n        if self.align == OWNomogram.ALIGN_LEFT:\n            max_sum = max_sum - sum(minimums)\n            min_sum = min_sum - sum(minimums)\n            for i in range(len(k)):  # pylint: disable=consider-using-enumerate\n                k[i] = k[i] - sum([min(q) for q in [p[i] for p in self.points]])\n        if self.scale == OWNomogram.POINT_SCALE:\n            min_sum *= d\n            max_sum *= d\n            d_ = d\n\n        values = self.get_ruler_values(min_sum, max_sum, max_width)\n        min_sum, max_sum = min(values), max(values)\n        diff_ = np.nan_to_num(max_sum - min_sum)\n        scale_x = max_width / diff_ if diff_ else max_width\n        cls_var, cls_index = self.domain.class_var, self.target_class_index\n        nomogram_footer = NomogramItem()\n\n        def get_normalized_probabilities(val):\n            if not self.normalize_probabilities:\n                return 1 / (1 + np.exp(k[cls_index] - val / d_))\n            totals = self.__get_totals_for_class_values(minimums)\n            p_sum = np.sum(1 / (1 + np.exp(k - totals / d_)))\n            return 1 / (1 + np.exp(k[cls_index] - val / d_)) / p_sum\n\n        def get_points(prob):\n            if not self.normalize_probabilities:\n                return (k[cls_index] - np.log(1 / prob - 1)) * d_\n            totals = self.__get_totals_for_class_values(minimums)\n            p_sum = np.sum(1 / (1 + np.exp(k - totals / d_)))\n            return (k[cls_index] - np.log(1 / (prob * p_sum) - 1)) * d_\n\n        probs_item = ProbabilitiesRulerItem(\n            probs_text, values, scale_x, name_offset, - scale_x * min_sum,\n            get_points=get_points,\n            title=\"{}='{}'\".format(cls_var.name, cls_var.values[cls_index]),\n            get_probabilities=get_normalized_probabilities)\n        nomogram_footer.add_items([probs_item])\n        return probs_item, nomogram_footer\n\n    def __get_totals_for_class_values(self, minimums):\n        cls_index = self.target_class_index\n        marker_values = self.scale_marker_values(self.feature_marker_values)\n        totals = np.full(len(self.domain.class_var.values), np.nan)\n        totals[cls_index] = marker_values.sum()\n        for i in range(len(self.domain.class_var.values)):\n            if i == cls_index:\n                continue\n            coeffs = [np.nan_to_num(p[i] / p[cls_index]) for p in self.points]\n            points = [p[cls_index] for p in self.points]\n            total = sum([self.get_points_from_coeffs(v, c, p) for (v, c, p)\n                         in zip(self.feature_marker_values, coeffs, points)])\n            if self.align == OWNomogram.ALIGN_LEFT:\n                points = [p - m for m, p in zip(minimums, points)]\n                total -= sum([min(p) for p in [p[i] for p in self.points]])\n            d = 100 / max(max(abs(p)) for p in points)\n            if self.scale == OWNomogram.POINT_SCALE:\n                total *= d\n            totals[i] = total\n        assert not np.any(np.isnan(totals))\n        return totals\n\n    def set_feature_marker_values(self):\n        if not (len(self.points) and len(self.feature_items)):\n            return\n        if not len(self.feature_marker_values):\n            self._init_feature_marker_values()\n        marker_values = self.scale_marker_values(self.feature_marker_values)\n\n        invisible_sum = 0\n        for i, marker in enumerate(marker_values):\n            try:\n                item = self.feature_items[i]\n            except KeyError:\n                invisible_sum += marker\n            else:\n                item.dot.move_to_val(marker)\n\n        item.dot.probs_dot.move_to_sum(invisible_sum)\n\n    def _init_feature_marker_values(self):\n        self.feature_marker_values = []\n        cls_index = self.target_class_index\n        instances = self.instances.transform(self.domain) \\\n            if self.instances else None\n        values = []\n        for i, attr in enumerate(self.domain.attributes):\n            value, feature_val = 0, None\n            if len(self.log_reg_coeffs):\n                if attr.is_discrete:\n                    ind, n = unique(self.data.X[:, i], return_counts=True)\n                    feature_val = np.nan_to_num(ind[np.argmax(n)])\n                else:\n                    feature_val = nanmean(self.data.X[:, i])\n\n            # If data is provided on a separate signal, use the first data\n            # instance to position the points instead of the mean\n            inst_in_dom = instances and attr in instances.domain\n            if inst_in_dom and not np.isnan(instances[0][attr]):\n                feature_val = instances[0][attr]\n\n            if feature_val is not None:\n                value = (self.points[i][cls_index][int(feature_val)]\n                         if attr.is_discrete else\n                         self.log_reg_coeffs_orig[i][cls_index][0] * feature_val)\n            values.append(value)\n        self.feature_marker_values = np.asarray(values)\n\n    def clear_scene(self):\n        self.feature_items = {}\n        self.scale_marker_values = lambda x: x\n        self.nomogram = None\n        self.nomogram_main = None\n        self.vertical_line = None\n        self.hidden_vertical_line = None\n        self.dot_animator.clear()\n        self.scene.clear()\n\n    def send_report(self):\n        self.report_plot()\n\n    @staticmethod\n    def reconstruct_domain(original, preprocessed):\n        # abuse dict to make \"in\" comparisons faster\n        attrs = OrderedDict()\n        for attr in preprocessed.attributes:\n            cv = attr._compute_value.variable._compute_value\n            var = cv.variable if cv else original[attr.name]\n            var = original[var.name] if var.name in original else attr\n            if var in attrs:    # the reason for OrderedDict\n                continue\n            attrs[var] = None   # we only need keys\n        attrs = list(attrs.keys())\n        return Domain(attrs, original.class_var, original.metas)\n\n    @staticmethod\n    def get_ruler_values(start, stop, max_width, round_to_nearest=True):\n        if max_width == 0:\n            return [0]\n        diff = np.nan_to_num((stop - start) / max_width)\n        if diff <= 0:\n            return [0]\n        decimals = int(np.floor(np.log10(diff)))\n        if diff > 4 * pow(10, decimals):\n            step = 5 * pow(10, decimals + 2)\n        elif diff > 2 * pow(10, decimals):\n            step = 2 * pow(10, decimals + 2)\n        elif diff > 1 * pow(10, decimals):\n            step = 1 * pow(10, decimals + 2)\n        else:\n            step = 5 * pow(10, decimals + 1)\n        round_by = int(- np.floor(np.log10(step)))\n        r = start % step\n        if not round_to_nearest:\n            _range = np.arange(start + step, stop + r, step) - r\n            start, stop = np.floor(start * 100) / 100, np.ceil(stop * 100) / 100\n            return np.round(np.hstack((start, _range, stop)), 2)\n        return np.round(np.arange(start, stop + r + step, step) - r, round_by)\n\n    @staticmethod\n    def get_points_from_coeffs(current_value, coefficients, possible_values):\n        if np.isnan(possible_values).any():\n            return 0\n        # pylint: disable=undefined-loop-variable\n        indices = np.argsort(possible_values)\n        sorted_values = possible_values[indices]\n        sorted_coefficients = coefficients[indices]\n        for i, val in enumerate(sorted_values):\n            if current_value < val:\n                break\n        diff = sorted_values[i] - sorted_values[i - 1]\n        k = 0 if diff < 1e-6 else (sorted_values[i] - current_value) / \\\n                                  (sorted_values[i] - sorted_values[i - 1])\n        return sorted_coefficients[i - 1] * sorted_values[i - 1] * k + \\\n               sorted_coefficients[i] * sorted_values[i] * (1 - k)\n\n    def reset_settings(self):\n        with warnings.catch_warnings():\n            # setting target_class_index will trigger this innocent warning\n            warnings.filterwarnings(\n                \"ignore\", \"combo box 'target_class_index' is empty\")\n            self._reset_settings()\n        self.update_scene()",
                            "ast_type_str": "ClassDef",
                            "start_lineno": 1282,
                            "end_lineno": 1293,
                            "parent": {
                                "file_name": "Orange/widgets/visualize/ownomogram.py",
                                "type": "class",
                                "name": "OWNomogram",
                                "content": "class OWNomogram(OWWidget):\n    name = \"Nomogram\"\n    description = \" Nomograms for Visualization of Naive Bayesian\" \\\n                  \" and Logistic Regression Classifiers.\"\n    icon = \"icons/Nomogram.svg\"\n    priority = 2000\n    keywords = []\n\n    class Inputs:\n        classifier = Input(\"Classifier\", Model)\n        data = Input(\"Data\", Table)\n\n    class Outputs:\n        features = Output(\"Features\", AttributeList)\n\n    MAX_N_ATTRS = 1000\n    POINT_SCALE = 0\n    ALIGN_LEFT = 0\n    ALIGN_ZERO = 1\n    ACCEPTABLE = (NaiveBayesModel, LogisticRegressionClassifier)\n    settingsHandler = ClassValuesContextHandler()\n    target_class_index = ContextSetting(0)\n    normalize_probabilities = Setting(False)\n    scale = Setting(1)\n    display_index = Setting(1)\n    n_attributes = Setting(10)\n    sort_index = Setting(SortBy.ABSOLUTE)\n    cont_feature_dim_index = Setting(0)\n\n    graph_name = \"scene\"\n\n    class Error(OWWidget.Error):\n        invalid_classifier = Msg(\"Nomogram accepts only Naive Bayes and \"\n                                 \"Logistic Regression classifiers.\")\n\n    def __init__(self):\n        super().__init__()\n        self.instances = None\n        self.domain = None\n        self.data = None\n        self.classifier = None\n        self.align = OWNomogram.ALIGN_ZERO\n        self.log_odds_ratios = []\n        self.log_reg_coeffs = []\n        self.log_reg_coeffs_orig = []\n        self.log_reg_cont_data_extremes = []\n        self.p = None\n        self.b0 = None\n        self.points = []\n        self.feature_items = {}\n        self.feature_marker_values = []\n        self.scale_marker_values = lambda x: x\n        self.nomogram_main = None\n        self.vertical_line = None\n        self.hidden_vertical_line = None\n        self.old_target_class_index = self.target_class_index\n        self.repaint = False\n\n        self.info.set_input_summary(self.info.NoInput)\n\n        # GUI\n        box = gui.vBox(self.controlArea, \"Target class\")\n        self.class_combo = gui.comboBox(\n            box, self, \"target_class_index\", callback=self._class_combo_changed,\n            contentsLength=12, searchable=True)\n        self.norm_check = gui.checkBox(\n            box, self, \"normalize_probabilities\", \"Normalize probabilities\",\n            hidden=True, callback=self.update_scene,\n            tooltip=\"For multiclass data 1 vs. all probabilities do not\"\n                    \" sum to 1 and therefore could be normalized.\")\n\n        self.scale_radio = gui.radioButtons(\n            self.controlArea, self, \"scale\", [\"Point scale\", \"Log odds ratios\"],\n            box=\"Scale\", callback=self.update_scene)\n\n        box = gui.vBox(self.controlArea, \"Display features\")\n        grid = QGridLayout()\n        radio_group = gui.radioButtonsInBox(\n            box, self, \"display_index\", [], orientation=grid,\n            callback=self.update_scene)\n        radio_all = gui.appendRadioButton(\n            radio_group, \"All\", addToLayout=False)\n        radio_best = gui.appendRadioButton(\n            radio_group, \"Best ranked:\", addToLayout=False)\n        spin_box = gui.hBox(None, margin=0)\n        self.n_spin = gui.spin(\n            spin_box, self, \"n_attributes\", 1, self.MAX_N_ATTRS, label=\" \",\n            controlWidth=60, callback=self._n_spin_changed)\n        grid.addWidget(radio_all, 1, 1)\n        grid.addWidget(radio_best, 2, 1)\n        grid.addWidget(spin_box, 2, 2)\n\n        self.sort_combo = gui.comboBox(\n            box, self, \"sort_index\", label=\"Rank by:\", items=SortBy.items(),\n            orientation=Qt.Horizontal, callback=self.update_scene)\n\n        self.cont_feature_dim_combo = gui.comboBox(\n            box, self, \"cont_feature_dim_index\", label=\"Numeric features: \",\n            items=[\"1D projection\", \"2D curve\"], orientation=Qt.Horizontal,\n            callback=self.update_scene)\n\n        gui.rubber(self.controlArea)\n\n        class _GraphicsView(QGraphicsView):\n            def __init__(self, scene, parent, **kwargs):\n                for k, v in dict(verticalScrollBarPolicy=Qt.ScrollBarAlwaysOff,\n                                 horizontalScrollBarPolicy=Qt.ScrollBarAlwaysOff,\n                                 viewportUpdateMode=QGraphicsView.BoundingRectViewportUpdate,\n                                 renderHints=(QPainter.Antialiasing |\n                                              QPainter.TextAntialiasing |\n                                              QPainter.SmoothPixmapTransform),\n                                 alignment=(Qt.AlignTop |\n                                            Qt.AlignLeft),\n                                 sizePolicy=QSizePolicy(QSizePolicy.MinimumExpanding,\n                                                        QSizePolicy.MinimumExpanding)).items():\n                    kwargs.setdefault(k, v)\n\n                super().__init__(scene, parent, **kwargs)\n\n        class GraphicsView(_GraphicsView):\n            def __init__(self, scene, parent):\n                super().__init__(scene, parent,\n                                 verticalScrollBarPolicy=Qt.ScrollBarAlwaysOn,\n                                 styleSheet='QGraphicsView {background: white}')\n                self.viewport().setMinimumWidth(300)  # XXX: This prevents some tests failing\n                self._is_resizing = False\n\n            w = self\n\n            def resizeEvent(self, resizeEvent):\n                # Recompute main scene on window width change\n                if resizeEvent.size().width() != resizeEvent.oldSize().width():\n                    self._is_resizing = True\n                    self.w.update_scene()\n                    self._is_resizing = False\n                return super().resizeEvent(resizeEvent)\n\n            def is_resizing(self):\n                return self._is_resizing\n\n            def sizeHint(self):\n                return QSize(400, 200)\n\n        class FixedSizeGraphicsView(_GraphicsView):\n            def __init__(self, scene, parent):\n                super().__init__(scene, parent,\n                                 sizePolicy=QSizePolicy(QSizePolicy.MinimumExpanding,\n                                                        QSizePolicy.Minimum))\n\n            def sizeHint(self):\n                return QSize(400, 85)\n\n        scene = self.scene = QGraphicsScene(self)\n\n        top_view = self.top_view = FixedSizeGraphicsView(scene, self)\n        mid_view = self.view = GraphicsView(scene, self)\n        bottom_view = self.bottom_view = FixedSizeGraphicsView(scene, self)\n\n        for view in (top_view, mid_view, bottom_view):\n            self.mainArea.layout().addWidget(view)\n\n        self.dot_animator = GraphicsColorAnimator(\n            self, 3000,\n            [\n                (0.9, DOT_COLOR),\n                (0.925, DOT_COLOR.lighter(115)),\n                (0.95, DOT_COLOR),\n                (0.975, DOT_COLOR.lighter(115)),\n                (1.0, DOT_COLOR)\n            ]\n        )\n\n    def _class_combo_changed(self):\n        with np.errstate(invalid='ignore'):\n            coeffs = [np.nan_to_num(p[self.target_class_index] /\n                                    p[self.old_target_class_index])\n                      for p in self.points]\n        points = [p[self.old_target_class_index] for p in self.points]\n        self.feature_marker_values = [\n            self.get_points_from_coeffs(v, c, p) for (v, c, p) in\n            zip(self.feature_marker_values, coeffs, points)]\n        self.feature_marker_values = np.asarray(self.feature_marker_values)\n        self.update_scene()\n        self.old_target_class_index = self.target_class_index\n\n    def _n_spin_changed(self):\n        self.display_index = 1\n        self.update_scene()\n\n    def update_controls(self):\n        self.class_combo.clear()\n        self.norm_check.setHidden(True)\n        self.cont_feature_dim_combo.setEnabled(True)\n        if self.domain is not None:\n            values = self.domain.class_vars[0].values\n            if values:\n                self.class_combo.addItems(values)\n                self.target_class_index = 0\n            if len(self.domain.attributes) > self.MAX_N_ATTRS:\n                self.display_index = 1\n            if len(self.domain.class_vars[0].values) > 2:\n                self.norm_check.setHidden(False)\n            if not self.domain.has_continuous_attributes():\n                self.cont_feature_dim_combo.setEnabled(False)\n                self.cont_feature_dim_index = 0\n        model = self.sort_combo.model()\n        item = model.item(SortBy.POSITIVE)\n        item.setFlags(item.flags() | Qt.ItemIsEnabled)\n        item = model.item(SortBy.NEGATIVE)\n        item.setFlags(item.flags() | Qt.ItemIsEnabled)\n        self.align = OWNomogram.ALIGN_ZERO\n        if self.classifier and isinstance(self.classifier,\n                                          LogisticRegressionClassifier):\n            self.align = OWNomogram.ALIGN_LEFT\n\n    @Inputs.data\n    def set_data(self, data):\n        self.instances = data\n        summary = len(data) if data else self.info.NoInput\n        details = format_summary_details(data) if data else \"\"\n        self.info.set_input_summary(summary, details)\n        self.feature_marker_values = []\n        self.set_feature_marker_values()\n        self.update_scene()\n\n    @Inputs.classifier\n    def set_classifier(self, classifier):\n        self.closeContext()\n        self.classifier = classifier\n        self.Error.clear()\n        if self.classifier and not isinstance(self.classifier, self.ACCEPTABLE):\n            self.Error.invalid_classifier()\n            self.classifier = None\n        self.domain = self.classifier.domain if self.classifier else None\n        self.data = None\n        self.calculate_log_odds_ratios()\n        self.calculate_log_reg_coefficients()\n        self.update_controls()\n        self.openContext(self.domain.class_var if self.domain is not None\n                         else None)\n        self.points = self.log_odds_ratios or self.log_reg_coeffs\n        self.feature_marker_values = []\n        self.old_target_class_index = self.target_class_index\n        self.update_scene()\n\n    def calculate_log_odds_ratios(self):\n        self.log_odds_ratios = []\n        self.p = None\n        if self.classifier is None or self.domain is None:\n            return\n        if not isinstance(self.classifier, NaiveBayesModel):\n            return\n\n        log_cont_prob = self.classifier.log_cont_prob\n        class_prob = self.classifier.class_prob\n        for i in range(len(self.domain.attributes)):\n            ca = np.exp(log_cont_prob[i]) * class_prob[:, None]\n            _or = (ca / (1 - ca)) / (class_prob / (1 - class_prob))[:, None]\n            self.log_odds_ratios.append(np.log(_or))\n        self.p = class_prob\n\n    def calculate_log_reg_coefficients(self):\n        self.log_reg_coeffs = []\n        self.log_reg_cont_data_extremes = []\n        self.b0 = None\n        if self.classifier is None or self.domain is None:\n            return\n        if not isinstance(self.classifier, LogisticRegressionClassifier):\n            return\n\n        self.domain = self.reconstruct_domain(self.classifier.original_domain,\n                                              self.domain)\n        self.data = self.classifier.original_data.transform(self.domain)\n        attrs, ranges, start = self.domain.attributes, [], 0\n        for attr in attrs:\n            stop = start + len(attr.values) if attr.is_discrete else start + 1\n            ranges.append(slice(start, stop))\n            start = stop\n\n        self.b0 = self.classifier.intercept\n        coeffs = self.classifier.coefficients\n        if len(self.domain.class_var.values) == 2:\n            self.b0 = np.hstack((self.b0 * (-1), self.b0))\n            coeffs = np.vstack((coeffs * (-1), coeffs))\n        self.log_reg_coeffs = [coeffs[:, r] for r in ranges]\n        self.log_reg_coeffs_orig = self.log_reg_coeffs.copy()\n\n        min_values = nanmin(self.data.X, axis=0)\n        max_values = nanmax(self.data.X, axis=0)\n\n        for i, min_t, max_t in zip(range(len(self.log_reg_coeffs)),\n                                   min_values, max_values):\n            if self.log_reg_coeffs[i].shape[1] == 1:\n                coef = self.log_reg_coeffs[i]\n                self.log_reg_coeffs[i] = np.hstack((coef * min_t, coef * max_t))\n                self.log_reg_cont_data_extremes.append(\n                    [sorted([min_t, max_t], reverse=(c < 0)) for c in coef.flat])\n            else:\n                self.log_reg_cont_data_extremes.append([None])\n\n    def update_scene(self):\n        self.clear_scene()\n        if self.domain is None or not len(self.points[0]):\n            self.Outputs.features.send(None)\n            return\n\n        n_attrs = self.n_attributes if self.display_index else int(1e10)\n        attr_inds, attributes = zip(*self.get_ordered_attributes()[:n_attrs])\n        self.Outputs.features.send(AttributeList(attributes))\n\n        name_items = [QGraphicsTextItem(attr.name) for attr in attributes]\n        point_text = QGraphicsTextItem(\"Points\")\n        probs_text = QGraphicsTextItem(\"Probabilities (%)\")\n        all_items = name_items + [point_text, probs_text]\n        name_offset = -max(t.boundingRect().width() for t in all_items) - 10\n        w = self.view.viewport().rect().width()\n        max_width = w + name_offset - 30\n\n        points = [self.points[i][self.target_class_index]\n                  for i in attr_inds]\n        if self.align == OWNomogram.ALIGN_LEFT:\n            points = [p - p.min() for p in points]\n        max_ = np.nan_to_num(max(max(abs(p)) for p in points))\n        d = 100 / max_ if max_ else 1\n        minimums = [p[self.target_class_index].min() for p in self.points]\n        if self.scale == OWNomogram.POINT_SCALE:\n            points = [p * d for p in points]\n\n            if self.align == OWNomogram.ALIGN_LEFT:\n                self.scale_marker_values = lambda x: (x - minimums) * d\n            else:\n                self.scale_marker_values = lambda x: x * d\n        else:\n            if self.align == OWNomogram.ALIGN_LEFT:\n                self.scale_marker_values = lambda x: x - minimums\n            else:\n                self.scale_marker_values = lambda x: x\n\n        point_item, nomogram_head = self.create_main_nomogram(\n            attributes, attr_inds,\n            name_items, points, max_width, point_text, name_offset)\n        probs_item, nomogram_foot = self.create_footer_nomogram(\n            probs_text, d, minimums, max_width, name_offset)\n        for item in self.feature_items.values():\n            item.dot.point_dot = point_item.dot\n            item.dot.probs_dot = probs_item.dot\n            item.dot.vertical_line = self.hidden_vertical_line\n\n        self.dot_animator.setGraphicsItems(\n            [item.dot for item in self.feature_items.values()]\n        )\n        self.dot_animator.start()\n\n        self.nomogram = nomogram = NomogramItem()\n        nomogram.add_items([nomogram_head, self.nomogram_main, nomogram_foot])\n        self.scene.addItem(nomogram)\n\n        self.set_feature_marker_values()\n\n        rect = QRectF(self.scene.itemsBoundingRect().x(),\n                      self.scene.itemsBoundingRect().y(),\n                      self.scene.itemsBoundingRect().width(),\n                      self.nomogram.preferredSize().height()).adjusted(10, 0, 20, 0)\n        self.scene.setSceneRect(rect)\n\n        # Clip top and bottom (60 and 150) parts from the main view\n        self.view.setSceneRect(rect.x(), rect.y() + 80, rect.width() - 10, rect.height() - 160)\n        self.view.viewport().setMaximumHeight(rect.height() - 160)\n        # Clip main part from top/bottom views\n        # below point values are imprecise (less/more than required) but this\n        # is not a problem due to clipped scene content still being drawn\n        self.top_view.setSceneRect(rect.x(), rect.y() + 3, rect.width() - 10, 20)\n        self.bottom_view.setSceneRect(rect.x(), rect.height() - 110, rect.width() - 10, 30)\n\n    @staticmethod\n    def _adjust_scale(attributes, points, max_width, diff, attr_inds,\n                      log_reg_cont_data_extremes, cls_index):\n        if not diff:\n            return max_width\n\n        def offset(name, point):\n            text_ = QGraphicsTextItem(name).boundingRect()\n            return scale * point + text_.width() / 2\n\n        lr = log_reg_cont_data_extremes\n        scale = max_width / diff\n        names = list(chain.from_iterable(\n            [_get_labels(a, lr and lr[i] and lr[i][0] and lr[i][cls_index],\n                         OWNomogram.get_ruler_values(p.min(), p.max(),\n                                                     scale * p.ptp(), False))\n             for i, a, p in zip(attr_inds, attributes, points)]))\n        points = list(chain.from_iterable(points))\n\n        old_scale = scale + 1\n        while old_scale > scale:\n            old_scale = scale\n            offsets = [offset(n, p) for n, p in zip(names, points)]\n            most_right_name = names[np.argmax(offsets)]\n            text = QGraphicsTextItem(most_right_name).boundingRect()\n            scale = (max_width - text.width() / 2) / diff\n        return scale\n\n    def create_main_nomogram(self, attributes, attr_inds, name_items, points,\n                             max_width, point_text, name_offset):\n        cls_index = self.target_class_index\n        min_p = min(p.min() for p in points)\n        max_p = max(p.max() for p in points)\n        values = self.get_ruler_values(min_p, max_p, max_width)\n        min_p, max_p = min(values), max(values)\n        diff_ = np.nan_to_num(max_p - min_p)\n        scale_x = self._adjust_scale(\n            attributes, points, max_width, diff_, attr_inds,\n            self.log_reg_cont_data_extremes, cls_index\n        )\n\n        nomogram_header = NomogramItem()\n        point_item = RulerItem(point_text, values, scale_x, name_offset,\n                               - scale_x * min_p)\n        point_item.setPreferredSize(point_item.preferredWidth(), 35)\n        nomogram_header.add_items([point_item])\n\n        self.nomogram_main = NomogramItem()\n        cont_feature_item_class = ContinuousFeature2DItem if \\\n            self.cont_feature_dim_index else ContinuousFeatureItem\n\n        feature_items = [\n            DiscreteFeatureItem(\n                name_item, attr, point,\n                scale_x, name_offset, - scale_x * min_p)\n            if attr.is_discrete else\n            cont_feature_item_class(\n                name_item, attr, self.log_reg_cont_data_extremes[i][cls_index],\n                self.get_ruler_values(\n                    point.min(), point.max(),\n                    scale_x * point.ptp(), False),\n                scale_x, name_offset, - scale_x * min_p)\n            for i, attr, name_item, point in\n            zip(attr_inds, attributes, name_items, points)]\n\n        self.nomogram_main.add_items(feature_items)\n        self.feature_items = OrderedDict(sorted(zip(attr_inds, feature_items)))\n\n        x = - scale_x * min_p\n        y = self.nomogram_main.layout().preferredHeight() + 10\n        self.vertical_line = QGraphicsLineItem(x, -6, x, y)\n        self.vertical_line.setPen(QPen(Qt.DotLine))\n        self.vertical_line.setParentItem(point_item)\n        self.hidden_vertical_line = QGraphicsLineItem(x, -6, x, y)\n        pen = QPen(Qt.DashLine)\n        pen.setBrush(QColor(Qt.red))\n        self.hidden_vertical_line.setPen(pen)\n        self.hidden_vertical_line.setParentItem(point_item)\n\n        return point_item, nomogram_header\n\n    def get_ordered_attributes(self):\n        \"\"\"Return (in_domain_index, attr) pairs, ordered by method in SortBy combo\"\"\"\n        if self.domain is None or not self.domain.attributes:\n            return []\n\n        attrs = self.domain.attributes\n        sort_by = self.sort_index\n        class_value = self.target_class_index\n\n        if sort_by == SortBy.NO_SORTING:\n            return list(enumerate(attrs))\n\n        elif sort_by == SortBy.NAME:\n\n            def key(x):\n                _, attr = x\n                return attr.name.lower()\n\n        elif sort_by == SortBy.ABSOLUTE:\n\n            def key(x):\n                i, attr = x\n                if attr.is_discrete:\n                    ptp = self.points[i][class_value].ptp()\n                else:\n                    coef = np.abs(self.log_reg_coeffs_orig[i][class_value]).mean()\n                    ptp = coef * np.ptp(self.log_reg_cont_data_extremes[i][class_value])\n                return -ptp\n\n        elif sort_by == SortBy.POSITIVE:\n\n            def key(x):\n                i, attr = x\n                max_value = (self.points[i][class_value].max()\n                             if attr.is_discrete else\n                             np.mean(self.log_reg_cont_data_extremes[i][class_value]))\n                return -max_value\n\n        elif sort_by == SortBy.NEGATIVE:\n\n            def key(x):\n                i, attr = x\n                min_value = (self.points[i][class_value].min()\n                             if attr.is_discrete else\n                             np.mean(self.log_reg_cont_data_extremes[i][class_value]))\n                return min_value\n\n        return sorted(enumerate(attrs), key=key)\n\n\n    def create_footer_nomogram(self, probs_text, d, minimums,\n                               max_width, name_offset):\n        # pylint: disable=invalid-unary-operand-type\n        eps, d_ = 0.05, 1\n        k = - np.log(self.p / (1 - self.p)) if self.p is not None else - self.b0\n        min_sum = k[self.target_class_index] - np.log((1 - eps) / eps)\n        max_sum = k[self.target_class_index] - np.log(eps / (1 - eps))\n        if self.align == OWNomogram.ALIGN_LEFT:\n            max_sum = max_sum - sum(minimums)\n            min_sum = min_sum - sum(minimums)\n            for i in range(len(k)):  # pylint: disable=consider-using-enumerate\n                k[i] = k[i] - sum([min(q) for q in [p[i] for p in self.points]])\n        if self.scale == OWNomogram.POINT_SCALE:\n            min_sum *= d\n            max_sum *= d\n            d_ = d\n\n        values = self.get_ruler_values(min_sum, max_sum, max_width)\n        min_sum, max_sum = min(values), max(values)\n        diff_ = np.nan_to_num(max_sum - min_sum)\n        scale_x = max_width / diff_ if diff_ else max_width\n        cls_var, cls_index = self.domain.class_var, self.target_class_index\n        nomogram_footer = NomogramItem()\n\n        def get_normalized_probabilities(val):\n            if not self.normalize_probabilities:\n                return 1 / (1 + np.exp(k[cls_index] - val / d_))\n            totals = self.__get_totals_for_class_values(minimums)\n            p_sum = np.sum(1 / (1 + np.exp(k - totals / d_)))\n            return 1 / (1 + np.exp(k[cls_index] - val / d_)) / p_sum\n\n        def get_points(prob):\n            if not self.normalize_probabilities:\n                return (k[cls_index] - np.log(1 / prob - 1)) * d_\n            totals = self.__get_totals_for_class_values(minimums)\n            p_sum = np.sum(1 / (1 + np.exp(k - totals / d_)))\n            return (k[cls_index] - np.log(1 / (prob * p_sum) - 1)) * d_\n\n        probs_item = ProbabilitiesRulerItem(\n            probs_text, values, scale_x, name_offset, - scale_x * min_sum,\n            get_points=get_points,\n            title=\"{}='{}'\".format(cls_var.name, cls_var.values[cls_index]),\n            get_probabilities=get_normalized_probabilities)\n        nomogram_footer.add_items([probs_item])\n        return probs_item, nomogram_footer\n\n    def __get_totals_for_class_values(self, minimums):\n        cls_index = self.target_class_index\n        marker_values = self.scale_marker_values(self.feature_marker_values)\n        totals = np.full(len(self.domain.class_var.values), np.nan)\n        totals[cls_index] = marker_values.sum()\n        for i in range(len(self.domain.class_var.values)):\n            if i == cls_index:\n                continue\n            coeffs = [np.nan_to_num(p[i] / p[cls_index]) for p in self.points]\n            points = [p[cls_index] for p in self.points]\n            total = sum([self.get_points_from_coeffs(v, c, p) for (v, c, p)\n                         in zip(self.feature_marker_values, coeffs, points)])\n            if self.align == OWNomogram.ALIGN_LEFT:\n                points = [p - m for m, p in zip(minimums, points)]\n                total -= sum([min(p) for p in [p[i] for p in self.points]])\n            d = 100 / max(max(abs(p)) for p in points)\n            if self.scale == OWNomogram.POINT_SCALE:\n                total *= d\n            totals[i] = total\n        assert not np.any(np.isnan(totals))\n        return totals\n\n    def set_feature_marker_values(self):\n        if not (len(self.points) and len(self.feature_items)):\n            return\n        if not len(self.feature_marker_values):\n            self._init_feature_marker_values()\n        marker_values = self.scale_marker_values(self.feature_marker_values)\n\n        invisible_sum = 0\n        for i, marker in enumerate(marker_values):\n            try:\n                item = self.feature_items[i]\n            except KeyError:\n                invisible_sum += marker\n            else:\n                item.dot.move_to_val(marker)\n\n        item.dot.probs_dot.move_to_sum(invisible_sum)\n\n    def _init_feature_marker_values(self):\n        self.feature_marker_values = []\n        cls_index = self.target_class_index\n        instances = self.instances.transform(self.domain) \\\n            if self.instances else None\n        values = []\n        for i, attr in enumerate(self.domain.attributes):\n            value, feature_val = 0, None\n            if len(self.log_reg_coeffs):\n                if attr.is_discrete:\n                    ind, n = unique(self.data.X[:, i], return_counts=True)\n                    feature_val = np.nan_to_num(ind[np.argmax(n)])\n                else:\n                    feature_val = nanmean(self.data.X[:, i])\n\n            # If data is provided on a separate signal, use the first data\n            # instance to position the points instead of the mean\n            inst_in_dom = instances and attr in instances.domain\n            if inst_in_dom and not np.isnan(instances[0][attr]):\n                feature_val = instances[0][attr]\n\n            if feature_val is not None:\n                value = (self.points[i][cls_index][int(feature_val)]\n                         if attr.is_discrete else\n                         self.log_reg_coeffs_orig[i][cls_index][0] * feature_val)\n            values.append(value)\n        self.feature_marker_values = np.asarray(values)\n\n    def clear_scene(self):\n        self.feature_items = {}\n        self.scale_marker_values = lambda x: x\n        self.nomogram = None\n        self.nomogram_main = None\n        self.vertical_line = None\n        self.hidden_vertical_line = None\n        self.dot_animator.clear()\n        self.scene.clear()\n\n    def send_report(self):\n        self.report_plot()\n\n    @staticmethod\n    def reconstruct_domain(original, preprocessed):\n        # abuse dict to make \"in\" comparisons faster\n        attrs = OrderedDict()\n        for attr in preprocessed.attributes:\n            cv = attr._compute_value.variable._compute_value\n            var = cv.variable if cv else original[attr.name]\n            var = original[var.name] if var.name in original else attr\n            if var in attrs:    # the reason for OrderedDict\n                continue\n            attrs[var] = None   # we only need keys\n        attrs = list(attrs.keys())\n        return Domain(attrs, original.class_var, original.metas)\n\n    @staticmethod\n    def get_ruler_values(start, stop, max_width, round_to_nearest=True):\n        if max_width == 0:\n            return [0]\n        diff = np.nan_to_num((stop - start) / max_width)\n        if diff <= 0:\n            return [0]\n        decimals = int(np.floor(np.log10(diff)))\n        if diff > 4 * pow(10, decimals):\n            step = 5 * pow(10, decimals + 2)\n        elif diff > 2 * pow(10, decimals):\n            step = 2 * pow(10, decimals + 2)\n        elif diff > 1 * pow(10, decimals):\n            step = 1 * pow(10, decimals + 2)\n        else:\n            step = 5 * pow(10, decimals + 1)\n        round_by = int(- np.floor(np.log10(step)))\n        r = start % step\n        if not round_to_nearest:\n            _range = np.arange(start + step, stop + r, step) - r\n            start, stop = np.floor(start * 100) / 100, np.ceil(stop * 100) / 100\n            return np.round(np.hstack((start, _range, stop)), 2)\n        return np.round(np.arange(start, stop + r + step, step) - r, round_by)\n\n    @staticmethod\n    def get_points_from_coeffs(current_value, coefficients, possible_values):\n        if np.isnan(possible_values).any():\n            return 0\n        # pylint: disable=undefined-loop-variable\n        indices = np.argsort(possible_values)\n        sorted_values = possible_values[indices]\n        sorted_coefficients = coefficients[indices]\n        for i, val in enumerate(sorted_values):\n            if current_value < val:\n                break\n        diff = sorted_values[i] - sorted_values[i - 1]\n        k = 0 if diff < 1e-6 else (sorted_values[i] - current_value) / \\\n                                  (sorted_values[i] - sorted_values[i - 1])\n        return sorted_coefficients[i - 1] * sorted_values[i - 1] * k + \\\n               sorted_coefficients[i] * sorted_values[i] * (1 - k)\n\n    def reset_settings(self):\n        with warnings.catch_warnings():\n            # setting target_class_index will trigger this innocent warning\n            warnings.filterwarnings(\n                \"ignore\", \"combo box 'target_class_index' is empty\")\n            self._reset_settings()\n        self.update_scene()",
                                "ast_type_str": "ClassDef",
                                "start_lineno": 649,
                                "end_lineno": 1342,
                                "parent": null
                            }
                        },
                        {
                            "file_name": "Orange/widgets/visualize/ownomogram.py",
                            "type": "class",
                            "name": "OWNomogram",
                            "content": "class OWNomogram(OWWidget):\n    name = \"Nomogram\"\n    description = \" Nomograms for Visualization of Naive Bayesian\" \\\n                  \" and Logistic Regression Classifiers.\"\n    icon = \"icons/Nomogram.svg\"\n    priority = 2000\n    keywords = []\n\n    class Inputs:\n        classifier = Input(\"Classifier\", Model)\n        data = Input(\"Data\", Table)\n\n    class Outputs:\n        features = Output(\"Features\", AttributeList)\n\n    MAX_N_ATTRS = 1000\n    POINT_SCALE = 0\n    ALIGN_LEFT = 0\n    ALIGN_ZERO = 1\n    ACCEPTABLE = (NaiveBayesModel, LogisticRegressionClassifier)\n    settingsHandler = ClassValuesContextHandler()\n    target_class_index = ContextSetting(0)\n    normalize_probabilities = Setting(False)\n    scale = Setting(1)\n    display_index = Setting(1)\n    n_attributes = Setting(10)\n    sort_index = Setting(SortBy.ABSOLUTE)\n    cont_feature_dim_index = Setting(0)\n\n    graph_name = \"scene\"\n\n    class Error(OWWidget.Error):\n        invalid_classifier = Msg(\"Nomogram accepts only Naive Bayes and \"\n                                 \"Logistic Regression classifiers.\")\n\n    def __init__(self):\n        super().__init__()\n        self.instances = None\n        self.domain = None\n        self.data = None\n        self.classifier = None\n        self.align = OWNomogram.ALIGN_ZERO\n        self.log_odds_ratios = []\n        self.log_reg_coeffs = []\n        self.log_reg_coeffs_orig = []\n        self.log_reg_cont_data_extremes = []\n        self.p = None\n        self.b0 = None\n        self.points = []\n        self.feature_items = {}\n        self.feature_marker_values = []\n        self.scale_marker_values = lambda x: x\n        self.nomogram_main = None\n        self.vertical_line = None\n        self.hidden_vertical_line = None\n        self.old_target_class_index = self.target_class_index\n        self.repaint = False\n\n        self.info.set_input_summary(self.info.NoInput)\n\n        # GUI\n        box = gui.vBox(self.controlArea, \"Target class\")\n        self.class_combo = gui.comboBox(\n            box, self, \"target_class_index\", callback=self._class_combo_changed,\n            contentsLength=12, searchable=True)\n        self.norm_check = gui.checkBox(\n            box, self, \"normalize_probabilities\", \"Normalize probabilities\",\n            hidden=True, callback=self.update_scene,\n            tooltip=\"For multiclass data 1 vs. all probabilities do not\"\n                    \" sum to 1 and therefore could be normalized.\")\n\n        self.scale_radio = gui.radioButtons(\n            self.controlArea, self, \"scale\", [\"Point scale\", \"Log odds ratios\"],\n            box=\"Scale\", callback=self.update_scene)\n\n        box = gui.vBox(self.controlArea, \"Display features\")\n        grid = QGridLayout()\n        radio_group = gui.radioButtonsInBox(\n            box, self, \"display_index\", [], orientation=grid,\n            callback=self.update_scene)\n        radio_all = gui.appendRadioButton(\n            radio_group, \"All\", addToLayout=False)\n        radio_best = gui.appendRadioButton(\n            radio_group, \"Best ranked:\", addToLayout=False)\n        spin_box = gui.hBox(None, margin=0)\n        self.n_spin = gui.spin(\n            spin_box, self, \"n_attributes\", 1, self.MAX_N_ATTRS, label=\" \",\n            controlWidth=60, callback=self._n_spin_changed)\n        grid.addWidget(radio_all, 1, 1)\n        grid.addWidget(radio_best, 2, 1)\n        grid.addWidget(spin_box, 2, 2)\n\n        self.sort_combo = gui.comboBox(\n            box, self, \"sort_index\", label=\"Rank by:\", items=SortBy.items(),\n            orientation=Qt.Horizontal, callback=self.update_scene)\n\n        self.cont_feature_dim_combo = gui.comboBox(\n            box, self, \"cont_feature_dim_index\", label=\"Numeric features: \",\n            items=[\"1D projection\", \"2D curve\"], orientation=Qt.Horizontal,\n            callback=self.update_scene)\n\n        gui.rubber(self.controlArea)\n\n        class _GraphicsView(QGraphicsView):\n            def __init__(self, scene, parent, **kwargs):\n                for k, v in dict(verticalScrollBarPolicy=Qt.ScrollBarAlwaysOff,\n                                 horizontalScrollBarPolicy=Qt.ScrollBarAlwaysOff,\n                                 viewportUpdateMode=QGraphicsView.BoundingRectViewportUpdate,\n                                 renderHints=(QPainter.Antialiasing |\n                                              QPainter.TextAntialiasing |\n                                              QPainter.SmoothPixmapTransform),\n                                 alignment=(Qt.AlignTop |\n                                            Qt.AlignLeft),\n                                 sizePolicy=QSizePolicy(QSizePolicy.MinimumExpanding,\n                                                        QSizePolicy.MinimumExpanding)).items():\n                    kwargs.setdefault(k, v)\n\n                super().__init__(scene, parent, **kwargs)\n\n        class GraphicsView(_GraphicsView):\n            def __init__(self, scene, parent):\n                super().__init__(scene, parent,\n                                 verticalScrollBarPolicy=Qt.ScrollBarAlwaysOn,\n                                 styleSheet='QGraphicsView {background: white}')\n                self.viewport().setMinimumWidth(300)  # XXX: This prevents some tests failing\n                self._is_resizing = False\n\n            w = self\n\n            def resizeEvent(self, resizeEvent):\n                # Recompute main scene on window width change\n                if resizeEvent.size().width() != resizeEvent.oldSize().width():\n                    self._is_resizing = True\n                    self.w.update_scene()\n                    self._is_resizing = False\n                return super().resizeEvent(resizeEvent)\n\n            def is_resizing(self):\n                return self._is_resizing\n\n            def sizeHint(self):\n                return QSize(400, 200)\n\n        class FixedSizeGraphicsView(_GraphicsView):\n            def __init__(self, scene, parent):\n                super().__init__(scene, parent,\n                                 sizePolicy=QSizePolicy(QSizePolicy.MinimumExpanding,\n                                                        QSizePolicy.Minimum))\n\n            def sizeHint(self):\n                return QSize(400, 85)\n\n        scene = self.scene = QGraphicsScene(self)\n\n        top_view = self.top_view = FixedSizeGraphicsView(scene, self)\n        mid_view = self.view = GraphicsView(scene, self)\n        bottom_view = self.bottom_view = FixedSizeGraphicsView(scene, self)\n\n        for view in (top_view, mid_view, bottom_view):\n            self.mainArea.layout().addWidget(view)\n\n        self.dot_animator = GraphicsColorAnimator(\n            self, 3000,\n            [\n                (0.9, DOT_COLOR),\n                (0.925, DOT_COLOR.lighter(115)),\n                (0.95, DOT_COLOR),\n                (0.975, DOT_COLOR.lighter(115)),\n                (1.0, DOT_COLOR)\n            ]\n        )\n\n    def _class_combo_changed(self):\n        with np.errstate(invalid='ignore'):\n            coeffs = [np.nan_to_num(p[self.target_class_index] /\n                                    p[self.old_target_class_index])\n                      for p in self.points]\n        points = [p[self.old_target_class_index] for p in self.points]\n        self.feature_marker_values = [\n            self.get_points_from_coeffs(v, c, p) for (v, c, p) in\n            zip(self.feature_marker_values, coeffs, points)]\n        self.feature_marker_values = np.asarray(self.feature_marker_values)\n        self.update_scene()\n        self.old_target_class_index = self.target_class_index\n\n    def _n_spin_changed(self):\n        self.display_index = 1\n        self.update_scene()\n\n    def update_controls(self):\n        self.class_combo.clear()\n        self.norm_check.setHidden(True)\n        self.cont_feature_dim_combo.setEnabled(True)\n        if self.domain is not None:\n            values = self.domain.class_vars[0].values\n            if values:\n                self.class_combo.addItems(values)\n                self.target_class_index = 0\n            if len(self.domain.attributes) > self.MAX_N_ATTRS:\n                self.display_index = 1\n            if len(self.domain.class_vars[0].values) > 2:\n                self.norm_check.setHidden(False)\n            if not self.domain.has_continuous_attributes():\n                self.cont_feature_dim_combo.setEnabled(False)\n                self.cont_feature_dim_index = 0\n        model = self.sort_combo.model()\n        item = model.item(SortBy.POSITIVE)\n        item.setFlags(item.flags() | Qt.ItemIsEnabled)\n        item = model.item(SortBy.NEGATIVE)\n        item.setFlags(item.flags() | Qt.ItemIsEnabled)\n        self.align = OWNomogram.ALIGN_ZERO\n        if self.classifier and isinstance(self.classifier,\n                                          LogisticRegressionClassifier):\n            self.align = OWNomogram.ALIGN_LEFT\n\n    @Inputs.data\n    def set_data(self, data):\n        self.instances = data\n        summary = len(data) if data else self.info.NoInput\n        details = format_summary_details(data) if data else \"\"\n        self.info.set_input_summary(summary, details)\n        self.feature_marker_values = []\n        self.set_feature_marker_values()\n        self.update_scene()\n\n    @Inputs.classifier\n    def set_classifier(self, classifier):\n        self.closeContext()\n        self.classifier = classifier\n        self.Error.clear()\n        if self.classifier and not isinstance(self.classifier, self.ACCEPTABLE):\n            self.Error.invalid_classifier()\n            self.classifier = None\n        self.domain = self.classifier.domain if self.classifier else None\n        self.data = None\n        self.calculate_log_odds_ratios()\n        self.calculate_log_reg_coefficients()\n        self.update_controls()\n        self.openContext(self.domain.class_var if self.domain is not None\n                         else None)\n        self.points = self.log_odds_ratios or self.log_reg_coeffs\n        self.feature_marker_values = []\n        self.old_target_class_index = self.target_class_index\n        self.update_scene()\n\n    def calculate_log_odds_ratios(self):\n        self.log_odds_ratios = []\n        self.p = None\n        if self.classifier is None or self.domain is None:\n            return\n        if not isinstance(self.classifier, NaiveBayesModel):\n            return\n\n        log_cont_prob = self.classifier.log_cont_prob\n        class_prob = self.classifier.class_prob\n        for i in range(len(self.domain.attributes)):\n            ca = np.exp(log_cont_prob[i]) * class_prob[:, None]\n            _or = (ca / (1 - ca)) / (class_prob / (1 - class_prob))[:, None]\n            self.log_odds_ratios.append(np.log(_or))\n        self.p = class_prob\n\n    def calculate_log_reg_coefficients(self):\n        self.log_reg_coeffs = []\n        self.log_reg_cont_data_extremes = []\n        self.b0 = None\n        if self.classifier is None or self.domain is None:\n            return\n        if not isinstance(self.classifier, LogisticRegressionClassifier):\n            return\n\n        self.domain = self.reconstruct_domain(self.classifier.original_domain,\n                                              self.domain)\n        self.data = self.classifier.original_data.transform(self.domain)\n        attrs, ranges, start = self.domain.attributes, [], 0\n        for attr in attrs:\n            stop = start + len(attr.values) if attr.is_discrete else start + 1\n            ranges.append(slice(start, stop))\n            start = stop\n\n        self.b0 = self.classifier.intercept\n        coeffs = self.classifier.coefficients\n        if len(self.domain.class_var.values) == 2:\n            self.b0 = np.hstack((self.b0 * (-1), self.b0))\n            coeffs = np.vstack((coeffs * (-1), coeffs))\n        self.log_reg_coeffs = [coeffs[:, r] for r in ranges]\n        self.log_reg_coeffs_orig = self.log_reg_coeffs.copy()\n\n        min_values = nanmin(self.data.X, axis=0)\n        max_values = nanmax(self.data.X, axis=0)\n\n        for i, min_t, max_t in zip(range(len(self.log_reg_coeffs)),\n                                   min_values, max_values):\n            if self.log_reg_coeffs[i].shape[1] == 1:\n                coef = self.log_reg_coeffs[i]\n                self.log_reg_coeffs[i] = np.hstack((coef * min_t, coef * max_t))\n                self.log_reg_cont_data_extremes.append(\n                    [sorted([min_t, max_t], reverse=(c < 0)) for c in coef.flat])\n            else:\n                self.log_reg_cont_data_extremes.append([None])\n\n    def update_scene(self):\n        self.clear_scene()\n        if self.domain is None or not len(self.points[0]):\n            self.Outputs.features.send(None)\n            return\n\n        n_attrs = self.n_attributes if self.display_index else int(1e10)\n        attr_inds, attributes = zip(*self.get_ordered_attributes()[:n_attrs])\n        self.Outputs.features.send(AttributeList(attributes))\n\n        name_items = [QGraphicsTextItem(attr.name) for attr in attributes]\n        point_text = QGraphicsTextItem(\"Points\")\n        probs_text = QGraphicsTextItem(\"Probabilities (%)\")\n        all_items = name_items + [point_text, probs_text]\n        name_offset = -max(t.boundingRect().width() for t in all_items) - 10\n        w = self.view.viewport().rect().width()\n        max_width = w + name_offset - 30\n\n        points = [self.points[i][self.target_class_index]\n                  for i in attr_inds]\n        if self.align == OWNomogram.ALIGN_LEFT:\n            points = [p - p.min() for p in points]\n        max_ = np.nan_to_num(max(max(abs(p)) for p in points))\n        d = 100 / max_ if max_ else 1\n        minimums = [p[self.target_class_index].min() for p in self.points]\n        if self.scale == OWNomogram.POINT_SCALE:\n            points = [p * d for p in points]\n\n            if self.align == OWNomogram.ALIGN_LEFT:\n                self.scale_marker_values = lambda x: (x - minimums) * d\n            else:\n                self.scale_marker_values = lambda x: x * d\n        else:\n            if self.align == OWNomogram.ALIGN_LEFT:\n                self.scale_marker_values = lambda x: x - minimums\n            else:\n                self.scale_marker_values = lambda x: x\n\n        point_item, nomogram_head = self.create_main_nomogram(\n            attributes, attr_inds,\n            name_items, points, max_width, point_text, name_offset)\n        probs_item, nomogram_foot = self.create_footer_nomogram(\n            probs_text, d, minimums, max_width, name_offset)\n        for item in self.feature_items.values():\n            item.dot.point_dot = point_item.dot\n            item.dot.probs_dot = probs_item.dot\n            item.dot.vertical_line = self.hidden_vertical_line\n\n        self.dot_animator.setGraphicsItems(\n            [item.dot for item in self.feature_items.values()]\n        )\n        self.dot_animator.start()\n\n        self.nomogram = nomogram = NomogramItem()\n        nomogram.add_items([nomogram_head, self.nomogram_main, nomogram_foot])\n        self.scene.addItem(nomogram)\n\n        self.set_feature_marker_values()\n\n        rect = QRectF(self.scene.itemsBoundingRect().x(),\n                      self.scene.itemsBoundingRect().y(),\n                      self.scene.itemsBoundingRect().width(),\n                      self.nomogram.preferredSize().height()).adjusted(10, 0, 20, 0)\n        self.scene.setSceneRect(rect)\n\n        # Clip top and bottom (60 and 150) parts from the main view\n        self.view.setSceneRect(rect.x(), rect.y() + 80, rect.width() - 10, rect.height() - 160)\n        self.view.viewport().setMaximumHeight(rect.height() - 160)\n        # Clip main part from top/bottom views\n        # below point values are imprecise (less/more than required) but this\n        # is not a problem due to clipped scene content still being drawn\n        self.top_view.setSceneRect(rect.x(), rect.y() + 3, rect.width() - 10, 20)\n        self.bottom_view.setSceneRect(rect.x(), rect.height() - 110, rect.width() - 10, 30)\n\n    @staticmethod\n    def _adjust_scale(attributes, points, max_width, diff, attr_inds,\n                      log_reg_cont_data_extremes, cls_index):\n        if not diff:\n            return max_width\n\n        def offset(name, point):\n            text_ = QGraphicsTextItem(name).boundingRect()\n            return scale * point + text_.width() / 2\n\n        lr = log_reg_cont_data_extremes\n        scale = max_width / diff\n        names = list(chain.from_iterable(\n            [_get_labels(a, lr and lr[i] and lr[i][0] and lr[i][cls_index],\n                         OWNomogram.get_ruler_values(p.min(), p.max(),\n                                                     scale * p.ptp(), False))\n             for i, a, p in zip(attr_inds, attributes, points)]))\n        points = list(chain.from_iterable(points))\n\n        old_scale = scale + 1\n        while old_scale > scale:\n            old_scale = scale\n            offsets = [offset(n, p) for n, p in zip(names, points)]\n            most_right_name = names[np.argmax(offsets)]\n            text = QGraphicsTextItem(most_right_name).boundingRect()\n            scale = (max_width - text.width() / 2) / diff\n        return scale\n\n    def create_main_nomogram(self, attributes, attr_inds, name_items, points,\n                             max_width, point_text, name_offset):\n        cls_index = self.target_class_index\n        min_p = min(p.min() for p in points)\n        max_p = max(p.max() for p in points)\n        values = self.get_ruler_values(min_p, max_p, max_width)\n        min_p, max_p = min(values), max(values)\n        diff_ = np.nan_to_num(max_p - min_p)\n        scale_x = self._adjust_scale(\n            attributes, points, max_width, diff_, attr_inds,\n            self.log_reg_cont_data_extremes, cls_index\n        )\n\n        nomogram_header = NomogramItem()\n        point_item = RulerItem(point_text, values, scale_x, name_offset,\n                               - scale_x * min_p)\n        point_item.setPreferredSize(point_item.preferredWidth(), 35)\n        nomogram_header.add_items([point_item])\n\n        self.nomogram_main = NomogramItem()\n        cont_feature_item_class = ContinuousFeature2DItem if \\\n            self.cont_feature_dim_index else ContinuousFeatureItem\n\n        feature_items = [\n            DiscreteFeatureItem(\n                name_item, attr, point,\n                scale_x, name_offset, - scale_x * min_p)\n            if attr.is_discrete else\n            cont_feature_item_class(\n                name_item, attr, self.log_reg_cont_data_extremes[i][cls_index],\n                self.get_ruler_values(\n                    point.min(), point.max(),\n                    scale_x * point.ptp(), False),\n                scale_x, name_offset, - scale_x * min_p)\n            for i, attr, name_item, point in\n            zip(attr_inds, attributes, name_items, points)]\n\n        self.nomogram_main.add_items(feature_items)\n        self.feature_items = OrderedDict(sorted(zip(attr_inds, feature_items)))\n\n        x = - scale_x * min_p\n        y = self.nomogram_main.layout().preferredHeight() + 10\n        self.vertical_line = QGraphicsLineItem(x, -6, x, y)\n        self.vertical_line.setPen(QPen(Qt.DotLine))\n        self.vertical_line.setParentItem(point_item)\n        self.hidden_vertical_line = QGraphicsLineItem(x, -6, x, y)\n        pen = QPen(Qt.DashLine)\n        pen.setBrush(QColor(Qt.red))\n        self.hidden_vertical_line.setPen(pen)\n        self.hidden_vertical_line.setParentItem(point_item)\n\n        return point_item, nomogram_header\n\n    def get_ordered_attributes(self):\n        \"\"\"Return (in_domain_index, attr) pairs, ordered by method in SortBy combo\"\"\"\n        if self.domain is None or not self.domain.attributes:\n            return []\n\n        attrs = self.domain.attributes\n        sort_by = self.sort_index\n        class_value = self.target_class_index\n\n        if sort_by == SortBy.NO_SORTING:\n            return list(enumerate(attrs))\n\n        elif sort_by == SortBy.NAME:\n\n            def key(x):\n                _, attr = x\n                return attr.name.lower()\n\n        elif sort_by == SortBy.ABSOLUTE:\n\n            def key(x):\n                i, attr = x\n                if attr.is_discrete:\n                    ptp = self.points[i][class_value].ptp()\n                else:\n                    coef = np.abs(self.log_reg_coeffs_orig[i][class_value]).mean()\n                    ptp = coef * np.ptp(self.log_reg_cont_data_extremes[i][class_value])\n                return -ptp\n\n        elif sort_by == SortBy.POSITIVE:\n\n            def key(x):\n                i, attr = x\n                max_value = (self.points[i][class_value].max()\n                             if attr.is_discrete else\n                             np.mean(self.log_reg_cont_data_extremes[i][class_value]))\n                return -max_value\n\n        elif sort_by == SortBy.NEGATIVE:\n\n            def key(x):\n                i, attr = x\n                min_value = (self.points[i][class_value].min()\n                             if attr.is_discrete else\n                             np.mean(self.log_reg_cont_data_extremes[i][class_value]))\n                return min_value\n\n        return sorted(enumerate(attrs), key=key)\n\n\n    def create_footer_nomogram(self, probs_text, d, minimums,\n                               max_width, name_offset):\n        # pylint: disable=invalid-unary-operand-type\n        eps, d_ = 0.05, 1\n        k = - np.log(self.p / (1 - self.p)) if self.p is not None else - self.b0\n        min_sum = k[self.target_class_index] - np.log((1 - eps) / eps)\n        max_sum = k[self.target_class_index] - np.log(eps / (1 - eps))\n        if self.align == OWNomogram.ALIGN_LEFT:\n            max_sum = max_sum - sum(minimums)\n            min_sum = min_sum - sum(minimums)\n            for i in range(len(k)):  # pylint: disable=consider-using-enumerate\n                k[i] = k[i] - sum([min(q) for q in [p[i] for p in self.points]])\n        if self.scale == OWNomogram.POINT_SCALE:\n            min_sum *= d\n            max_sum *= d\n            d_ = d\n\n        values = self.get_ruler_values(min_sum, max_sum, max_width)\n        min_sum, max_sum = min(values), max(values)\n        diff_ = np.nan_to_num(max_sum - min_sum)\n        scale_x = max_width / diff_ if diff_ else max_width\n        cls_var, cls_index = self.domain.class_var, self.target_class_index\n        nomogram_footer = NomogramItem()\n\n        def get_normalized_probabilities(val):\n            if not self.normalize_probabilities:\n                return 1 / (1 + np.exp(k[cls_index] - val / d_))\n            totals = self.__get_totals_for_class_values(minimums)\n            p_sum = np.sum(1 / (1 + np.exp(k - totals / d_)))\n            return 1 / (1 + np.exp(k[cls_index] - val / d_)) / p_sum\n\n        def get_points(prob):\n            if not self.normalize_probabilities:\n                return (k[cls_index] - np.log(1 / prob - 1)) * d_\n            totals = self.__get_totals_for_class_values(minimums)\n            p_sum = np.sum(1 / (1 + np.exp(k - totals / d_)))\n            return (k[cls_index] - np.log(1 / (prob * p_sum) - 1)) * d_\n\n        probs_item = ProbabilitiesRulerItem(\n            probs_text, values, scale_x, name_offset, - scale_x * min_sum,\n            get_points=get_points,\n            title=\"{}='{}'\".format(cls_var.name, cls_var.values[cls_index]),\n            get_probabilities=get_normalized_probabilities)\n        nomogram_footer.add_items([probs_item])\n        return probs_item, nomogram_footer\n\n    def __get_totals_for_class_values(self, minimums):\n        cls_index = self.target_class_index\n        marker_values = self.scale_marker_values(self.feature_marker_values)\n        totals = np.full(len(self.domain.class_var.values), np.nan)\n        totals[cls_index] = marker_values.sum()\n        for i in range(len(self.domain.class_var.values)):\n            if i == cls_index:\n                continue\n            coeffs = [np.nan_to_num(p[i] / p[cls_index]) for p in self.points]\n            points = [p[cls_index] for p in self.points]\n            total = sum([self.get_points_from_coeffs(v, c, p) for (v, c, p)\n                         in zip(self.feature_marker_values, coeffs, points)])\n            if self.align == OWNomogram.ALIGN_LEFT:\n                points = [p - m for m, p in zip(minimums, points)]\n                total -= sum([min(p) for p in [p[i] for p in self.points]])\n            d = 100 / max(max(abs(p)) for p in points)\n            if self.scale == OWNomogram.POINT_SCALE:\n                total *= d\n            totals[i] = total\n        assert not np.any(np.isnan(totals))\n        return totals\n\n    def set_feature_marker_values(self):\n        if not (len(self.points) and len(self.feature_items)):\n            return\n        if not len(self.feature_marker_values):\n            self._init_feature_marker_values()\n        marker_values = self.scale_marker_values(self.feature_marker_values)\n\n        invisible_sum = 0\n        for i, marker in enumerate(marker_values):\n            try:\n                item = self.feature_items[i]\n            except KeyError:\n                invisible_sum += marker\n            else:\n                item.dot.move_to_val(marker)\n\n        item.dot.probs_dot.move_to_sum(invisible_sum)\n\n    def _init_feature_marker_values(self):\n        self.feature_marker_values = []\n        cls_index = self.target_class_index\n        instances = self.instances.transform(self.domain) \\\n            if self.instances else None\n        values = []\n        for i, attr in enumerate(self.domain.attributes):\n            value, feature_val = 0, None\n            if len(self.log_reg_coeffs):\n                if attr.is_discrete:\n                    ind, n = unique(self.data.X[:, i], return_counts=True)\n                    feature_val = np.nan_to_num(ind[np.argmax(n)])\n                else:\n                    feature_val = nanmean(self.data.X[:, i])\n\n            # If data is provided on a separate signal, use the first data\n            # instance to position the points instead of the mean\n            inst_in_dom = instances and attr in instances.domain\n            if inst_in_dom and not np.isnan(instances[0][attr]):\n                feature_val = instances[0][attr]\n\n            if feature_val is not None:\n                value = (self.points[i][cls_index][int(feature_val)]\n                         if attr.is_discrete else\n                         self.log_reg_coeffs_orig[i][cls_index][0] * feature_val)\n            values.append(value)\n        self.feature_marker_values = np.asarray(values)\n\n    def clear_scene(self):\n        self.feature_items = {}\n        self.scale_marker_values = lambda x: x\n        self.nomogram = None\n        self.nomogram_main = None\n        self.vertical_line = None\n        self.hidden_vertical_line = None\n        self.dot_animator.clear()\n        self.scene.clear()\n\n    def send_report(self):\n        self.report_plot()\n\n    @staticmethod\n    def reconstruct_domain(original, preprocessed):\n        # abuse dict to make \"in\" comparisons faster\n        attrs = OrderedDict()\n        for attr in preprocessed.attributes:\n            cv = attr._compute_value.variable._compute_value\n            var = cv.variable if cv else original[attr.name]\n            var = original[var.name] if var.name in original else attr\n            if var in attrs:    # the reason for OrderedDict\n                continue\n            attrs[var] = None   # we only need keys\n        attrs = list(attrs.keys())\n        return Domain(attrs, original.class_var, original.metas)\n\n    @staticmethod\n    def get_ruler_values(start, stop, max_width, round_to_nearest=True):\n        if max_width == 0:\n            return [0]\n        diff = np.nan_to_num((stop - start) / max_width)\n        if diff <= 0:\n            return [0]\n        decimals = int(np.floor(np.log10(diff)))\n        if diff > 4 * pow(10, decimals):\n            step = 5 * pow(10, decimals + 2)\n        elif diff > 2 * pow(10, decimals):\n            step = 2 * pow(10, decimals + 2)\n        elif diff > 1 * pow(10, decimals):\n            step = 1 * pow(10, decimals + 2)\n        else:\n            step = 5 * pow(10, decimals + 1)\n        round_by = int(- np.floor(np.log10(step)))\n        r = start % step\n        if not round_to_nearest:\n            _range = np.arange(start + step, stop + r, step) - r\n            start, stop = np.floor(start * 100) / 100, np.ceil(stop * 100) / 100\n            return np.round(np.hstack((start, _range, stop)), 2)\n        return np.round(np.arange(start, stop + r + step, step) - r, round_by)\n\n    @staticmethod\n    def get_points_from_coeffs(current_value, coefficients, possible_values):\n        if np.isnan(possible_values).any():\n            return 0\n        # pylint: disable=undefined-loop-variable\n        indices = np.argsort(possible_values)\n        sorted_values = possible_values[indices]\n        sorted_coefficients = coefficients[indices]\n        for i, val in enumerate(sorted_values):\n            if current_value < val:\n                break\n        diff = sorted_values[i] - sorted_values[i - 1]\n        k = 0 if diff < 1e-6 else (sorted_values[i] - current_value) / \\\n                                  (sorted_values[i] - sorted_values[i - 1])\n        return sorted_coefficients[i - 1] * sorted_values[i - 1] * k + \\\n               sorted_coefficients[i] * sorted_values[i] * (1 - k)\n\n    def reset_settings(self):\n        with warnings.catch_warnings():\n            # setting target_class_index will trigger this innocent warning\n            warnings.filterwarnings(\n                \"ignore\", \"combo box 'target_class_index' is empty\")\n            self._reset_settings()\n        self.update_scene()",
                            "ast_type_str": "ClassDef",
                            "start_lineno": 649,
                            "end_lineno": 1342,
                            "parent": null
                        }
                    ],
                    "added_entities": [],
                    "deleted_entities": []
                }
            ]
        },
        {
            "old_file_content": "# Test methods with long descriptive names can omit docstrings\n# pylint: disable=missing-docstring,protected-access\nimport unittest\nfrom unittest.mock import Mock, patch\n\nimport numpy as np\n\nfrom AnyQt.QtCore import QPoint, QPropertyAnimation\n\nfrom orangewidget.widget import StateInfo\n\nfrom Orange.data import Table, Domain, ContinuousVariable, DiscreteVariable\nfrom Orange.classification import (\n    NaiveBayesLearner, LogisticRegressionLearner, MajorityLearner\n)\nfrom Orange.tests import test_filename\nfrom Orange.widgets.tests.base import WidgetTest\nfrom Orange.widgets.utils.state_summary import format_summary_details\nfrom Orange.widgets.visualize.ownomogram import (\n    OWNomogram, DiscreteFeatureItem, ContinuousFeatureItem, ProbabilitiesDotItem,\n    MovableToolTip\n)\n\n\nclass TestOWNomogram(WidgetTest):\n    @classmethod\n    def setUpClass(cls):\n        super().setUpClass()\n        cls.data = Table(\"heart_disease\")\n        cls.nb_cls = NaiveBayesLearner()(cls.data)\n        cls.lr_cls = LogisticRegressionLearner()(cls.data)\n        cls.titanic = Table(\"titanic\")\n        cls.lenses = Table(test_filename(\"datasets/lenses.tab\"))\n\n    def setUp(self):\n        self.widget = self.create_widget(OWNomogram)  # type: OWNomogram\n\n    def test_input_nb_cls(self):\n        \"\"\"Check naive bayes classifier on input\"\"\"\n        self.send_signal(self.widget.Inputs.classifier, self.nb_cls)\n        self.assertEqual(len([item for item in self.widget.scene.items() if\n                              isinstance(item, DiscreteFeatureItem)]),\n                         min(self.widget.n_attributes,\n                             len(self.data.domain.attributes)))\n\n    def test_input_lr_cls(self):\n        \"\"\"Check logistic regression classifier on input\"\"\"\n        self.widget.display_index = 0  # display ALL features\n        self.send_signal(self.widget.Inputs.classifier, self.lr_cls)\n        self.assertEqual(\n            len([item for item in self.widget.scene.items() if\n                 isinstance(item, DiscreteFeatureItem)]),\n            len([a for a in self.data.domain.attributes if a.is_discrete]))\n        self.assertEqual(\n            len([item for item in self.widget.scene.items() if\n                 isinstance(item, ContinuousFeatureItem)]),\n            len([a for a in self.data.domain.attributes if a.is_continuous]))\n\n    def test_input_invalid_cls(self):\n        \"\"\"Check any classifier on input\"\"\"\n        majority_cls = MajorityLearner()(self.data)\n        self.send_signal(self.widget.Inputs.classifier, majority_cls)\n        self.assertTrue(self.widget.Error.invalid_classifier.is_shown())\n        self.send_signal(self.widget.Inputs.classifier, None)\n        self.assertFalse(self.widget.Error.invalid_classifier.is_shown())\n\n    def test_input_instance(self):\n        \"\"\" Check data instance on input\"\"\"\n        self.send_signal(self.widget.Inputs.data, self.data)\n        self.assertIsNotNone(self.widget.instances)\n        self.send_signal(self.widget.Inputs.data, None)\n        self.assertIsNone(self.widget.instances)\n\n    def test_target_values(self):\n        \"\"\"Check Target class combo values\"\"\"\n        self.send_signal(self.widget.Inputs.classifier, self.nb_cls)\n        for i, text in enumerate(self.data.domain.class_var.values):\n            self.assertEqual(text, self.widget.class_combo.itemText(i))\n        self.send_signal(self.widget.Inputs.classifier, None)\n        self.assertEqual(0, self.widget.class_combo.count())\n\n    def test_nomogram_nb(self):\n        \"\"\"Check probabilities for naive bayes classifier for various values\n        of classes and radio buttons\"\"\"\n        self._test_helper(self.nb_cls, [54, 46])\n\n    def test_nomogram_lr(self):\n        \"\"\"Check probabilities for logistic regression classifier for various\n        values of classes and radio buttons\"\"\"\n        self.widget.display_index = 0  # show ALL features\n        self._test_helper(self.lr_cls, [58, 42])\n\n    def test_nomogram_nb_multiclass(self):\n        \"\"\"Check probabilities for naive bayes classifier for various values\n        of classes and radio buttons for multiclass data\"\"\"\n        cls = NaiveBayesLearner()(self.lenses)\n        self._test_helper(cls, [19, 53, 13])\n\n    def test_nomogram_lr_multiclass(self):\n        \"\"\"Check probabilities for logistic regression classifier for various\n        values of classes and radio buttons for multiclass data\"\"\"\n        cls = LogisticRegressionLearner(\n            multi_class=\"ovr\", solver=\"liblinear\"\n        )(self.lenses)\n        self._test_helper(cls, [9, 45, 52])\n\n    def test_nomogram_with_instance_nb(self):\n        \"\"\"Check initialized marker values and feature sorting for naive bayes\n        classifier and data on input\"\"\"\n        cls = NaiveBayesLearner()(self.titanic)\n        data = self.titanic[10:11]\n        self.send_signal(self.widget.Inputs.classifier, cls)\n        self.send_signal(self.widget.Inputs.data, data)\n        self._check_values(data.domain.attributes, data)\n        self._test_sort([[\"status\", \"age\", \"sex\"],\n                         [\"age\", \"sex\", \"status\"],\n                         [\"sex\", \"status\", \"age\"],\n                         [\"sex\", \"status\", \"age\"],\n                         [\"sex\", \"status\", \"age\"]])\n\n    def test_nomogram_with_instance_lr(self):\n        \"\"\"Check initialized marker values and feature sorting for logistic\n        regression classifier and data on input\"\"\"\n        cls = LogisticRegressionLearner()(self.titanic)\n        data = self.titanic[10:11]\n        self.send_signal(self.widget.Inputs.classifier, cls)\n        self.send_signal(self.widget.Inputs.data, data)\n        self._check_values(data.domain.attributes, data)\n        self._test_sort([[\"status\", \"age\", \"sex\"],\n                         [\"age\", \"sex\", \"status\"],\n                         [\"sex\", \"status\", \"age\"],\n                         [\"sex\", \"status\", \"age\"],\n                         [\"sex\", \"status\", \"age\"]])\n\n    def test_constant_feature_disc(self):\n        \"\"\"Check nomogram for data with constant discrete feature\"\"\"\n        domain = Domain([DiscreteVariable(\"d1\", (\"a\", \"c\")),\n                         DiscreteVariable(\"d2\", (\"b\",))],\n                        DiscreteVariable(\"cls\", (\"e\", \"d\")))\n        X = np.array([[0, 0], [1, 0], [0, 0], [1, 0]])\n        data = Table(domain, X, np.array([0, 1, 1, 0]))\n        cls = NaiveBayesLearner()(data)\n        self._test_helper(cls, [50, 50])\n        cls = LogisticRegressionLearner()(data)\n        self._test_helper(cls, [50, 50])\n\n    def test_constant_feature_cont(self):\n        \"\"\"Check nomogram for data with constant continuous feature\"\"\"\n        domain = Domain([DiscreteVariable(\"d\", (\"a\", \"b\")),\n                         ContinuousVariable(\"c\")],\n                        DiscreteVariable(\"cls\", (\"c\", \"d\")))\n        X = np.array([[0, 0], [1, 0], [0, 0], [1, 0]])\n        data = Table(domain, X, np.array([0, 1, 1, 0]))\n        cls = NaiveBayesLearner()(data)\n        self._test_helper(cls, [50, 50])\n        cls = LogisticRegressionLearner()(data)\n        self._test_helper(cls, [50, 50])\n\n    def _test_helper(self, cls, values):\n        self.send_signal(self.widget.Inputs.classifier, cls)\n\n        # check for all class values\n        for i in range(self.widget.class_combo.count()):\n            self.widget.class_combo.activated.emit(i)\n            self.widget.class_combo.setCurrentIndex(i)\n\n            # check probabilities marker value\n            self._test_helper_check_probability(values[i])\n\n            # point scale\n            self.widget.controls.scale.buttons[0].click()\n            self._test_helper_check_probability(values[i])\n\n            # best ranked\n            self.widget.n_attributes = 5\n            self.widget.controls.display_index.buttons[1].click()\n            visible_items = [item for item in self.widget.scene.items() if\n                             isinstance(item, (DiscreteFeatureItem,\n                                               ContinuousFeatureItem)) and\n                             item.isVisible()]\n            self.assertGreaterEqual(5, len(visible_items))\n\n            # 2D curve\n            self.widget.n_attributes = 15\n            self.widget.cont_feature_dim_combo.activated.emit(1)\n            self.widget.cont_feature_dim_combo.setCurrentIndex(1)\n            self._test_helper_check_probability(values[i])\n\n            # initial state\n            self.widget.controls.scale.buttons[1].click()\n            self.widget.controls.display_index.buttons[0].click()\n            self.widget.cont_feature_dim_combo.activated.emit(0)\n            self.widget.cont_feature_dim_combo.setCurrentIndex(0)\n\n    def _test_helper_check_probability(self, value):\n        prob_marker = [item for item in self.widget.scene.items() if\n                       isinstance(item, ProbabilitiesDotItem)][0]\n        self.assertIn(\"Probability: {}\".format(value),\n                      prob_marker.get_tooltip_text())\n\n    def _check_values(self, attributes, data):\n        for attr, item in zip(attributes, self.widget.feature_items.values()):\n            assert attr.name == item.childItems()[0].toPlainText()\n            value = data[0][attr.name].value\n            value = \"{}: 100%\".format(value) if attr.is_discrete \\\n                else \"Value: {}\".format(value)\n            self.assertIn(value, item.dot.get_tooltip_text())\n\n    def _test_sort(self, names):\n        for i in range(self.widget.sort_combo.count()):\n            self.widget.sort_combo.activated.emit(i)\n            self.widget.sort_combo.setCurrentIndex(i)\n            ordered = [self.widget.nomogram_main.layout().itemAt(i).childItems()[0].toPlainText()\n                       for i in range(self.widget.nomogram_main.layout().count())]\n            self.assertListEqual(names[i], ordered)\n\n    def test_tooltip(self):\n        # had problems on PyQt4\n        m = MovableToolTip()\n        m.show(QPoint(0, 0), \"Some text.\")\n\n    def test_output(self):\n        cls = LogisticRegressionLearner()(self.titanic)\n        data = self.titanic[10:11]\n        status, age, sex = data.domain.attributes\n        self.send_signal(self.widget.Inputs.classifier, cls)\n        self.widget.sort_combo.setCurrentIndex(1)\n        self.widget.sort_combo.activated.emit(1)\n\n        # Output more attributer than there are -> output all\n        self.widget.n_attributes = 5\n        self.widget.n_spin.valueChanged.emit(5)\n        attrs = self.get_output(self.widget.Outputs.features)\n        self.assertEqual(attrs, [age, sex, status])\n\n        # Output the first two\n        self.widget.n_attributes = 2\n        self.widget.n_spin.valueChanged.emit(2)\n        attrs = self.get_output(self.widget.Outputs.features)\n        self.assertEqual(attrs, [age, sex])\n\n        # Set to output all\n        self.widget.display_index = 0\n        self.widget.controls.display_index.group.buttonClicked[int].emit(0)\n        attrs = self.get_output(self.widget.Outputs.features)\n        self.assertEqual(attrs, [age, sex, status])\n\n        # Remove classifier -> output None\n        self.send_signal(self.widget.Inputs.classifier, None)\n        attrs = self.get_output(self.widget.Outputs.features)\n        self.assertIsNone(attrs)\n\n    def test_reset_settings(self):\n        self.widget.n_attributes = 5\n        self.widget.n_spin.valueChanged.emit(5)\n        self.widget.reset_settings()\n        self.assertEqual(10, self.widget.n_attributes)\n\n    @patch(\"Orange.widgets.visualize.ownomogram.QGraphicsTextItem\")\n    def test_adjust_scale(self, mocked_item: Mock):\n        def mocked_width():\n            nonlocal ind\n            ind += 1\n            most_right = {4: 30, 9: 59, 14: 59}\n            return [2, 30, 59, 2, most_right.get(ind)][ind % 5]\n\n        ind = -1\n        mocked_item().boundingRect().width.side_effect = mocked_width\n        attrs = [DiscreteVariable(\"var1\", values=(\"foo1\", \"foo2\")),\n                 DiscreteVariable(\"var2\", values=(\"foo3\", \"foo4\"))]\n        points = [np.array([0, 1.8]), np.array([1.5, 2.0])]\n        diff = np.max(points) - np.min(points)\n        # foo3 eventually overcomes foo2, while the scale is getting smaller\n        #\n        #  0                        1              1.5         1.8     2\n        # foo1                                          _______foo2_______\n        #                               __________foo3__________      foo4\n        self.widget._adjust_scale(attrs, points, 100, diff, [0, 1], [], 0)\n        # most left text at 1. iteration\n        self.assertEqual(mocked_item.call_args_list[5][0][0], \"foo2\")\n        # most left text at 2. iteration\n        self.assertEqual(mocked_item.call_args_list[10][0][0], \"foo3\")\n        # most left text at 3. iteration is the same -> stop\n        self.assertEqual(mocked_item.call_args_list[15][0][0], \"foo3\")\n\n    def test_summary(self):\n        \"\"\"Check if status bar is updated when data is received\"\"\"\n        data = self.data\n        info = self.widget.info\n        no_input = \"No data on input\"\n\n        self.send_signal(self.widget.Inputs.data, data)\n        summary, details = f\"{len(data)}\", format_summary_details(data)\n        self.assertEqual(info._StateInfo__input_summary.brief, summary)\n        self.assertEqual(info._StateInfo__input_summary.details, details)\n        self.send_signal(self.widget.Inputs.data, None)\n        self.assertIsInstance(info._StateInfo__input_summary, StateInfo.Empty)\n        self.assertEqual(info._StateInfo__input_summary.details, no_input)\n\n    def test_dots_stop_flashing(self):\n        self.widget.set_data(self.data)\n        self.widget.set_classifier(self.nb_cls)\n        animator = self.widget.dot_animator\n        dot = animator._GraphicsColorAnimator__items[0]\n        dot._mousePressFunc()\n        anim = animator._GraphicsColorAnimator__animation\n        self.assertNotEqual(anim.state(), QPropertyAnimation.Running)\n\n\nif __name__ == \"__main__\":\n    unittest.main()\n",
            "new_file_content": "# Test methods with long descriptive names can omit docstrings\n# pylint: disable=missing-docstring,protected-access\nimport unittest\nfrom unittest.mock import Mock, patch\n\nimport numpy as np\n\nfrom AnyQt.QtCore import QPoint, QPropertyAnimation\n\nfrom orangewidget.widget import StateInfo\n\nfrom Orange.data import Table, Domain, ContinuousVariable, DiscreteVariable\nfrom Orange.classification import (\n    NaiveBayesLearner, LogisticRegressionLearner, MajorityLearner\n)\nfrom Orange.preprocess import Scale, Continuize\nfrom Orange.tests import test_filename\nfrom Orange.widgets.tests.base import WidgetTest\nfrom Orange.widgets.utils.state_summary import format_summary_details\nfrom Orange.widgets.visualize.ownomogram import (\n    OWNomogram, DiscreteFeatureItem, ContinuousFeatureItem, ProbabilitiesDotItem,\n    MovableToolTip\n)\n\n\nclass TestOWNomogram(WidgetTest):\n    @classmethod\n    def setUpClass(cls):\n        super().setUpClass()\n        cls.data = Table(\"heart_disease\")\n        cls.nb_cls = NaiveBayesLearner()(cls.data)\n        cls.lr_cls = LogisticRegressionLearner()(cls.data)\n        cls.titanic = Table(\"titanic\")\n        cls.lenses = Table(test_filename(\"datasets/lenses.tab\"))\n\n    def setUp(self):\n        self.widget = self.create_widget(OWNomogram)  # type: OWNomogram\n\n    def test_input_nb_cls(self):\n        \"\"\"Check naive bayes classifier on input\"\"\"\n        self.send_signal(self.widget.Inputs.classifier, self.nb_cls)\n        self.assertEqual(len([item for item in self.widget.scene.items() if\n                              isinstance(item, DiscreteFeatureItem)]),\n                         min(self.widget.n_attributes,\n                             len(self.data.domain.attributes)))\n\n    def test_input_lr_cls(self):\n        \"\"\"Check logistic regression classifier on input\"\"\"\n        self.widget.display_index = 0  # display ALL features\n        self.send_signal(self.widget.Inputs.classifier, self.lr_cls)\n        self.assertEqual(\n            len([item for item in self.widget.scene.items() if\n                 isinstance(item, DiscreteFeatureItem)]),\n            len([a for a in self.data.domain.attributes if a.is_discrete]))\n        self.assertEqual(\n            len([item for item in self.widget.scene.items() if\n                 isinstance(item, ContinuousFeatureItem)]),\n            len([a for a in self.data.domain.attributes if a.is_continuous]))\n\n    def test_input_invalid_cls(self):\n        \"\"\"Check any classifier on input\"\"\"\n        majority_cls = MajorityLearner()(self.data)\n        self.send_signal(self.widget.Inputs.classifier, majority_cls)\n        self.assertTrue(self.widget.Error.invalid_classifier.is_shown())\n        self.send_signal(self.widget.Inputs.classifier, None)\n        self.assertFalse(self.widget.Error.invalid_classifier.is_shown())\n\n    def test_input_instance(self):\n        \"\"\" Check data instance on input\"\"\"\n        self.send_signal(self.widget.Inputs.data, self.data)\n        self.assertIsNotNone(self.widget.instances)\n        self.send_signal(self.widget.Inputs.data, None)\n        self.assertIsNone(self.widget.instances)\n\n    def test_target_values(self):\n        \"\"\"Check Target class combo values\"\"\"\n        self.send_signal(self.widget.Inputs.classifier, self.nb_cls)\n        for i, text in enumerate(self.data.domain.class_var.values):\n            self.assertEqual(text, self.widget.class_combo.itemText(i))\n        self.send_signal(self.widget.Inputs.classifier, None)\n        self.assertEqual(0, self.widget.class_combo.count())\n\n    def test_nomogram_nb(self):\n        \"\"\"Check probabilities for naive bayes classifier for various values\n        of classes and radio buttons\"\"\"\n        self._test_helper(self.nb_cls, [54, 46])\n\n    def test_nomogram_lr(self):\n        \"\"\"Check probabilities for logistic regression classifier for various\n        values of classes and radio buttons\"\"\"\n        self.widget.display_index = 0  # show ALL features\n        self._test_helper(self.lr_cls, [58, 42])\n\n    def test_nomogram_nb_multiclass(self):\n        \"\"\"Check probabilities for naive bayes classifier for various values\n        of classes and radio buttons for multiclass data\"\"\"\n        cls = NaiveBayesLearner()(self.lenses)\n        self._test_helper(cls, [19, 53, 13])\n\n    def test_nomogram_lr_multiclass(self):\n        \"\"\"Check probabilities for logistic regression classifier for various\n        values of classes and radio buttons for multiclass data\"\"\"\n        cls = LogisticRegressionLearner(\n            multi_class=\"ovr\", solver=\"liblinear\"\n        )(self.lenses)\n        self._test_helper(cls, [9, 45, 52])\n\n    def test_nomogram_with_instance_nb(self):\n        \"\"\"Check initialized marker values and feature sorting for naive bayes\n        classifier and data on input\"\"\"\n        cls = NaiveBayesLearner()(self.titanic)\n        data = self.titanic[10:11]\n        self.send_signal(self.widget.Inputs.classifier, cls)\n        self.send_signal(self.widget.Inputs.data, data)\n        self._check_values(data.domain.attributes, data)\n        self._test_sort([[\"status\", \"age\", \"sex\"],\n                         [\"age\", \"sex\", \"status\"],\n                         [\"sex\", \"status\", \"age\"],\n                         [\"sex\", \"status\", \"age\"],\n                         [\"sex\", \"status\", \"age\"]])\n\n    def test_nomogram_with_instance_lr(self):\n        \"\"\"Check initialized marker values and feature sorting for logistic\n        regression classifier and data on input\"\"\"\n        cls = LogisticRegressionLearner()(self.titanic)\n        data = self.titanic[10:11]\n        self.send_signal(self.widget.Inputs.classifier, cls)\n        self.send_signal(self.widget.Inputs.data, data)\n        self._check_values(data.domain.attributes, data)\n        self._test_sort([[\"status\", \"age\", \"sex\"],\n                         [\"age\", \"sex\", \"status\"],\n                         [\"sex\", \"status\", \"age\"],\n                         [\"sex\", \"status\", \"age\"],\n                         [\"sex\", \"status\", \"age\"]])\n\n    def test_constant_feature_disc(self):\n        \"\"\"Check nomogram for data with constant discrete feature\"\"\"\n        domain = Domain([DiscreteVariable(\"d1\", (\"a\", \"c\")),\n                         DiscreteVariable(\"d2\", (\"b\",))],\n                        DiscreteVariable(\"cls\", (\"e\", \"d\")))\n        X = np.array([[0, 0], [1, 0], [0, 0], [1, 0]])\n        data = Table(domain, X, np.array([0, 1, 1, 0]))\n        cls = NaiveBayesLearner()(data)\n        self._test_helper(cls, [50, 50])\n        cls = LogisticRegressionLearner()(data)\n        self._test_helper(cls, [50, 50])\n\n    def test_constant_feature_cont(self):\n        \"\"\"Check nomogram for data with constant continuous feature\"\"\"\n        domain = Domain([DiscreteVariable(\"d\", (\"a\", \"b\")),\n                         ContinuousVariable(\"c\")],\n                        DiscreteVariable(\"cls\", (\"c\", \"d\")))\n        X = np.array([[0, 0], [1, 0], [0, 0], [1, 0]])\n        data = Table(domain, X, np.array([0, 1, 1, 0]))\n        cls = NaiveBayesLearner()(data)\n        self._test_helper(cls, [50, 50])\n        cls = LogisticRegressionLearner()(data)\n        self._test_helper(cls, [50, 50])\n\n    def _test_helper(self, cls, values):\n        self.send_signal(self.widget.Inputs.classifier, cls)\n\n        # check for all class values\n        for i in range(self.widget.class_combo.count()):\n            self.widget.class_combo.activated.emit(i)\n            self.widget.class_combo.setCurrentIndex(i)\n\n            # check probabilities marker value\n            self._test_helper_check_probability(values[i])\n\n            # point scale\n            self.widget.controls.scale.buttons[0].click()\n            self._test_helper_check_probability(values[i])\n\n            # best ranked\n            self.widget.n_attributes = 5\n            self.widget.controls.display_index.buttons[1].click()\n            visible_items = [item for item in self.widget.scene.items() if\n                             isinstance(item, (DiscreteFeatureItem,\n                                               ContinuousFeatureItem)) and\n                             item.isVisible()]\n            self.assertGreaterEqual(5, len(visible_items))\n\n            # 2D curve\n            self.widget.n_attributes = 15\n            self.widget.cont_feature_dim_combo.activated.emit(1)\n            self.widget.cont_feature_dim_combo.setCurrentIndex(1)\n            self._test_helper_check_probability(values[i])\n\n            # initial state\n            self.widget.controls.scale.buttons[1].click()\n            self.widget.controls.display_index.buttons[0].click()\n            self.widget.cont_feature_dim_combo.activated.emit(0)\n            self.widget.cont_feature_dim_combo.setCurrentIndex(0)\n\n    def _test_helper_check_probability(self, value):\n        prob_marker = [item for item in self.widget.scene.items() if\n                       isinstance(item, ProbabilitiesDotItem)][0]\n        self.assertIn(\"Probability: {}\".format(value),\n                      prob_marker.get_tooltip_text())\n\n    def _check_values(self, attributes, data):\n        for attr, item in zip(attributes, self.widget.feature_items.values()):\n            assert attr.name == item.childItems()[0].toPlainText()\n            value = data[0][attr.name].value\n            value = \"{}: 100%\".format(value) if attr.is_discrete \\\n                else \"Value: {}\".format(value)\n            self.assertIn(value, item.dot.get_tooltip_text())\n\n    def _test_sort(self, names):\n        for i in range(self.widget.sort_combo.count()):\n            self.widget.sort_combo.activated.emit(i)\n            self.widget.sort_combo.setCurrentIndex(i)\n            ordered = [self.widget.nomogram_main.layout().itemAt(i).childItems()[0].toPlainText()\n                       for i in range(self.widget.nomogram_main.layout().count())]\n            self.assertListEqual(names[i], ordered)\n\n    def test_tooltip(self):\n        # had problems on PyQt4\n        m = MovableToolTip()\n        m.show(QPoint(0, 0), \"Some text.\")\n\n    def test_output(self):\n        cls = LogisticRegressionLearner()(self.titanic)\n        data = self.titanic[10:11]\n        status, age, sex = data.domain.attributes\n        self.send_signal(self.widget.Inputs.classifier, cls)\n        self.widget.sort_combo.setCurrentIndex(1)\n        self.widget.sort_combo.activated.emit(1)\n\n        # Output more attributer than there are -> output all\n        self.widget.n_attributes = 5\n        self.widget.n_spin.valueChanged.emit(5)\n        attrs = self.get_output(self.widget.Outputs.features)\n        self.assertEqual(attrs, [age, sex, status])\n\n        # Output the first two\n        self.widget.n_attributes = 2\n        self.widget.n_spin.valueChanged.emit(2)\n        attrs = self.get_output(self.widget.Outputs.features)\n        self.assertEqual(attrs, [age, sex])\n\n        # Set to output all\n        self.widget.display_index = 0\n        self.widget.controls.display_index.group.buttonClicked[int].emit(0)\n        attrs = self.get_output(self.widget.Outputs.features)\n        self.assertEqual(attrs, [age, sex, status])\n\n        # Remove classifier -> output None\n        self.send_signal(self.widget.Inputs.classifier, None)\n        attrs = self.get_output(self.widget.Outputs.features)\n        self.assertIsNone(attrs)\n\n    def test_reset_settings(self):\n        self.widget.n_attributes = 5\n        self.widget.n_spin.valueChanged.emit(5)\n        self.widget.reset_settings()\n        self.assertEqual(10, self.widget.n_attributes)\n\n    @patch(\"Orange.widgets.visualize.ownomogram.QGraphicsTextItem\")\n    def test_adjust_scale(self, mocked_item: Mock):\n        def mocked_width():\n            nonlocal ind\n            ind += 1\n            most_right = {4: 30, 9: 59, 14: 59}\n            return [2, 30, 59, 2, most_right.get(ind)][ind % 5]\n\n        ind = -1\n        mocked_item().boundingRect().width.side_effect = mocked_width\n        attrs = [DiscreteVariable(\"var1\", values=(\"foo1\", \"foo2\")),\n                 DiscreteVariable(\"var2\", values=(\"foo3\", \"foo4\"))]\n        points = [np.array([0, 1.8]), np.array([1.5, 2.0])]\n        diff = np.max(points) - np.min(points)\n        # foo3 eventually overcomes foo2, while the scale is getting smaller\n        #\n        #  0                        1              1.5         1.8     2\n        # foo1                                          _______foo2_______\n        #                               __________foo3__________      foo4\n        self.widget._adjust_scale(attrs, points, 100, diff, [0, 1], [], 0)\n        # most left text at 1. iteration\n        self.assertEqual(mocked_item.call_args_list[5][0][0], \"foo2\")\n        # most left text at 2. iteration\n        self.assertEqual(mocked_item.call_args_list[10][0][0], \"foo3\")\n        # most left text at 3. iteration is the same -> stop\n        self.assertEqual(mocked_item.call_args_list[15][0][0], \"foo3\")\n\n    def test_summary(self):\n        \"\"\"Check if status bar is updated when data is received\"\"\"\n        data = self.data\n        info = self.widget.info\n        no_input = \"No data on input\"\n\n        self.send_signal(self.widget.Inputs.data, data)\n        summary, details = f\"{len(data)}\", format_summary_details(data)\n        self.assertEqual(info._StateInfo__input_summary.brief, summary)\n        self.assertEqual(info._StateInfo__input_summary.details, details)\n        self.send_signal(self.widget.Inputs.data, None)\n        self.assertIsInstance(info._StateInfo__input_summary, StateInfo.Empty)\n        self.assertEqual(info._StateInfo__input_summary.details, no_input)\n\n    def test_dots_stop_flashing(self):\n        self.widget.set_data(self.data)\n        self.widget.set_classifier(self.nb_cls)\n        animator = self.widget.dot_animator\n        dot = animator._GraphicsColorAnimator__items[0]\n        dot._mousePressFunc()\n        anim = animator._GraphicsColorAnimator__animation\n        self.assertNotEqual(anim.state(), QPropertyAnimation.Running)\n\n    def test_reconstruct_domain(self):\n        data = Table(\"heart_disease\")\n        cls = LogisticRegressionLearner()(data)\n        domain = OWNomogram.reconstruct_domain(cls.original_domain, cls.domain)\n        transformed_data = cls.original_data.transform(domain)\n        self.assertEqual(transformed_data.X.shape, data.X.shape)\n        self.assertFalse(np.isnan(transformed_data.X[0]).any())\n\n        scaled_data = Scale()(data)\n        cls = LogisticRegressionLearner()(scaled_data)\n        domain = OWNomogram.reconstruct_domain(cls.original_domain, cls.domain)\n        transformed_data = cls.original_data.transform(domain)\n        self.assertEqual(transformed_data.X.shape, scaled_data.X.shape)\n        self.assertFalse(np.isnan(transformed_data.X[0]).any())\n\n        disc_data = Continuize()(data)\n        cls = LogisticRegressionLearner()(disc_data)\n        domain = OWNomogram.reconstruct_domain(cls.original_domain, cls.domain)\n        transformed_data = cls.original_data.transform(domain)\n        self.assertEqual(transformed_data.X.shape, disc_data.X.shape)\n        self.assertFalse(np.isnan(transformed_data.X[0]).any())\n\n\nif __name__ == \"__main__\":\n    unittest.main()\n",
            "header": {
                "file": {
                    "path": "Orange/widgets/visualize/tests/test_ownomogram.py"
                },
                "misc_line": null
            },
            "index_line": {
                "old_commit_hash": "b2d51c4dc",
                "new_commit_hash": "9409cb8a9",
                "mode": "100644"
            },
            "is_binary_file": false,
            "binary_line": null,
            "minus_file": {
                "path": "a/Orange/widgets/visualize/tests/test_ownomogram.py"
            },
            "plus_file": {
                "path": "b/Orange/widgets/visualize/tests/test_ownomogram.py"
            },
            "hunks": [
                {
                    "descriptor": {
                        "old_range": {
                            "start": 13,
                            "length": 6
                        },
                        "new_range": {
                            "start": 13,
                            "length": 7
                        },
                        "section": "from Orange.data import Table, Domain, ContinuousVariable, DiscreteVariable"
                    },
                    "line_group": {
                        "all_lines": [
                            {
                                "content": "from Orange.classification import (",
                                "type": "context"
                            },
                            {
                                "content": "    NaiveBayesLearner, LogisticRegressionLearner, MajorityLearner",
                                "type": "context"
                            },
                            {
                                "content": ")",
                                "type": "context"
                            },
                            {
                                "content": "from Orange.preprocess import Scale, Continuize",
                                "type": "added"
                            },
                            {
                                "content": "from Orange.tests import test_filename",
                                "type": "context"
                            },
                            {
                                "content": "from Orange.widgets.tests.base import WidgetTest",
                                "type": "context"
                            },
                            {
                                "content": "from Orange.widgets.utils.state_summary import format_summary_details",
                                "type": "context"
                            }
                        ]
                    },
                    "modified_entities": [],
                    "added_entities": [
                        {
                            "file_name": "Orange/widgets/visualize/tests/test_ownomogram.py",
                            "type": "statement",
                            "name": "",
                            "content": "from Orange.preprocess import Scale, Continuize",
                            "ast_type_str": "ImportFrom",
                            "start_lineno": 16,
                            "end_lineno": 16,
                            "parent": null
                        }
                    ],
                    "deleted_entities": []
                },
                {
                    "descriptor": {
                        "old_range": {
                            "start": 306,
                            "length": 6
                        },
                        "new_range": {
                            "start": 307,
                            "length": 28
                        },
                        "section": "class TestOWNomogram(WidgetTest):"
                    },
                    "line_group": {
                        "all_lines": [
                            {
                                "content": "        anim = animator._GraphicsColorAnimator__animation",
                                "type": "context"
                            },
                            {
                                "content": "        self.assertNotEqual(anim.state(), QPropertyAnimation.Running)",
                                "type": "context"
                            },
                            {
                                "content": "",
                                "type": "context"
                            },
                            {
                                "content": "    def test_reconstruct_domain(self):",
                                "type": "added"
                            },
                            {
                                "content": "        data = Table(\"heart_disease\")",
                                "type": "added"
                            },
                            {
                                "content": "        cls = LogisticRegressionLearner()(data)",
                                "type": "added"
                            },
                            {
                                "content": "        domain = OWNomogram.reconstruct_domain(cls.original_domain, cls.domain)",
                                "type": "added"
                            },
                            {
                                "content": "        transformed_data = cls.original_data.transform(domain)",
                                "type": "added"
                            },
                            {
                                "content": "        self.assertEqual(transformed_data.X.shape, data.X.shape)",
                                "type": "added"
                            },
                            {
                                "content": "        self.assertFalse(np.isnan(transformed_data.X[0]).any())",
                                "type": "added"
                            },
                            {
                                "content": "",
                                "type": "added"
                            },
                            {
                                "content": "        scaled_data = Scale()(data)",
                                "type": "added"
                            },
                            {
                                "content": "        cls = LogisticRegressionLearner()(scaled_data)",
                                "type": "added"
                            },
                            {
                                "content": "        domain = OWNomogram.reconstruct_domain(cls.original_domain, cls.domain)",
                                "type": "added"
                            },
                            {
                                "content": "        transformed_data = cls.original_data.transform(domain)",
                                "type": "added"
                            },
                            {
                                "content": "        self.assertEqual(transformed_data.X.shape, scaled_data.X.shape)",
                                "type": "added"
                            },
                            {
                                "content": "        self.assertFalse(np.isnan(transformed_data.X[0]).any())",
                                "type": "added"
                            },
                            {
                                "content": "",
                                "type": "added"
                            },
                            {
                                "content": "        disc_data = Continuize()(data)",
                                "type": "added"
                            },
                            {
                                "content": "        cls = LogisticRegressionLearner()(disc_data)",
                                "type": "added"
                            },
                            {
                                "content": "        domain = OWNomogram.reconstruct_domain(cls.original_domain, cls.domain)",
                                "type": "added"
                            },
                            {
                                "content": "        transformed_data = cls.original_data.transform(domain)",
                                "type": "added"
                            },
                            {
                                "content": "        self.assertEqual(transformed_data.X.shape, disc_data.X.shape)",
                                "type": "added"
                            },
                            {
                                "content": "        self.assertFalse(np.isnan(transformed_data.X[0]).any())",
                                "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/visualize/tests/test_ownomogram.py",
                            "type": "class",
                            "name": "TestOWNomogram",
                            "content": "class TestOWNomogram(WidgetTest):\n    @classmethod\n    def setUpClass(cls):\n        super().setUpClass()\n        cls.data = Table(\"heart_disease\")\n        cls.nb_cls = NaiveBayesLearner()(cls.data)\n        cls.lr_cls = LogisticRegressionLearner()(cls.data)\n        cls.titanic = Table(\"titanic\")\n        cls.lenses = Table(test_filename(\"datasets/lenses.tab\"))\n\n    def setUp(self):\n        self.widget = self.create_widget(OWNomogram)  # type: OWNomogram\n\n    def test_input_nb_cls(self):\n        \"\"\"Check naive bayes classifier on input\"\"\"\n        self.send_signal(self.widget.Inputs.classifier, self.nb_cls)\n        self.assertEqual(len([item for item in self.widget.scene.items() if\n                              isinstance(item, DiscreteFeatureItem)]),\n                         min(self.widget.n_attributes,\n                             len(self.data.domain.attributes)))\n\n    def test_input_lr_cls(self):\n        \"\"\"Check logistic regression classifier on input\"\"\"\n        self.widget.display_index = 0  # display ALL features\n        self.send_signal(self.widget.Inputs.classifier, self.lr_cls)\n        self.assertEqual(\n            len([item for item in self.widget.scene.items() if\n                 isinstance(item, DiscreteFeatureItem)]),\n            len([a for a in self.data.domain.attributes if a.is_discrete]))\n        self.assertEqual(\n            len([item for item in self.widget.scene.items() if\n                 isinstance(item, ContinuousFeatureItem)]),\n            len([a for a in self.data.domain.attributes if a.is_continuous]))\n\n    def test_input_invalid_cls(self):\n        \"\"\"Check any classifier on input\"\"\"\n        majority_cls = MajorityLearner()(self.data)\n        self.send_signal(self.widget.Inputs.classifier, majority_cls)\n        self.assertTrue(self.widget.Error.invalid_classifier.is_shown())\n        self.send_signal(self.widget.Inputs.classifier, None)\n        self.assertFalse(self.widget.Error.invalid_classifier.is_shown())\n\n    def test_input_instance(self):\n        \"\"\" Check data instance on input\"\"\"\n        self.send_signal(self.widget.Inputs.data, self.data)\n        self.assertIsNotNone(self.widget.instances)\n        self.send_signal(self.widget.Inputs.data, None)\n        self.assertIsNone(self.widget.instances)\n\n    def test_target_values(self):\n        \"\"\"Check Target class combo values\"\"\"\n        self.send_signal(self.widget.Inputs.classifier, self.nb_cls)\n        for i, text in enumerate(self.data.domain.class_var.values):\n            self.assertEqual(text, self.widget.class_combo.itemText(i))\n        self.send_signal(self.widget.Inputs.classifier, None)\n        self.assertEqual(0, self.widget.class_combo.count())\n\n    def test_nomogram_nb(self):\n        \"\"\"Check probabilities for naive bayes classifier for various values\n        of classes and radio buttons\"\"\"\n        self._test_helper(self.nb_cls, [54, 46])\n\n    def test_nomogram_lr(self):\n        \"\"\"Check probabilities for logistic regression classifier for various\n        values of classes and radio buttons\"\"\"\n        self.widget.display_index = 0  # show ALL features\n        self._test_helper(self.lr_cls, [58, 42])\n\n    def test_nomogram_nb_multiclass(self):\n        \"\"\"Check probabilities for naive bayes classifier for various values\n        of classes and radio buttons for multiclass data\"\"\"\n        cls = NaiveBayesLearner()(self.lenses)\n        self._test_helper(cls, [19, 53, 13])\n\n    def test_nomogram_lr_multiclass(self):\n        \"\"\"Check probabilities for logistic regression classifier for various\n        values of classes and radio buttons for multiclass data\"\"\"\n        cls = LogisticRegressionLearner(\n            multi_class=\"ovr\", solver=\"liblinear\"\n        )(self.lenses)\n        self._test_helper(cls, [9, 45, 52])\n\n    def test_nomogram_with_instance_nb(self):\n        \"\"\"Check initialized marker values and feature sorting for naive bayes\n        classifier and data on input\"\"\"\n        cls = NaiveBayesLearner()(self.titanic)\n        data = self.titanic[10:11]\n        self.send_signal(self.widget.Inputs.classifier, cls)\n        self.send_signal(self.widget.Inputs.data, data)\n        self._check_values(data.domain.attributes, data)\n        self._test_sort([[\"status\", \"age\", \"sex\"],\n                         [\"age\", \"sex\", \"status\"],\n                         [\"sex\", \"status\", \"age\"],\n                         [\"sex\", \"status\", \"age\"],\n                         [\"sex\", \"status\", \"age\"]])\n\n    def test_nomogram_with_instance_lr(self):\n        \"\"\"Check initialized marker values and feature sorting for logistic\n        regression classifier and data on input\"\"\"\n        cls = LogisticRegressionLearner()(self.titanic)\n        data = self.titanic[10:11]\n        self.send_signal(self.widget.Inputs.classifier, cls)\n        self.send_signal(self.widget.Inputs.data, data)\n        self._check_values(data.domain.attributes, data)\n        self._test_sort([[\"status\", \"age\", \"sex\"],\n                         [\"age\", \"sex\", \"status\"],\n                         [\"sex\", \"status\", \"age\"],\n                         [\"sex\", \"status\", \"age\"],\n                         [\"sex\", \"status\", \"age\"]])\n\n    def test_constant_feature_disc(self):\n        \"\"\"Check nomogram for data with constant discrete feature\"\"\"\n        domain = Domain([DiscreteVariable(\"d1\", (\"a\", \"c\")),\n                         DiscreteVariable(\"d2\", (\"b\",))],\n                        DiscreteVariable(\"cls\", (\"e\", \"d\")))\n        X = np.array([[0, 0], [1, 0], [0, 0], [1, 0]])\n        data = Table(domain, X, np.array([0, 1, 1, 0]))\n        cls = NaiveBayesLearner()(data)\n        self._test_helper(cls, [50, 50])\n        cls = LogisticRegressionLearner()(data)\n        self._test_helper(cls, [50, 50])\n\n    def test_constant_feature_cont(self):\n        \"\"\"Check nomogram for data with constant continuous feature\"\"\"\n        domain = Domain([DiscreteVariable(\"d\", (\"a\", \"b\")),\n                         ContinuousVariable(\"c\")],\n                        DiscreteVariable(\"cls\", (\"c\", \"d\")))\n        X = np.array([[0, 0], [1, 0], [0, 0], [1, 0]])\n        data = Table(domain, X, np.array([0, 1, 1, 0]))\n        cls = NaiveBayesLearner()(data)\n        self._test_helper(cls, [50, 50])\n        cls = LogisticRegressionLearner()(data)\n        self._test_helper(cls, [50, 50])\n\n    def _test_helper(self, cls, values):\n        self.send_signal(self.widget.Inputs.classifier, cls)\n\n        # check for all class values\n        for i in range(self.widget.class_combo.count()):\n            self.widget.class_combo.activated.emit(i)\n            self.widget.class_combo.setCurrentIndex(i)\n\n            # check probabilities marker value\n            self._test_helper_check_probability(values[i])\n\n            # point scale\n            self.widget.controls.scale.buttons[0].click()\n            self._test_helper_check_probability(values[i])\n\n            # best ranked\n            self.widget.n_attributes = 5\n            self.widget.controls.display_index.buttons[1].click()\n            visible_items = [item for item in self.widget.scene.items() if\n                             isinstance(item, (DiscreteFeatureItem,\n                                               ContinuousFeatureItem)) and\n                             item.isVisible()]\n            self.assertGreaterEqual(5, len(visible_items))\n\n            # 2D curve\n            self.widget.n_attributes = 15\n            self.widget.cont_feature_dim_combo.activated.emit(1)\n            self.widget.cont_feature_dim_combo.setCurrentIndex(1)\n            self._test_helper_check_probability(values[i])\n\n            # initial state\n            self.widget.controls.scale.buttons[1].click()\n            self.widget.controls.display_index.buttons[0].click()\n            self.widget.cont_feature_dim_combo.activated.emit(0)\n            self.widget.cont_feature_dim_combo.setCurrentIndex(0)\n\n    def _test_helper_check_probability(self, value):\n        prob_marker = [item for item in self.widget.scene.items() if\n                       isinstance(item, ProbabilitiesDotItem)][0]\n        self.assertIn(\"Probability: {}\".format(value),\n                      prob_marker.get_tooltip_text())\n\n    def _check_values(self, attributes, data):\n        for attr, item in zip(attributes, self.widget.feature_items.values()):\n            assert attr.name == item.childItems()[0].toPlainText()\n            value = data[0][attr.name].value\n            value = \"{}: 100%\".format(value) if attr.is_discrete \\\n                else \"Value: {}\".format(value)\n            self.assertIn(value, item.dot.get_tooltip_text())\n\n    def _test_sort(self, names):\n        for i in range(self.widget.sort_combo.count()):\n            self.widget.sort_combo.activated.emit(i)\n            self.widget.sort_combo.setCurrentIndex(i)\n            ordered = [self.widget.nomogram_main.layout().itemAt(i).childItems()[0].toPlainText()\n                       for i in range(self.widget.nomogram_main.layout().count())]\n            self.assertListEqual(names[i], ordered)\n\n    def test_tooltip(self):\n        # had problems on PyQt4\n        m = MovableToolTip()\n        m.show(QPoint(0, 0), \"Some text.\")\n\n    def test_output(self):\n        cls = LogisticRegressionLearner()(self.titanic)\n        data = self.titanic[10:11]\n        status, age, sex = data.domain.attributes\n        self.send_signal(self.widget.Inputs.classifier, cls)\n        self.widget.sort_combo.setCurrentIndex(1)\n        self.widget.sort_combo.activated.emit(1)\n\n        # Output more attributer than there are -> output all\n        self.widget.n_attributes = 5\n        self.widget.n_spin.valueChanged.emit(5)\n        attrs = self.get_output(self.widget.Outputs.features)\n        self.assertEqual(attrs, [age, sex, status])\n\n        # Output the first two\n        self.widget.n_attributes = 2\n        self.widget.n_spin.valueChanged.emit(2)\n        attrs = self.get_output(self.widget.Outputs.features)\n        self.assertEqual(attrs, [age, sex])\n\n        # Set to output all\n        self.widget.display_index = 0\n        self.widget.controls.display_index.group.buttonClicked[int].emit(0)\n        attrs = self.get_output(self.widget.Outputs.features)\n        self.assertEqual(attrs, [age, sex, status])\n\n        # Remove classifier -> output None\n        self.send_signal(self.widget.Inputs.classifier, None)\n        attrs = self.get_output(self.widget.Outputs.features)\n        self.assertIsNone(attrs)\n\n    def test_reset_settings(self):\n        self.widget.n_attributes = 5\n        self.widget.n_spin.valueChanged.emit(5)\n        self.widget.reset_settings()\n        self.assertEqual(10, self.widget.n_attributes)\n\n    @patch(\"Orange.widgets.visualize.ownomogram.QGraphicsTextItem\")\n    def test_adjust_scale(self, mocked_item: Mock):\n        def mocked_width():\n            nonlocal ind\n            ind += 1\n            most_right = {4: 30, 9: 59, 14: 59}\n            return [2, 30, 59, 2, most_right.get(ind)][ind % 5]\n\n        ind = -1\n        mocked_item().boundingRect().width.side_effect = mocked_width\n        attrs = [DiscreteVariable(\"var1\", values=(\"foo1\", \"foo2\")),\n                 DiscreteVariable(\"var2\", values=(\"foo3\", \"foo4\"))]\n        points = [np.array([0, 1.8]), np.array([1.5, 2.0])]\n        diff = np.max(points) - np.min(points)\n        # foo3 eventually overcomes foo2, while the scale is getting smaller\n        #\n        #  0                        1              1.5         1.8     2\n        # foo1                                          _______foo2_______\n        #                               __________foo3__________      foo4\n        self.widget._adjust_scale(attrs, points, 100, diff, [0, 1], [], 0)\n        # most left text at 1. iteration\n        self.assertEqual(mocked_item.call_args_list[5][0][0], \"foo2\")\n        # most left text at 2. iteration\n        self.assertEqual(mocked_item.call_args_list[10][0][0], \"foo3\")\n        # most left text at 3. iteration is the same -> stop\n        self.assertEqual(mocked_item.call_args_list[15][0][0], \"foo3\")\n\n    def test_summary(self):\n        \"\"\"Check if status bar is updated when data is received\"\"\"\n        data = self.data\n        info = self.widget.info\n        no_input = \"No data on input\"\n\n        self.send_signal(self.widget.Inputs.data, data)\n        summary, details = f\"{len(data)}\", format_summary_details(data)\n        self.assertEqual(info._StateInfo__input_summary.brief, summary)\n        self.assertEqual(info._StateInfo__input_summary.details, details)\n        self.send_signal(self.widget.Inputs.data, None)\n        self.assertIsInstance(info._StateInfo__input_summary, StateInfo.Empty)\n        self.assertEqual(info._StateInfo__input_summary.details, no_input)\n\n    def test_dots_stop_flashing(self):\n        self.widget.set_data(self.data)\n        self.widget.set_classifier(self.nb_cls)\n        animator = self.widget.dot_animator\n        dot = animator._GraphicsColorAnimator__items[0]\n        dot._mousePressFunc()\n        anim = animator._GraphicsColorAnimator__animation\n        self.assertNotEqual(anim.state(), QPropertyAnimation.Running)\n\n    def test_reconstruct_domain(self):\n        data = Table(\"heart_disease\")\n        cls = LogisticRegressionLearner()(data)\n        domain = OWNomogram.reconstruct_domain(cls.original_domain, cls.domain)\n        transformed_data = cls.original_data.transform(domain)\n        self.assertEqual(transformed_data.X.shape, data.X.shape)\n        self.assertFalse(np.isnan(transformed_data.X[0]).any())\n\n        scaled_data = Scale()(data)\n        cls = LogisticRegressionLearner()(scaled_data)\n        domain = OWNomogram.reconstruct_domain(cls.original_domain, cls.domain)\n        transformed_data = cls.original_data.transform(domain)\n        self.assertEqual(transformed_data.X.shape, scaled_data.X.shape)\n        self.assertFalse(np.isnan(transformed_data.X[0]).any())\n\n        disc_data = Continuize()(data)\n        cls = LogisticRegressionLearner()(disc_data)\n        domain = OWNomogram.reconstruct_domain(cls.original_domain, cls.domain)\n        transformed_data = cls.original_data.transform(domain)\n        self.assertEqual(transformed_data.X.shape, disc_data.X.shape)\n        self.assertFalse(np.isnan(transformed_data.X[0]).any())",
                            "ast_type_str": "ClassDef",
                            "start_lineno": 26,
                            "end_lineno": 330,
                            "parent": null
                        }
                    ],
                    "added_entities": [
                        {
                            "file_name": "Orange/widgets/visualize/tests/test_ownomogram.py",
                            "type": "method",
                            "name": "TestOWNomogram.test_reconstruct_domain",
                            "content": "class TestOWNomogram(WidgetTest):\n    @classmethod\n    def setUpClass(cls):\n        super().setUpClass()\n        cls.data = Table(\"heart_disease\")\n        cls.nb_cls = NaiveBayesLearner()(cls.data)\n        cls.lr_cls = LogisticRegressionLearner()(cls.data)\n        cls.titanic = Table(\"titanic\")\n        cls.lenses = Table(test_filename(\"datasets/lenses.tab\"))\n\n    def setUp(self):\n        self.widget = self.create_widget(OWNomogram)  # type: OWNomogram\n\n    def test_input_nb_cls(self):\n        \"\"\"Check naive bayes classifier on input\"\"\"\n        self.send_signal(self.widget.Inputs.classifier, self.nb_cls)\n        self.assertEqual(len([item for item in self.widget.scene.items() if\n                              isinstance(item, DiscreteFeatureItem)]),\n                         min(self.widget.n_attributes,\n                             len(self.data.domain.attributes)))\n\n    def test_input_lr_cls(self):\n        \"\"\"Check logistic regression classifier on input\"\"\"\n        self.widget.display_index = 0  # display ALL features\n        self.send_signal(self.widget.Inputs.classifier, self.lr_cls)\n        self.assertEqual(\n            len([item for item in self.widget.scene.items() if\n                 isinstance(item, DiscreteFeatureItem)]),\n            len([a for a in self.data.domain.attributes if a.is_discrete]))\n        self.assertEqual(\n            len([item for item in self.widget.scene.items() if\n                 isinstance(item, ContinuousFeatureItem)]),\n            len([a for a in self.data.domain.attributes if a.is_continuous]))\n\n    def test_input_invalid_cls(self):\n        \"\"\"Check any classifier on input\"\"\"\n        majority_cls = MajorityLearner()(self.data)\n        self.send_signal(self.widget.Inputs.classifier, majority_cls)\n        self.assertTrue(self.widget.Error.invalid_classifier.is_shown())\n        self.send_signal(self.widget.Inputs.classifier, None)\n        self.assertFalse(self.widget.Error.invalid_classifier.is_shown())\n\n    def test_input_instance(self):\n        \"\"\" Check data instance on input\"\"\"\n        self.send_signal(self.widget.Inputs.data, self.data)\n        self.assertIsNotNone(self.widget.instances)\n        self.send_signal(self.widget.Inputs.data, None)\n        self.assertIsNone(self.widget.instances)\n\n    def test_target_values(self):\n        \"\"\"Check Target class combo values\"\"\"\n        self.send_signal(self.widget.Inputs.classifier, self.nb_cls)\n        for i, text in enumerate(self.data.domain.class_var.values):\n            self.assertEqual(text, self.widget.class_combo.itemText(i))\n        self.send_signal(self.widget.Inputs.classifier, None)\n        self.assertEqual(0, self.widget.class_combo.count())\n\n    def test_nomogram_nb(self):\n        \"\"\"Check probabilities for naive bayes classifier for various values\n        of classes and radio buttons\"\"\"\n        self._test_helper(self.nb_cls, [54, 46])\n\n    def test_nomogram_lr(self):\n        \"\"\"Check probabilities for logistic regression classifier for various\n        values of classes and radio buttons\"\"\"\n        self.widget.display_index = 0  # show ALL features\n        self._test_helper(self.lr_cls, [58, 42])\n\n    def test_nomogram_nb_multiclass(self):\n        \"\"\"Check probabilities for naive bayes classifier for various values\n        of classes and radio buttons for multiclass data\"\"\"\n        cls = NaiveBayesLearner()(self.lenses)\n        self._test_helper(cls, [19, 53, 13])\n\n    def test_nomogram_lr_multiclass(self):\n        \"\"\"Check probabilities for logistic regression classifier for various\n        values of classes and radio buttons for multiclass data\"\"\"\n        cls = LogisticRegressionLearner(\n            multi_class=\"ovr\", solver=\"liblinear\"\n        )(self.lenses)\n        self._test_helper(cls, [9, 45, 52])\n\n    def test_nomogram_with_instance_nb(self):\n        \"\"\"Check initialized marker values and feature sorting for naive bayes\n        classifier and data on input\"\"\"\n        cls = NaiveBayesLearner()(self.titanic)\n        data = self.titanic[10:11]\n        self.send_signal(self.widget.Inputs.classifier, cls)\n        self.send_signal(self.widget.Inputs.data, data)\n        self._check_values(data.domain.attributes, data)\n        self._test_sort([[\"status\", \"age\", \"sex\"],\n                         [\"age\", \"sex\", \"status\"],\n                         [\"sex\", \"status\", \"age\"],\n                         [\"sex\", \"status\", \"age\"],\n                         [\"sex\", \"status\", \"age\"]])\n\n    def test_nomogram_with_instance_lr(self):\n        \"\"\"Check initialized marker values and feature sorting for logistic\n        regression classifier and data on input\"\"\"\n        cls = LogisticRegressionLearner()(self.titanic)\n        data = self.titanic[10:11]\n        self.send_signal(self.widget.Inputs.classifier, cls)\n        self.send_signal(self.widget.Inputs.data, data)\n        self._check_values(data.domain.attributes, data)\n        self._test_sort([[\"status\", \"age\", \"sex\"],\n                         [\"age\", \"sex\", \"status\"],\n                         [\"sex\", \"status\", \"age\"],\n                         [\"sex\", \"status\", \"age\"],\n                         [\"sex\", \"status\", \"age\"]])\n\n    def test_constant_feature_disc(self):\n        \"\"\"Check nomogram for data with constant discrete feature\"\"\"\n        domain = Domain([DiscreteVariable(\"d1\", (\"a\", \"c\")),\n                         DiscreteVariable(\"d2\", (\"b\",))],\n                        DiscreteVariable(\"cls\", (\"e\", \"d\")))\n        X = np.array([[0, 0], [1, 0], [0, 0], [1, 0]])\n        data = Table(domain, X, np.array([0, 1, 1, 0]))\n        cls = NaiveBayesLearner()(data)\n        self._test_helper(cls, [50, 50])\n        cls = LogisticRegressionLearner()(data)\n        self._test_helper(cls, [50, 50])\n\n    def test_constant_feature_cont(self):\n        \"\"\"Check nomogram for data with constant continuous feature\"\"\"\n        domain = Domain([DiscreteVariable(\"d\", (\"a\", \"b\")),\n                         ContinuousVariable(\"c\")],\n                        DiscreteVariable(\"cls\", (\"c\", \"d\")))\n        X = np.array([[0, 0], [1, 0], [0, 0], [1, 0]])\n        data = Table(domain, X, np.array([0, 1, 1, 0]))\n        cls = NaiveBayesLearner()(data)\n        self._test_helper(cls, [50, 50])\n        cls = LogisticRegressionLearner()(data)\n        self._test_helper(cls, [50, 50])\n\n    def _test_helper(self, cls, values):\n        self.send_signal(self.widget.Inputs.classifier, cls)\n\n        # check for all class values\n        for i in range(self.widget.class_combo.count()):\n            self.widget.class_combo.activated.emit(i)\n            self.widget.class_combo.setCurrentIndex(i)\n\n            # check probabilities marker value\n            self._test_helper_check_probability(values[i])\n\n            # point scale\n            self.widget.controls.scale.buttons[0].click()\n            self._test_helper_check_probability(values[i])\n\n            # best ranked\n            self.widget.n_attributes = 5\n            self.widget.controls.display_index.buttons[1].click()\n            visible_items = [item for item in self.widget.scene.items() if\n                             isinstance(item, (DiscreteFeatureItem,\n                                               ContinuousFeatureItem)) and\n                             item.isVisible()]\n            self.assertGreaterEqual(5, len(visible_items))\n\n            # 2D curve\n            self.widget.n_attributes = 15\n            self.widget.cont_feature_dim_combo.activated.emit(1)\n            self.widget.cont_feature_dim_combo.setCurrentIndex(1)\n            self._test_helper_check_probability(values[i])\n\n            # initial state\n            self.widget.controls.scale.buttons[1].click()\n            self.widget.controls.display_index.buttons[0].click()\n            self.widget.cont_feature_dim_combo.activated.emit(0)\n            self.widget.cont_feature_dim_combo.setCurrentIndex(0)\n\n    def _test_helper_check_probability(self, value):\n        prob_marker = [item for item in self.widget.scene.items() if\n                       isinstance(item, ProbabilitiesDotItem)][0]\n        self.assertIn(\"Probability: {}\".format(value),\n                      prob_marker.get_tooltip_text())\n\n    def _check_values(self, attributes, data):\n        for attr, item in zip(attributes, self.widget.feature_items.values()):\n            assert attr.name == item.childItems()[0].toPlainText()\n            value = data[0][attr.name].value\n            value = \"{}: 100%\".format(value) if attr.is_discrete \\\n                else \"Value: {}\".format(value)\n            self.assertIn(value, item.dot.get_tooltip_text())\n\n    def _test_sort(self, names):\n        for i in range(self.widget.sort_combo.count()):\n            self.widget.sort_combo.activated.emit(i)\n            self.widget.sort_combo.setCurrentIndex(i)\n            ordered = [self.widget.nomogram_main.layout().itemAt(i).childItems()[0].toPlainText()\n                       for i in range(self.widget.nomogram_main.layout().count())]\n            self.assertListEqual(names[i], ordered)\n\n    def test_tooltip(self):\n        # had problems on PyQt4\n        m = MovableToolTip()\n        m.show(QPoint(0, 0), \"Some text.\")\n\n    def test_output(self):\n        cls = LogisticRegressionLearner()(self.titanic)\n        data = self.titanic[10:11]\n        status, age, sex = data.domain.attributes\n        self.send_signal(self.widget.Inputs.classifier, cls)\n        self.widget.sort_combo.setCurrentIndex(1)\n        self.widget.sort_combo.activated.emit(1)\n\n        # Output more attributer than there are -> output all\n        self.widget.n_attributes = 5\n        self.widget.n_spin.valueChanged.emit(5)\n        attrs = self.get_output(self.widget.Outputs.features)\n        self.assertEqual(attrs, [age, sex, status])\n\n        # Output the first two\n        self.widget.n_attributes = 2\n        self.widget.n_spin.valueChanged.emit(2)\n        attrs = self.get_output(self.widget.Outputs.features)\n        self.assertEqual(attrs, [age, sex])\n\n        # Set to output all\n        self.widget.display_index = 0\n        self.widget.controls.display_index.group.buttonClicked[int].emit(0)\n        attrs = self.get_output(self.widget.Outputs.features)\n        self.assertEqual(attrs, [age, sex, status])\n\n        # Remove classifier -> output None\n        self.send_signal(self.widget.Inputs.classifier, None)\n        attrs = self.get_output(self.widget.Outputs.features)\n        self.assertIsNone(attrs)\n\n    def test_reset_settings(self):\n        self.widget.n_attributes = 5\n        self.widget.n_spin.valueChanged.emit(5)\n        self.widget.reset_settings()\n        self.assertEqual(10, self.widget.n_attributes)\n\n    @patch(\"Orange.widgets.visualize.ownomogram.QGraphicsTextItem\")\n    def test_adjust_scale(self, mocked_item: Mock):\n        def mocked_width():\n            nonlocal ind\n            ind += 1\n            most_right = {4: 30, 9: 59, 14: 59}\n            return [2, 30, 59, 2, most_right.get(ind)][ind % 5]\n\n        ind = -1\n        mocked_item().boundingRect().width.side_effect = mocked_width\n        attrs = [DiscreteVariable(\"var1\", values=(\"foo1\", \"foo2\")),\n                 DiscreteVariable(\"var2\", values=(\"foo3\", \"foo4\"))]\n        points = [np.array([0, 1.8]), np.array([1.5, 2.0])]\n        diff = np.max(points) - np.min(points)\n        # foo3 eventually overcomes foo2, while the scale is getting smaller\n        #\n        #  0                        1              1.5         1.8     2\n        # foo1                                          _______foo2_______\n        #                               __________foo3__________      foo4\n        self.widget._adjust_scale(attrs, points, 100, diff, [0, 1], [], 0)\n        # most left text at 1. iteration\n        self.assertEqual(mocked_item.call_args_list[5][0][0], \"foo2\")\n        # most left text at 2. iteration\n        self.assertEqual(mocked_item.call_args_list[10][0][0], \"foo3\")\n        # most left text at 3. iteration is the same -> stop\n        self.assertEqual(mocked_item.call_args_list[15][0][0], \"foo3\")\n\n    def test_summary(self):\n        \"\"\"Check if status bar is updated when data is received\"\"\"\n        data = self.data\n        info = self.widget.info\n        no_input = \"No data on input\"\n\n        self.send_signal(self.widget.Inputs.data, data)\n        summary, details = f\"{len(data)}\", format_summary_details(data)\n        self.assertEqual(info._StateInfo__input_summary.brief, summary)\n        self.assertEqual(info._StateInfo__input_summary.details, details)\n        self.send_signal(self.widget.Inputs.data, None)\n        self.assertIsInstance(info._StateInfo__input_summary, StateInfo.Empty)\n        self.assertEqual(info._StateInfo__input_summary.details, no_input)\n\n    def test_dots_stop_flashing(self):\n        self.widget.set_data(self.data)\n        self.widget.set_classifier(self.nb_cls)\n        animator = self.widget.dot_animator\n        dot = animator._GraphicsColorAnimator__items[0]\n        dot._mousePressFunc()\n        anim = animator._GraphicsColorAnimator__animation\n        self.assertNotEqual(anim.state(), QPropertyAnimation.Running)\n\n    def test_reconstruct_domain(self):\n        data = Table(\"heart_disease\")\n        cls = LogisticRegressionLearner()(data)\n        domain = OWNomogram.reconstruct_domain(cls.original_domain, cls.domain)\n        transformed_data = cls.original_data.transform(domain)\n        self.assertEqual(transformed_data.X.shape, data.X.shape)\n        self.assertFalse(np.isnan(transformed_data.X[0]).any())\n\n        scaled_data = Scale()(data)\n        cls = LogisticRegressionLearner()(scaled_data)\n        domain = OWNomogram.reconstruct_domain(cls.original_domain, cls.domain)\n        transformed_data = cls.original_data.transform(domain)\n        self.assertEqual(transformed_data.X.shape, scaled_data.X.shape)\n        self.assertFalse(np.isnan(transformed_data.X[0]).any())\n\n        disc_data = Continuize()(data)\n        cls = LogisticRegressionLearner()(disc_data)\n        domain = OWNomogram.reconstruct_domain(cls.original_domain, cls.domain)\n        transformed_data = cls.original_data.transform(domain)\n        self.assertEqual(transformed_data.X.shape, disc_data.X.shape)\n        self.assertFalse(np.isnan(transformed_data.X[0]).any())",
                            "ast_type_str": "ClassDef",
                            "start_lineno": 310,
                            "end_lineno": 330,
                            "parent": {
                                "file_name": "Orange/widgets/visualize/tests/test_ownomogram.py",
                                "type": "class",
                                "name": "TestOWNomogram",
                                "content": "class TestOWNomogram(WidgetTest):\n    @classmethod\n    def setUpClass(cls):\n        super().setUpClass()\n        cls.data = Table(\"heart_disease\")\n        cls.nb_cls = NaiveBayesLearner()(cls.data)\n        cls.lr_cls = LogisticRegressionLearner()(cls.data)\n        cls.titanic = Table(\"titanic\")\n        cls.lenses = Table(test_filename(\"datasets/lenses.tab\"))\n\n    def setUp(self):\n        self.widget = self.create_widget(OWNomogram)  # type: OWNomogram\n\n    def test_input_nb_cls(self):\n        \"\"\"Check naive bayes classifier on input\"\"\"\n        self.send_signal(self.widget.Inputs.classifier, self.nb_cls)\n        self.assertEqual(len([item for item in self.widget.scene.items() if\n                              isinstance(item, DiscreteFeatureItem)]),\n                         min(self.widget.n_attributes,\n                             len(self.data.domain.attributes)))\n\n    def test_input_lr_cls(self):\n        \"\"\"Check logistic regression classifier on input\"\"\"\n        self.widget.display_index = 0  # display ALL features\n        self.send_signal(self.widget.Inputs.classifier, self.lr_cls)\n        self.assertEqual(\n            len([item for item in self.widget.scene.items() if\n                 isinstance(item, DiscreteFeatureItem)]),\n            len([a for a in self.data.domain.attributes if a.is_discrete]))\n        self.assertEqual(\n            len([item for item in self.widget.scene.items() if\n                 isinstance(item, ContinuousFeatureItem)]),\n            len([a for a in self.data.domain.attributes if a.is_continuous]))\n\n    def test_input_invalid_cls(self):\n        \"\"\"Check any classifier on input\"\"\"\n        majority_cls = MajorityLearner()(self.data)\n        self.send_signal(self.widget.Inputs.classifier, majority_cls)\n        self.assertTrue(self.widget.Error.invalid_classifier.is_shown())\n        self.send_signal(self.widget.Inputs.classifier, None)\n        self.assertFalse(self.widget.Error.invalid_classifier.is_shown())\n\n    def test_input_instance(self):\n        \"\"\" Check data instance on input\"\"\"\n        self.send_signal(self.widget.Inputs.data, self.data)\n        self.assertIsNotNone(self.widget.instances)\n        self.send_signal(self.widget.Inputs.data, None)\n        self.assertIsNone(self.widget.instances)\n\n    def test_target_values(self):\n        \"\"\"Check Target class combo values\"\"\"\n        self.send_signal(self.widget.Inputs.classifier, self.nb_cls)\n        for i, text in enumerate(self.data.domain.class_var.values):\n            self.assertEqual(text, self.widget.class_combo.itemText(i))\n        self.send_signal(self.widget.Inputs.classifier, None)\n        self.assertEqual(0, self.widget.class_combo.count())\n\n    def test_nomogram_nb(self):\n        \"\"\"Check probabilities for naive bayes classifier for various values\n        of classes and radio buttons\"\"\"\n        self._test_helper(self.nb_cls, [54, 46])\n\n    def test_nomogram_lr(self):\n        \"\"\"Check probabilities for logistic regression classifier for various\n        values of classes and radio buttons\"\"\"\n        self.widget.display_index = 0  # show ALL features\n        self._test_helper(self.lr_cls, [58, 42])\n\n    def test_nomogram_nb_multiclass(self):\n        \"\"\"Check probabilities for naive bayes classifier for various values\n        of classes and radio buttons for multiclass data\"\"\"\n        cls = NaiveBayesLearner()(self.lenses)\n        self._test_helper(cls, [19, 53, 13])\n\n    def test_nomogram_lr_multiclass(self):\n        \"\"\"Check probabilities for logistic regression classifier for various\n        values of classes and radio buttons for multiclass data\"\"\"\n        cls = LogisticRegressionLearner(\n            multi_class=\"ovr\", solver=\"liblinear\"\n        )(self.lenses)\n        self._test_helper(cls, [9, 45, 52])\n\n    def test_nomogram_with_instance_nb(self):\n        \"\"\"Check initialized marker values and feature sorting for naive bayes\n        classifier and data on input\"\"\"\n        cls = NaiveBayesLearner()(self.titanic)\n        data = self.titanic[10:11]\n        self.send_signal(self.widget.Inputs.classifier, cls)\n        self.send_signal(self.widget.Inputs.data, data)\n        self._check_values(data.domain.attributes, data)\n        self._test_sort([[\"status\", \"age\", \"sex\"],\n                         [\"age\", \"sex\", \"status\"],\n                         [\"sex\", \"status\", \"age\"],\n                         [\"sex\", \"status\", \"age\"],\n                         [\"sex\", \"status\", \"age\"]])\n\n    def test_nomogram_with_instance_lr(self):\n        \"\"\"Check initialized marker values and feature sorting for logistic\n        regression classifier and data on input\"\"\"\n        cls = LogisticRegressionLearner()(self.titanic)\n        data = self.titanic[10:11]\n        self.send_signal(self.widget.Inputs.classifier, cls)\n        self.send_signal(self.widget.Inputs.data, data)\n        self._check_values(data.domain.attributes, data)\n        self._test_sort([[\"status\", \"age\", \"sex\"],\n                         [\"age\", \"sex\", \"status\"],\n                         [\"sex\", \"status\", \"age\"],\n                         [\"sex\", \"status\", \"age\"],\n                         [\"sex\", \"status\", \"age\"]])\n\n    def test_constant_feature_disc(self):\n        \"\"\"Check nomogram for data with constant discrete feature\"\"\"\n        domain = Domain([DiscreteVariable(\"d1\", (\"a\", \"c\")),\n                         DiscreteVariable(\"d2\", (\"b\",))],\n                        DiscreteVariable(\"cls\", (\"e\", \"d\")))\n        X = np.array([[0, 0], [1, 0], [0, 0], [1, 0]])\n        data = Table(domain, X, np.array([0, 1, 1, 0]))\n        cls = NaiveBayesLearner()(data)\n        self._test_helper(cls, [50, 50])\n        cls = LogisticRegressionLearner()(data)\n        self._test_helper(cls, [50, 50])\n\n    def test_constant_feature_cont(self):\n        \"\"\"Check nomogram for data with constant continuous feature\"\"\"\n        domain = Domain([DiscreteVariable(\"d\", (\"a\", \"b\")),\n                         ContinuousVariable(\"c\")],\n                        DiscreteVariable(\"cls\", (\"c\", \"d\")))\n        X = np.array([[0, 0], [1, 0], [0, 0], [1, 0]])\n        data = Table(domain, X, np.array([0, 1, 1, 0]))\n        cls = NaiveBayesLearner()(data)\n        self._test_helper(cls, [50, 50])\n        cls = LogisticRegressionLearner()(data)\n        self._test_helper(cls, [50, 50])\n\n    def _test_helper(self, cls, values):\n        self.send_signal(self.widget.Inputs.classifier, cls)\n\n        # check for all class values\n        for i in range(self.widget.class_combo.count()):\n            self.widget.class_combo.activated.emit(i)\n            self.widget.class_combo.setCurrentIndex(i)\n\n            # check probabilities marker value\n            self._test_helper_check_probability(values[i])\n\n            # point scale\n            self.widget.controls.scale.buttons[0].click()\n            self._test_helper_check_probability(values[i])\n\n            # best ranked\n            self.widget.n_attributes = 5\n            self.widget.controls.display_index.buttons[1].click()\n            visible_items = [item for item in self.widget.scene.items() if\n                             isinstance(item, (DiscreteFeatureItem,\n                                               ContinuousFeatureItem)) and\n                             item.isVisible()]\n            self.assertGreaterEqual(5, len(visible_items))\n\n            # 2D curve\n            self.widget.n_attributes = 15\n            self.widget.cont_feature_dim_combo.activated.emit(1)\n            self.widget.cont_feature_dim_combo.setCurrentIndex(1)\n            self._test_helper_check_probability(values[i])\n\n            # initial state\n            self.widget.controls.scale.buttons[1].click()\n            self.widget.controls.display_index.buttons[0].click()\n            self.widget.cont_feature_dim_combo.activated.emit(0)\n            self.widget.cont_feature_dim_combo.setCurrentIndex(0)\n\n    def _test_helper_check_probability(self, value):\n        prob_marker = [item for item in self.widget.scene.items() if\n                       isinstance(item, ProbabilitiesDotItem)][0]\n        self.assertIn(\"Probability: {}\".format(value),\n                      prob_marker.get_tooltip_text())\n\n    def _check_values(self, attributes, data):\n        for attr, item in zip(attributes, self.widget.feature_items.values()):\n            assert attr.name == item.childItems()[0].toPlainText()\n            value = data[0][attr.name].value\n            value = \"{}: 100%\".format(value) if attr.is_discrete \\\n                else \"Value: {}\".format(value)\n            self.assertIn(value, item.dot.get_tooltip_text())\n\n    def _test_sort(self, names):\n        for i in range(self.widget.sort_combo.count()):\n            self.widget.sort_combo.activated.emit(i)\n            self.widget.sort_combo.setCurrentIndex(i)\n            ordered = [self.widget.nomogram_main.layout().itemAt(i).childItems()[0].toPlainText()\n                       for i in range(self.widget.nomogram_main.layout().count())]\n            self.assertListEqual(names[i], ordered)\n\n    def test_tooltip(self):\n        # had problems on PyQt4\n        m = MovableToolTip()\n        m.show(QPoint(0, 0), \"Some text.\")\n\n    def test_output(self):\n        cls = LogisticRegressionLearner()(self.titanic)\n        data = self.titanic[10:11]\n        status, age, sex = data.domain.attributes\n        self.send_signal(self.widget.Inputs.classifier, cls)\n        self.widget.sort_combo.setCurrentIndex(1)\n        self.widget.sort_combo.activated.emit(1)\n\n        # Output more attributer than there are -> output all\n        self.widget.n_attributes = 5\n        self.widget.n_spin.valueChanged.emit(5)\n        attrs = self.get_output(self.widget.Outputs.features)\n        self.assertEqual(attrs, [age, sex, status])\n\n        # Output the first two\n        self.widget.n_attributes = 2\n        self.widget.n_spin.valueChanged.emit(2)\n        attrs = self.get_output(self.widget.Outputs.features)\n        self.assertEqual(attrs, [age, sex])\n\n        # Set to output all\n        self.widget.display_index = 0\n        self.widget.controls.display_index.group.buttonClicked[int].emit(0)\n        attrs = self.get_output(self.widget.Outputs.features)\n        self.assertEqual(attrs, [age, sex, status])\n\n        # Remove classifier -> output None\n        self.send_signal(self.widget.Inputs.classifier, None)\n        attrs = self.get_output(self.widget.Outputs.features)\n        self.assertIsNone(attrs)\n\n    def test_reset_settings(self):\n        self.widget.n_attributes = 5\n        self.widget.n_spin.valueChanged.emit(5)\n        self.widget.reset_settings()\n        self.assertEqual(10, self.widget.n_attributes)\n\n    @patch(\"Orange.widgets.visualize.ownomogram.QGraphicsTextItem\")\n    def test_adjust_scale(self, mocked_item: Mock):\n        def mocked_width():\n            nonlocal ind\n            ind += 1\n            most_right = {4: 30, 9: 59, 14: 59}\n            return [2, 30, 59, 2, most_right.get(ind)][ind % 5]\n\n        ind = -1\n        mocked_item().boundingRect().width.side_effect = mocked_width\n        attrs = [DiscreteVariable(\"var1\", values=(\"foo1\", \"foo2\")),\n                 DiscreteVariable(\"var2\", values=(\"foo3\", \"foo4\"))]\n        points = [np.array([0, 1.8]), np.array([1.5, 2.0])]\n        diff = np.max(points) - np.min(points)\n        # foo3 eventually overcomes foo2, while the scale is getting smaller\n        #\n        #  0                        1              1.5         1.8     2\n        # foo1                                          _______foo2_______\n        #                               __________foo3__________      foo4\n        self.widget._adjust_scale(attrs, points, 100, diff, [0, 1], [], 0)\n        # most left text at 1. iteration\n        self.assertEqual(mocked_item.call_args_list[5][0][0], \"foo2\")\n        # most left text at 2. iteration\n        self.assertEqual(mocked_item.call_args_list[10][0][0], \"foo3\")\n        # most left text at 3. iteration is the same -> stop\n        self.assertEqual(mocked_item.call_args_list[15][0][0], \"foo3\")\n\n    def test_summary(self):\n        \"\"\"Check if status bar is updated when data is received\"\"\"\n        data = self.data\n        info = self.widget.info\n        no_input = \"No data on input\"\n\n        self.send_signal(self.widget.Inputs.data, data)\n        summary, details = f\"{len(data)}\", format_summary_details(data)\n        self.assertEqual(info._StateInfo__input_summary.brief, summary)\n        self.assertEqual(info._StateInfo__input_summary.details, details)\n        self.send_signal(self.widget.Inputs.data, None)\n        self.assertIsInstance(info._StateInfo__input_summary, StateInfo.Empty)\n        self.assertEqual(info._StateInfo__input_summary.details, no_input)\n\n    def test_dots_stop_flashing(self):\n        self.widget.set_data(self.data)\n        self.widget.set_classifier(self.nb_cls)\n        animator = self.widget.dot_animator\n        dot = animator._GraphicsColorAnimator__items[0]\n        dot._mousePressFunc()\n        anim = animator._GraphicsColorAnimator__animation\n        self.assertNotEqual(anim.state(), QPropertyAnimation.Running)\n\n    def test_reconstruct_domain(self):\n        data = Table(\"heart_disease\")\n        cls = LogisticRegressionLearner()(data)\n        domain = OWNomogram.reconstruct_domain(cls.original_domain, cls.domain)\n        transformed_data = cls.original_data.transform(domain)\n        self.assertEqual(transformed_data.X.shape, data.X.shape)\n        self.assertFalse(np.isnan(transformed_data.X[0]).any())\n\n        scaled_data = Scale()(data)\n        cls = LogisticRegressionLearner()(scaled_data)\n        domain = OWNomogram.reconstruct_domain(cls.original_domain, cls.domain)\n        transformed_data = cls.original_data.transform(domain)\n        self.assertEqual(transformed_data.X.shape, scaled_data.X.shape)\n        self.assertFalse(np.isnan(transformed_data.X[0]).any())\n\n        disc_data = Continuize()(data)\n        cls = LogisticRegressionLearner()(disc_data)\n        domain = OWNomogram.reconstruct_domain(cls.original_domain, cls.domain)\n        transformed_data = cls.original_data.transform(domain)\n        self.assertEqual(transformed_data.X.shape, disc_data.X.shape)\n        self.assertFalse(np.isnan(transformed_data.X[0]).any())",
                                "ast_type_str": "ClassDef",
                                "start_lineno": 26,
                                "end_lineno": 330,
                                "parent": null
                            }
                        }
                    ],
                    "deleted_entities": []
                }
            ]
        }
    ],
    "old_commit_hash": "b173cd294c0a273862501d3fb9129c16fc8eb3df^",
    "new_commit_hash": "b173cd294c0a273862501d3fb9129c16fc8eb3df",
    "commit_message": "Merge pull request #5382 from VesnaT/nomogram_domain_fix\n\n[FIX] Nomogram: Retain original compute_value",
    "commit_date": "2021-04-09T10:12:35+02:00",
    "metadata": {}
}
{
    "repo_name": "orange3",
    "new_commit_hash": "b173cd294c0a273862501d3fb9129c16fc8eb3df",
    "test_file_codes": [
        "# Test methods with long descriptive names can omit docstrings\n# pylint: disable=missing-docstring,protected-access\nimport unittest\nfrom unittest.mock import Mock, patch\n\nimport numpy as np\n\nfrom AnyQt.QtCore import QPoint, QPropertyAnimation\n\nfrom orangewidget.widget import StateInfo\n\nfrom Orange.data import Table, Domain, ContinuousVariable, DiscreteVariable\nfrom Orange.classification import (\n    NaiveBayesLearner, LogisticRegressionLearner, MajorityLearner\n)\nfrom Orange.preprocess import Scale, Continuize\nfrom Orange.tests import test_filename\nfrom Orange.widgets.tests.base import WidgetTest\nfrom Orange.widgets.utils.state_summary import format_summary_details\nfrom Orange.widgets.visualize.ownomogram import (\n    OWNomogram, DiscreteFeatureItem, ContinuousFeatureItem, ProbabilitiesDotItem,\n    MovableToolTip\n)\n\n\nclass TestOWNomogram(WidgetTest):\n    @classmethod\n    def setUpClass(cls):\n        super().setUpClass()\n        cls.data = Table(\"heart_disease\")\n        cls.nb_cls = NaiveBayesLearner()(cls.data)\n        cls.lr_cls = LogisticRegressionLearner()(cls.data)\n        cls.titanic = Table(\"titanic\")\n        cls.lenses = Table(test_filename(\"datasets/lenses.tab\"))\n\n    def setUp(self):\n        self.widget = self.create_widget(OWNomogram)  # type: OWNomogram\n\n    def test_input_nb_cls(self):\n        \"\"\"Check naive bayes classifier on input\"\"\"\n        self.send_signal(self.widget.Inputs.classifier, self.nb_cls)\n        self.assertEqual(len([item for item in self.widget.scene.items() if\n                              isinstance(item, DiscreteFeatureItem)]),\n                         min(self.widget.n_attributes,\n                             len(self.data.domain.attributes)))\n\n    def test_input_lr_cls(self):\n        \"\"\"Check logistic regression classifier on input\"\"\"\n        self.widget.display_index = 0  # display ALL features\n        self.send_signal(self.widget.Inputs.classifier, self.lr_cls)\n        self.assertEqual(\n            len([item for item in self.widget.scene.items() if\n                 isinstance(item, DiscreteFeatureItem)]),\n            len([a for a in self.data.domain.attributes if a.is_discrete]))\n        self.assertEqual(\n            len([item for item in self.widget.scene.items() if\n                 isinstance(item, ContinuousFeatureItem)]),\n            len([a for a in self.data.domain.attributes if a.is_continuous]))\n\n    def test_input_invalid_cls(self):\n        \"\"\"Check any classifier on input\"\"\"\n        majority_cls = MajorityLearner()(self.data)\n        self.send_signal(self.widget.Inputs.classifier, majority_cls)\n        self.assertTrue(self.widget.Error.invalid_classifier.is_shown())\n        self.send_signal(self.widget.Inputs.classifier, None)\n        self.assertFalse(self.widget.Error.invalid_classifier.is_shown())\n\n    def test_input_instance(self):\n        \"\"\" Check data instance on input\"\"\"\n        self.send_signal(self.widget.Inputs.data, self.data)\n        self.assertIsNotNone(self.widget.instances)\n        self.send_signal(self.widget.Inputs.data, None)\n        self.assertIsNone(self.widget.instances)\n\n    def test_target_values(self):\n        \"\"\"Check Target class combo values\"\"\"\n        self.send_signal(self.widget.Inputs.classifier, self.nb_cls)\n        for i, text in enumerate(self.data.domain.class_var.values):\n            self.assertEqual(text, self.widget.class_combo.itemText(i))\n        self.send_signal(self.widget.Inputs.classifier, None)\n        self.assertEqual(0, self.widget.class_combo.count())\n\n    def test_nomogram_nb(self):\n        \"\"\"Check probabilities for naive bayes classifier for various values\n        of classes and radio buttons\"\"\"\n        self._test_helper(self.nb_cls, [54, 46])\n\n    def test_nomogram_lr(self):\n        \"\"\"Check probabilities for logistic regression classifier for various\n        values of classes and radio buttons\"\"\"\n        self.widget.display_index = 0  # show ALL features\n        self._test_helper(self.lr_cls, [58, 42])\n\n    def test_nomogram_nb_multiclass(self):\n        \"\"\"Check probabilities for naive bayes classifier for various values\n        of classes and radio buttons for multiclass data\"\"\"\n        cls = NaiveBayesLearner()(self.lenses)\n        self._test_helper(cls, [19, 53, 13])\n\n    def test_nomogram_lr_multiclass(self):\n        \"\"\"Check probabilities for logistic regression classifier for various\n        values of classes and radio buttons for multiclass data\"\"\"\n        cls = LogisticRegressionLearner(\n            multi_class=\"ovr\", solver=\"liblinear\"\n        )(self.lenses)\n        self._test_helper(cls, [9, 45, 52])\n\n    def test_nomogram_with_instance_nb(self):\n        \"\"\"Check initialized marker values and feature sorting for naive bayes\n        classifier and data on input\"\"\"\n        cls = NaiveBayesLearner()(self.titanic)\n        data = self.titanic[10:11]\n        self.send_signal(self.widget.Inputs.classifier, cls)\n        self.send_signal(self.widget.Inputs.data, data)\n        self._check_values(data.domain.attributes, data)\n        self._test_sort([[\"status\", \"age\", \"sex\"],\n                         [\"age\", \"sex\", \"status\"],\n                         [\"sex\", \"status\", \"age\"],\n                         [\"sex\", \"status\", \"age\"],\n                         [\"sex\", \"status\", \"age\"]])\n\n    def test_nomogram_with_instance_lr(self):\n        \"\"\"Check initialized marker values and feature sorting for logistic\n        regression classifier and data on input\"\"\"\n        cls = LogisticRegressionLearner()(self.titanic)\n        data = self.titanic[10:11]\n        self.send_signal(self.widget.Inputs.classifier, cls)\n        self.send_signal(self.widget.Inputs.data, data)\n        self._check_values(data.domain.attributes, data)\n        self._test_sort([[\"status\", \"age\", \"sex\"],\n                         [\"age\", \"sex\", \"status\"],\n                         [\"sex\", \"status\", \"age\"],\n                         [\"sex\", \"status\", \"age\"],\n                         [\"sex\", \"status\", \"age\"]])\n\n    def test_constant_feature_disc(self):\n        \"\"\"Check nomogram for data with constant discrete feature\"\"\"\n        domain = Domain([DiscreteVariable(\"d1\", (\"a\", \"c\")),\n                         DiscreteVariable(\"d2\", (\"b\",))],\n                        DiscreteVariable(\"cls\", (\"e\", \"d\")))\n        X = np.array([[0, 0], [1, 0], [0, 0], [1, 0]])\n        data = Table(domain, X, np.array([0, 1, 1, 0]))\n        cls = NaiveBayesLearner()(data)\n        self._test_helper(cls, [50, 50])\n        cls = LogisticRegressionLearner()(data)\n        self._test_helper(cls, [50, 50])\n\n    def test_constant_feature_cont(self):\n        \"\"\"Check nomogram for data with constant continuous feature\"\"\"\n        domain = Domain([DiscreteVariable(\"d\", (\"a\", \"b\")),\n                         ContinuousVariable(\"c\")],\n                        DiscreteVariable(\"cls\", (\"c\", \"d\")))\n        X = np.array([[0, 0], [1, 0], [0, 0], [1, 0]])\n        data = Table(domain, X, np.array([0, 1, 1, 0]))\n        cls = NaiveBayesLearner()(data)\n        self._test_helper(cls, [50, 50])\n        cls = LogisticRegressionLearner()(data)\n        self._test_helper(cls, [50, 50])\n\n    def _test_helper(self, cls, values):\n        self.send_signal(self.widget.Inputs.classifier, cls)\n\n        # check for all class values\n        for i in range(self.widget.class_combo.count()):\n            self.widget.class_combo.activated.emit(i)\n            self.widget.class_combo.setCurrentIndex(i)\n\n            # check probabilities marker value\n            self._test_helper_check_probability(values[i])\n\n            # point scale\n            self.widget.controls.scale.buttons[0].click()\n            self._test_helper_check_probability(values[i])\n\n            # best ranked\n            self.widget.n_attributes = 5\n            self.widget.controls.display_index.buttons[1].click()\n            visible_items = [item for item in self.widget.scene.items() if\n                             isinstance(item, (DiscreteFeatureItem,\n                                               ContinuousFeatureItem)) and\n                             item.isVisible()]\n            self.assertGreaterEqual(5, len(visible_items))\n\n            # 2D curve\n            self.widget.n_attributes = 15\n            self.widget.cont_feature_dim_combo.activated.emit(1)\n            self.widget.cont_feature_dim_combo.setCurrentIndex(1)\n            self._test_helper_check_probability(values[i])\n\n            # initial state\n            self.widget.controls.scale.buttons[1].click()\n            self.widget.controls.display_index.buttons[0].click()\n            self.widget.cont_feature_dim_combo.activated.emit(0)\n            self.widget.cont_feature_dim_combo.setCurrentIndex(0)\n\n    def _test_helper_check_probability(self, value):\n        prob_marker = [item for item in self.widget.scene.items() if\n                       isinstance(item, ProbabilitiesDotItem)][0]\n        self.assertIn(\"Probability: {}\".format(value),\n                      prob_marker.get_tooltip_text())\n\n    def _check_values(self, attributes, data):\n        for attr, item in zip(attributes, self.widget.feature_items.values()):\n            assert attr.name == item.childItems()[0].toPlainText()\n            value = data[0][attr.name].value\n            value = \"{}: 100%\".format(value) if attr.is_discrete \\\n                else \"Value: {}\".format(value)\n            self.assertIn(value, item.dot.get_tooltip_text())\n\n    def _test_sort(self, names):\n        for i in range(self.widget.sort_combo.count()):\n            self.widget.sort_combo.activated.emit(i)\n            self.widget.sort_combo.setCurrentIndex(i)\n            ordered = [self.widget.nomogram_main.layout().itemAt(i).childItems()[0].toPlainText()\n                       for i in range(self.widget.nomogram_main.layout().count())]\n            self.assertListEqual(names[i], ordered)\n\n    def test_tooltip(self):\n        # had problems on PyQt4\n        m = MovableToolTip()\n        m.show(QPoint(0, 0), \"Some text.\")\n\n    def test_output(self):\n        cls = LogisticRegressionLearner()(self.titanic)\n        data = self.titanic[10:11]\n        status, age, sex = data.domain.attributes\n        self.send_signal(self.widget.Inputs.classifier, cls)\n        self.widget.sort_combo.setCurrentIndex(1)\n        self.widget.sort_combo.activated.emit(1)\n\n        # Output more attributer than there are -> output all\n        self.widget.n_attributes = 5\n        self.widget.n_spin.valueChanged.emit(5)\n        attrs = self.get_output(self.widget.Outputs.features)\n        self.assertEqual(attrs, [age, sex, status])\n\n        # Output the first two\n        self.widget.n_attributes = 2\n        self.widget.n_spin.valueChanged.emit(2)\n        attrs = self.get_output(self.widget.Outputs.features)\n        self.assertEqual(attrs, [age, sex])\n\n        # Set to output all\n        self.widget.display_index = 0\n        self.widget.controls.display_index.group.buttonClicked[int].emit(0)\n        attrs = self.get_output(self.widget.Outputs.features)\n        self.assertEqual(attrs, [age, sex, status])\n\n        # Remove classifier -> output None\n        self.send_signal(self.widget.Inputs.classifier, None)\n        attrs = self.get_output(self.widget.Outputs.features)\n        self.assertIsNone(attrs)\n\n    def test_reset_settings(self):\n        self.widget.n_attributes = 5\n        self.widget.n_spin.valueChanged.emit(5)\n        self.widget.reset_settings()\n        self.assertEqual(10, self.widget.n_attributes)\n\n    @patch(\"Orange.widgets.visualize.ownomogram.QGraphicsTextItem\")\n    def test_adjust_scale(self, mocked_item: Mock):\n        def mocked_width():\n            nonlocal ind\n            ind += 1\n            most_right = {4: 30, 9: 59, 14: 59}\n            return [2, 30, 59, 2, most_right.get(ind)][ind % 5]\n\n        ind = -1\n        mocked_item().boundingRect().width.side_effect = mocked_width\n        attrs = [DiscreteVariable(\"var1\", values=(\"foo1\", \"foo2\")),\n                 DiscreteVariable(\"var2\", values=(\"foo3\", \"foo4\"))]\n        points = [np.array([0, 1.8]), np.array([1.5, 2.0])]\n        diff = np.max(points) - np.min(points)\n        # foo3 eventually overcomes foo2, while the scale is getting smaller\n        #\n        #  0                        1              1.5         1.8     2\n        # foo1                                          _______foo2_______\n        #                               __________foo3__________      foo4\n        self.widget._adjust_scale(attrs, points, 100, diff, [0, 1], [], 0)\n        # most left text at 1. iteration\n        self.assertEqual(mocked_item.call_args_list[5][0][0], \"foo2\")\n        # most left text at 2. iteration\n        self.assertEqual(mocked_item.call_args_list[10][0][0], \"foo3\")\n        # most left text at 3. iteration is the same -> stop\n        self.assertEqual(mocked_item.call_args_list[15][0][0], \"foo3\")\n\n    def test_summary(self):\n        \"\"\"Check if status bar is updated when data is received\"\"\"\n        data = self.data\n        info = self.widget.info\n        no_input = \"No data on input\"\n\n        self.send_signal(self.widget.Inputs.data, data)\n        summary, details = f\"{len(data)}\", format_summary_details(data)\n        self.assertEqual(info._StateInfo__input_summary.brief, summary)\n        self.assertEqual(info._StateInfo__input_summary.details, details)\n        self.send_signal(self.widget.Inputs.data, None)\n        self.assertIsInstance(info._StateInfo__input_summary, StateInfo.Empty)\n        self.assertEqual(info._StateInfo__input_summary.details, no_input)\n\n    def test_dots_stop_flashing(self):\n        self.widget.set_data(self.data)\n        self.widget.set_classifier(self.nb_cls)\n        animator = self.widget.dot_animator\n        dot = animator._GraphicsColorAnimator__items[0]\n        dot._mousePressFunc()\n        anim = animator._GraphicsColorAnimator__animation\n        self.assertNotEqual(anim.state(), QPropertyAnimation.Running)\n\n    def test_reconstruct_domain(self):\n        data = Table(\"heart_disease\")\n        cls = LogisticRegressionLearner()(data)\n        domain = OWNomogram.reconstruct_domain(cls.original_domain, cls.domain)\n        transformed_data = cls.original_data.transform(domain)\n        self.assertEqual(transformed_data.X.shape, data.X.shape)\n        self.assertFalse(np.isnan(transformed_data.X[0]).any())\n\n        scaled_data = Scale()(data)\n        cls = LogisticRegressionLearner()(scaled_data)\n        domain = OWNomogram.reconstruct_domain(cls.original_domain, cls.domain)\n        transformed_data = cls.original_data.transform(domain)\n        self.assertEqual(transformed_data.X.shape, scaled_data.X.shape)\n        self.assertFalse(np.isnan(transformed_data.X[0]).any())\n\n        disc_data = Continuize()(data)\n        cls = LogisticRegressionLearner()(disc_data)\n        domain = OWNomogram.reconstruct_domain(cls.original_domain, cls.domain)\n        transformed_data = cls.original_data.transform(domain)\n        self.assertEqual(transformed_data.X.shape, disc_data.X.shape)\n        self.assertFalse(np.isnan(transformed_data.X[0]).any())\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_b173cd294c0a273862501d3fb9129c16fc8eb3df/.venv/lib/python3.7/site-packages/numpy/core/include -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_b173cd294c0a273862501d3fb9129c16fc8eb3df/.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_b173cd294c0a273862501d3fb9129c16fc8eb3df/.venv/lib/python3.7/site-packages/numpy/core/include -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_b173cd294c0a273862501d3fb9129c16fc8eb3df/.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_b173cd294c0a273862501d3fb9129c16fc8eb3df/.venv/lib/python3.7/site-packages/numpy/core/include -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_b173cd294c0a273862501d3fb9129c16fc8eb3df/.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_b173cd294c0a273862501d3fb9129c16fc8eb3df/.venv/lib/python3.7/site-packages/numpy/core/include -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_b173cd294c0a273862501d3fb9129c16fc8eb3df/.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_b173cd294c0a273862501d3fb9129c16fc8eb3df/.venv/lib/python3.7/site-packages/numpy/core/include -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_b173cd294c0a273862501d3fb9129c16fc8eb3df/.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_b173cd294c0a273862501d3fb9129c16fc8eb3df/.venv/lib/python3.7/site-packages/numpy/core/include -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_b173cd294c0a273862501d3fb9129c16fc8eb3df/.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_b173cd294c0a273862501d3fb9129c16fc8eb3df/.venv/lib/python3.7/site-packages/numpy/core/include -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_b173cd294c0a273862501d3fb9129c16fc8eb3df/.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_b173cd294c0a273862501d3fb9129c16fc8eb3df/.venv/lib/python3.7/site-packages/numpy/core/include -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_b173cd294c0a273862501d3fb9129c16fc8eb3df/.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_b173cd294c0a273862501d3fb9129c16fc8eb3df/.venv/lib/python3.7/site-packages/numpy/core/include -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_b173cd294c0a273862501d3fb9129c16fc8eb3df/.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_b173cd294c0a273862501d3fb9129c16fc8eb3df/.venv/lib/python3.7/site-packages/numpy/core/include -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_b173cd294c0a273862501d3fb9129c16fc8eb3df/.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_b173cd294c0a273862501d3fb9129c16fc8eb3df/.venv/lib/python3.7/site-packages/numpy/core/include -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_b173cd294c0a273862501d3fb9129c16fc8eb3df/.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_b173cd294c0a273862501d3fb9129c16fc8eb3df/.venv/lib/python3.7/site-packages/Orange3.egg-link (link to .)\nAdding Orange3 3.29.0.dev0+b173cd2 to easy-install.pth file\nInstalling orange-canvas script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_b173cd294c0a273862501d3fb9129c16fc8eb3df/.venv/bin\n\nInstalled /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_b173cd294c0a273862501d3fb9129c16fc8eb3df\nProcessing dependencies for Orange3==3.29.0.dev0+b173cd2\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_b173cd294c0a273862501d3fb9129c16fc8eb3df/.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_b173cd294c0a273862501d3fb9129c16fc8eb3df/.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_b173cd294c0a273862501d3fb9129c16fc8eb3df/.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_b173cd294c0a273862501d3fb9129c16fc8eb3df/.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_b173cd294c0a273862501d3fb9129c16fc8eb3df/.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_b173cd294c0a273862501d3fb9129c16fc8eb3df/.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_b173cd294c0a273862501d3fb9129c16fc8eb3df/.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_b173cd294c0a273862501d3fb9129c16fc8eb3df/.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_b173cd294c0a273862501d3fb9129c16fc8eb3df/.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_b173cd294c0a273862501d3fb9129c16fc8eb3df/.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_b173cd294c0a273862501d3fb9129c16fc8eb3df/.venv/bin\nInstalling pip3 script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_b173cd294c0a273862501d3fb9129c16fc8eb3df/.venv/bin\nInstalling pip3.10 script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_b173cd294c0a273862501d3fb9129c16fc8eb3df/.venv/bin\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_b173cd294c0a273862501d3fb9129c16fc8eb3df/.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_b173cd294c0a273862501d3fb9129c16fc8eb3df/.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_b173cd294c0a273862501d3fb9129c16fc8eb3df/.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_b173cd294c0a273862501d3fb9129c16fc8eb3df/.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_b173cd294c0a273862501d3fb9129c16fc8eb3df/.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_b173cd294c0a273862501d3fb9129c16fc8eb3df/.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_b173cd294c0a273862501d3fb9129c16fc8eb3df/.venv/bin\nInstalling f2py3 script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_b173cd294c0a273862501d3fb9129c16fc8eb3df/.venv/bin\nInstalling f2py3.7 script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_b173cd294c0a273862501d3fb9129c16fc8eb3df/.venv/bin\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_b173cd294c0a273862501d3fb9129c16fc8eb3df/.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_b173cd294c0a273862501d3fb9129c16fc8eb3df/.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_b173cd294c0a273862501d3fb9129c16fc8eb3df/.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_b173cd294c0a273862501d3fb9129c16fc8eb3df/.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_b173cd294c0a273862501d3fb9129c16fc8eb3df/.venv/bin\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_b173cd294c0a273862501d3fb9129c16fc8eb3df/.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_b173cd294c0a273862501d3fb9129c16fc8eb3df/.venv/lib/python3.7/site-packages\nSearching for httpx==0.16.1\nBest match: httpx 0.16.1\nAdding httpx 0.16.1 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_b173cd294c0a273862501d3fb9129c16fc8eb3df/.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_b173cd294c0a273862501d3fb9129c16fc8eb3df/.venv/bin\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_b173cd294c0a273862501d3fb9129c16fc8eb3df/.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_b173cd294c0a273862501d3fb9129c16fc8eb3df/.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_b173cd294c0a273862501d3fb9129c16fc8eb3df/.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_b173cd294c0a273862501d3fb9129c16fc8eb3df/.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_b173cd294c0a273862501d3fb9129c16fc8eb3df/.venv/bin\nInstalling pyrcc5 script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_b173cd294c0a273862501d3fb9129c16fc8eb3df/.venv/bin\nInstalling pyuic5 script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_b173cd294c0a273862501d3fb9129c16fc8eb3df/.venv/bin\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_b173cd294c0a273862501d3fb9129c16fc8eb3df/.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_b173cd294c0a273862501d3fb9129c16fc8eb3df/.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_b173cd294c0a273862501d3fb9129c16fc8eb3df/.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_b173cd294c0a273862501d3fb9129c16fc8eb3df/.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_b173cd294c0a273862501d3fb9129c16fc8eb3df/.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_b173cd294c0a273862501d3fb9129c16fc8eb3df/.venv/bin\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_b173cd294c0a273862501d3fb9129c16fc8eb3df/.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_b173cd294c0a273862501d3fb9129c16fc8eb3df/.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_b173cd294c0a273862501d3fb9129c16fc8eb3df/.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_b173cd294c0a273862501d3fb9129c16fc8eb3df/.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_b173cd294c0a273862501d3fb9129c16fc8eb3df/.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_b173cd294c0a273862501d3fb9129c16fc8eb3df/.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_b173cd294c0a273862501d3fb9129c16fc8eb3df/.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_b173cd294c0a273862501d3fb9129c16fc8eb3df/.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_b173cd294c0a273862501d3fb9129c16fc8eb3df/.venv/bin\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_b173cd294c0a273862501d3fb9129c16fc8eb3df/.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_b173cd294c0a273862501d3fb9129c16fc8eb3df/.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_b173cd294c0a273862501d3fb9129c16fc8eb3df/.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_b173cd294c0a273862501d3fb9129c16fc8eb3df/.venv/bin\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_b173cd294c0a273862501d3fb9129c16fc8eb3df/.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_b173cd294c0a273862501d3fb9129c16fc8eb3df/.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_b173cd294c0a273862501d3fb9129c16fc8eb3df/.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_b173cd294c0a273862501d3fb9129c16fc8eb3df/.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_b173cd294c0a273862501d3fb9129c16fc8eb3df/.venv/bin\nInstalling pyftmerge script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_b173cd294c0a273862501d3fb9129c16fc8eb3df/.venv/bin\nInstalling pyftsubset script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_b173cd294c0a273862501d3fb9129c16fc8eb3df/.venv/bin\nInstalling ttx script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_b173cd294c0a273862501d3fb9129c16fc8eb3df/.venv/bin\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_b173cd294c0a273862501d3fb9129c16fc8eb3df/.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_b173cd294c0a273862501d3fb9129c16fc8eb3df/.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_b173cd294c0a273862501d3fb9129c16fc8eb3df/.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_b173cd294c0a273862501d3fb9129c16fc8eb3df/.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_b173cd294c0a273862501d3fb9129c16fc8eb3df/.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_b173cd294c0a273862501d3fb9129c16fc8eb3df/.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_b173cd294c0a273862501d3fb9129c16fc8eb3df/.venv/lib/python3.7/site-packages\nSearching for httpcore==0.12.3\nBest match: httpcore 0.12.3\nAdding httpcore 0.12.3 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_b173cd294c0a273862501d3fb9129c16fc8eb3df/.venv/lib/python3.7/site-packages\nSearching for rfc3986==1.5.0\nBest match: rfc3986 1.5.0\nAdding rfc3986 1.5.0 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_b173cd294c0a273862501d3fb9129c16fc8eb3df/.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_b173cd294c0a273862501d3fb9129c16fc8eb3df/.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_b173cd294c0a273862501d3fb9129c16fc8eb3df/.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_b173cd294c0a273862501d3fb9129c16fc8eb3df/.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_b173cd294c0a273862501d3fb9129c16fc8eb3df/.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_b173cd294c0a273862501d3fb9129c16fc8eb3df/.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_b173cd294c0a273862501d3fb9129c16fc8eb3df/.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_b173cd294c0a273862501d3fb9129c16fc8eb3df/.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_b173cd294c0a273862501d3fb9129c16fc8eb3df/.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_b173cd294c0a273862501d3fb9129c16fc8eb3df/.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_b173cd294c0a273862501d3fb9129c16fc8eb3df/.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_b173cd294c0a273862501d3fb9129c16fc8eb3df/.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_b173cd294c0a273862501d3fb9129c16fc8eb3df/.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_b173cd294c0a273862501d3fb9129c16fc8eb3df/.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_b173cd294c0a273862501d3fb9129c16fc8eb3df/.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_b173cd294c0a273862501d3fb9129c16fc8eb3df/.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_b173cd294c0a273862501d3fb9129c16fc8eb3df/.venv/lib/python3.7/site-packages\nFinished processing dependencies for Orange3==3.29.0.dev0+b173cd2\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 47ms\nPrepared 18 packages in 29ms\nInstalled 18 packages in 90ms\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 53 packages in 298ms\nInstalled 47 packages in 748ms\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.12.3\n + httpx==0.16.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 + rfc3986==1.5.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 39 packages in 99ms\nInstalled 13 packages in 148ms\n + anyqt==0.2.0\n + attrs==24.2.0\n + cattrs==23.1.2\n + commonmark==0.9.1\n + dictdiffer==0.9.0\n + docutils==0.20.1\n + orange-canvas-core==0.1.35\n + orange-widget-base==4.22.0\n + platformdirs==4.0.0\n + pyqtgraph==0.12.4\n + qasync==0.25.0\n + requests-cache==1.1.1\n + url-normalize==1.4.3\nResolved 2 packages in 3ms\nInstalled 2 packages in 42ms\n + psycopg2==2.9.9\n + pymssql==2.3.2\nResolved 19 packages in 46ms\nInstalled 5 packages in 1.20s\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_b173cd294c0a273862501d3fb9129c16fc8eb3df/.venv/lib/python3.7/site-packages/numpy/core/include/numpy/ndarraytypes.h:1830,\n                 from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_b173cd294c0a273862501d3fb9129c16fc8eb3df/.venv/lib/python3.7/site-packages/numpy/core/include/numpy/ndarrayobject.h:12,\n                 from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_b173cd294c0a273862501d3fb9129c16fc8eb3df/.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_b173cd294c0a273862501d3fb9129c16fc8eb3df/.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:3413:27: warning: comparison of integer expressions of different signedness: ‘unsigned int’ and ‘int’ [-Wsign-compare]\n 3413 |   __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:4869:27: warning: comparison of integer expressions of different signedness: ‘unsigned int’ and ‘int’ [-Wsign-compare]\n 4869 |   __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:5615:43: warning: comparison of integer expressions of different signedness: ‘unsigned int’ and ‘int’ [-Wsign-compare]\n 5615 |           for (__pyx_t_19 = 0; __pyx_t_19 < __pyx_t_18; __pyx_t_19+=1) {\n      |                                           ^\nOrange/classification/_tree_scorers.c:5736:39: warning: comparison of integer expressions of different signedness: ‘unsigned int’ and ‘int’ [-Wsign-compare]\n 5736 |           __pyx_t_20 = ((__pyx_v_left >= __pyx_v_min_leaf) != 0);\n      |                                       ^~\nOrange/classification/_tree_scorers.c:5742:53: warning: comparison of integer expressions of different signedness: ‘unsigned int’ and ‘int’ [-Wsign-compare]\n 5742 |           __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:7202:41: warning: comparison of integer expressions of different signedness: ‘unsigned int’ and ‘int’ [-Wsign-compare]\n 7202 |         for (__pyx_t_11 = 0; __pyx_t_11 < __pyx_t_10; __pyx_t_11+=1) {\n      |                                         ^\nOrange/classification/_tree_scorers.c: In function ‘__pyx_pf_6Orange_14classification_13_tree_scorers_16compute_predictions_csc’:\nOrange/classification/_tree_scorers.c:7857:41: warning: comparison of integer expressions of different signedness: ‘unsigned int’ and ‘int’ [-Wsign-compare]\n 7857 |         for (__pyx_t_11 = 0; __pyx_t_11 < __pyx_t_10; __pyx_t_11+=1) {\n      |                                         ^\nOrange/classification/_tree_scorers.c:7917:143: warning: comparison of integer expressions of different signedness: ‘__pyx_t_5numpy_int32_t’ {aka ‘int’} and ‘unsigned int’ [-Wsign-compare]\n 7917 |               __pyx_t_15 = (((*((__pyx_t_5numpy_int32_t *) ( /* dim=0 */ (__pyx_v_indices.data + __pyx_t_14 * __pyx_v_indices.strides[0]) ))) != __pyx_v_i) != 0);\n      |                                                                                                                                               ^~\nIn file included from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_b173cd294c0a273862501d3fb9129c16fc8eb3df/.venv/lib/python3.7/site-packages/numpy/core/include/numpy/ndarraytypes.h:1830,\n                 from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_b173cd294c0a273862501d3fb9129c16fc8eb3df/.venv/lib/python3.7/site-packages/numpy/core/include/numpy/ndarrayobject.h:12,\n                 from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_b173cd294c0a273862501d3fb9129c16fc8eb3df/.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_b173cd294c0a273862501d3fb9129c16fc8eb3df/.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_b173cd294c0a273862501d3fb9129c16fc8eb3df/.venv/lib/python3.7/site-packages/numpy/core/include/numpy/ndarraytypes.h:1830,\n                 from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_b173cd294c0a273862501d3fb9129c16fc8eb3df/.venv/lib/python3.7/site-packages/numpy/core/include/numpy/ndarrayobject.h:12,\n                 from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_b173cd294c0a273862501d3fb9129c16fc8eb3df/.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_b173cd294c0a273862501d3fb9129c16fc8eb3df/.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_b173cd294c0a273862501d3fb9129c16fc8eb3df/.venv/lib/python3.7/site-packages/numpy/core/include/numpy/ndarraytypes.h:1830,\n                 from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_b173cd294c0a273862501d3fb9129c16fc8eb3df/.venv/lib/python3.7/site-packages/numpy/core/include/numpy/ndarrayobject.h:12,\n                 from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_b173cd294c0a273862501d3fb9129c16fc8eb3df/.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_b173cd294c0a273862501d3fb9129c16fc8eb3df/.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_b173cd294c0a273862501d3fb9129c16fc8eb3df/.venv/lib/python3.7/site-packages/numpy/core/include/numpy/ndarraytypes.h:1830,\n                 from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_b173cd294c0a273862501d3fb9129c16fc8eb3df/.venv/lib/python3.7/site-packages/numpy/core/include/numpy/ndarrayobject.h:12,\n                 from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_b173cd294c0a273862501d3fb9129c16fc8eb3df/.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_b173cd294c0a273862501d3fb9129c16fc8eb3df/.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_b173cd294c0a273862501d3fb9129c16fc8eb3df/.venv/lib/python3.7/site-packages/numpy/core/include/numpy/ndarraytypes.h:1830,\n                 from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_b173cd294c0a273862501d3fb9129c16fc8eb3df/.venv/lib/python3.7/site-packages/numpy/core/include/numpy/ndarrayobject.h:12,\n                 from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_b173cd294c0a273862501d3fb9129c16fc8eb3df/.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_b173cd294c0a273862501d3fb9129c16fc8eb3df/.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_b173cd294c0a273862501d3fb9129c16fc8eb3df/.venv/lib/python3.7/site-packages/numpy/core/include/numpy/ndarraytypes.h:1830,\n                 from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_b173cd294c0a273862501d3fb9129c16fc8eb3df/.venv/lib/python3.7/site-packages/numpy/core/include/numpy/ndarrayobject.h:12,\n                 from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_b173cd294c0a273862501d3fb9129c16fc8eb3df/.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_b173cd294c0a273862501d3fb9129c16fc8eb3df/.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_b173cd294c0a273862501d3fb9129c16fc8eb3df/.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_b173cd294c0a273862501d3fb9129c16fc8eb3df/.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 '*.js' under directory 'Orange/widgets'\nwarning: no files found matching '*.css' under directory 'Orange/widgets'\nwarning: no files found matching '*.html' under directory 'Orange/widgets'\nwarning: no files found matching 'COPYING'\n",
    "new_commit_res_code": 1,
    "new_commit_res_stdout": "============================= test session starts ==============================\nplatform linux -- Python 3.7.9, pytest-7.4.4, pluggy-1.2.0\nrootdir: /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_b173cd294c0a273862501d3fb9129c16fc8eb3df\ncollected 27 items\n\nr2e_tests/test_1.py Esss...........FF.FFFF.....                          [100%]\n\n==================================== ERRORS ====================================\n_______________________ ERROR at setup of test_filename ________________________\nfile /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_b173cd294c0a273862501d3fb9129c16fc8eb3df/Orange/tests/__init__.py, line 65\n  def test_filename(path):\nE       fixture 'path' not found\n>       available fixtures: cache, capfd, capfdbinary, caplog, capsys, capsysbinary, doctest_namespace, monkeypatch, pytestconfig, record_property, record_testsuite_property, record_xml_attribute, recwarn, tmp_path, tmp_path_factory, tmpdir, tmpdir_factory\n>       use 'pytest --fixtures [testpath]' for help on them.\n\n/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_b173cd294c0a273862501d3fb9129c16fc8eb3df/Orange/tests/__init__.py:65\n=================================== FAILURES ===================================\n_______________________ TestOWNomogram.test_nomogram_lr ________________________\n\nself = <r2e_tests.test_1.TestOWNomogram testMethod=test_nomogram_lr>\n\n    def test_nomogram_lr(self):\n        \"\"\"Check probabilities for logistic regression classifier for various\n        values of classes and radio buttons\"\"\"\n        self.widget.display_index = 0  # show ALL features\n>       self._test_helper(self.lr_cls, [58, 42])\n\nr2e_tests/test_1.py:92: \n_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ \nr2e_tests/test_1.py:169: in _test_helper\n    self._test_helper_check_probability(values[i])\nr2e_tests/test_1.py:200: in _test_helper_check_probability\n    prob_marker.get_tooltip_text())\nE   AssertionError: 'Probability: 58' not found in '<html><head><style type=\"text/css\">ul {margin-top: 1px; margin-bottom: 1px;}</style>\\n    </head><body><b>diameter narrowing=\\'0\\'</b><hr/>Total: -0.0 <br/>Probability: 0%</body></html>\\n    '\n__________________ TestOWNomogram.test_nomogram_lr_multiclass __________________\n\nself = <r2e_tests.test_1.TestOWNomogram testMethod=test_nomogram_lr_multiclass>\n\n    def test_nomogram_lr_multiclass(self):\n        \"\"\"Check probabilities for logistic regression classifier for various\n        values of classes and radio buttons for multiclass data\"\"\"\n        cls = LogisticRegressionLearner(\n            multi_class=\"ovr\", solver=\"liblinear\"\n        )(self.lenses)\n>       self._test_helper(cls, [9, 45, 52])\n\nr2e_tests/test_1.py:106: \n_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ \nr2e_tests/test_1.py:169: in _test_helper\n    self._test_helper_check_probability(values[i])\nr2e_tests/test_1.py:200: in _test_helper_check_probability\n    prob_marker.get_tooltip_text())\nE   AssertionError: 'Probability: 9' not found in '<html><head><style type=\"text/css\">ul {margin-top: 1px; margin-bottom: 1px;}</style>\\n    </head><body><b>lenses=\\'hard\\'</b><hr/>Total: -0.0 <br/>Probability: 2%</body></html>\\n    '\n__________________ TestOWNomogram.test_nomogram_nb_multiclass __________________\n\nself = <r2e_tests.test_1.TestOWNomogram testMethod=test_nomogram_nb_multiclass>\n\n    def test_nomogram_nb_multiclass(self):\n        \"\"\"Check probabilities for naive bayes classifier for various values\n        of classes and radio buttons for multiclass data\"\"\"\n        cls = NaiveBayesLearner()(self.lenses)\n>       self._test_helper(cls, [19, 53, 13])\n\nr2e_tests/test_1.py:98: \n_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ \nr2e_tests/test_1.py:169: in _test_helper\n    self._test_helper_check_probability(values[i])\nr2e_tests/test_1.py:200: in _test_helper_check_probability\n    prob_marker.get_tooltip_text())\nE   AssertionError: 'Probability: 53' not found in '<html><head><style type=\"text/css\">ul {margin-top: 1px; margin-bottom: 1px;}</style>\\n    </head><body><b>lenses=\\'none\\'</b><hr/>Total: -0.0 <br/>Probability: 59%</body></html>\\n    '\n________________ TestOWNomogram.test_nomogram_with_instance_lr _________________\n\nself = <r2e_tests.test_1.TestOWNomogram testMethod=test_nomogram_with_instance_lr>\n\n    def test_nomogram_with_instance_lr(self):\n        \"\"\"Check initialized marker values and feature sorting for logistic\n        regression classifier and data on input\"\"\"\n        cls = LogisticRegressionLearner()(self.titanic)\n        data = self.titanic[10:11]\n        self.send_signal(self.widget.Inputs.classifier, cls)\n        self.send_signal(self.widget.Inputs.data, data)\n>       self._check_values(data.domain.attributes, data)\n\nr2e_tests/test_1.py:129: \n_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ \nr2e_tests/test_1.py:208: in _check_values\n    self.assertIn(value, item.dot.get_tooltip_text())\nE   AssertionError: 'first: 100%' not found in '<html><head><style type=\"text/css\">ul {margin-top: 1px; margin-bottom: 1px;}</style>\\n    </head><body><b>Points: 1.74</b><hr/>second: 0%<br/>third: 100%</body></html>\\n    '\n________________ TestOWNomogram.test_nomogram_with_instance_nb _________________\n\nself = <r2e_tests.test_1.TestOWNomogram testMethod=test_nomogram_with_instance_nb>\n\n    def test_nomogram_with_instance_nb(self):\n        \"\"\"Check initialized marker values and feature sorting for naive bayes\n        classifier and data on input\"\"\"\n        cls = NaiveBayesLearner()(self.titanic)\n        data = self.titanic[10:11]\n        self.send_signal(self.widget.Inputs.classifier, cls)\n        self.send_signal(self.widget.Inputs.data, data)\n>       self._check_values(data.domain.attributes, data)\n\nr2e_tests/test_1.py:115: \n_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ \nr2e_tests/test_1.py:208: in _check_values\n    self.assertIn(value, item.dot.get_tooltip_text())\nE   AssertionError: 'first: 100%' not found in '<html><head><style type=\"text/css\">ul {margin-top: 1px; margin-bottom: 1px;}</style>\\n    </head><body><b>Points: 0.41</b><hr/>third: 0%<br/>crew: 100%</body></html>\\n    '\n__________________________ TestOWNomogram.test_output __________________________\n\nself = <r2e_tests.test_1.TestOWNomogram testMethod=test_output>\n\n    def test_output(self):\n        cls = LogisticRegressionLearner()(self.titanic)\n        data = self.titanic[10:11]\n        status, age, sex = data.domain.attributes\n        self.send_signal(self.widget.Inputs.classifier, cls)\n        self.widget.sort_combo.setCurrentIndex(1)\n        self.widget.sort_combo.activated.emit(1)\n    \n        # Output more attributer than there are -> output all\n        self.widget.n_attributes = 5\n        self.widget.n_spin.valueChanged.emit(5)\n        attrs = self.get_output(self.widget.Outputs.features)\n        self.assertEqual(attrs, [age, sex, status])\n    \n        # Output the first two\n        self.widget.n_attributes = 2\n        self.widget.n_spin.valueChanged.emit(2)\n        attrs = self.get_output(self.widget.Outputs.features)\n        self.assertEqual(attrs, [age, sex])\n    \n        # Set to output all\n        self.widget.display_index = 0\n        self.widget.controls.display_index.group.buttonClicked[int].emit(0)\n        attrs = self.get_output(self.widget.Outputs.features)\n>       self.assertEqual(attrs, [age, sex, status])\nE       AssertionError: [Disc[103 chars]le'))] != [Disc[103 chars]le')), DiscreteVariable(name='status', values=[33 chars]d'))]\n\nr2e_tests/test_1.py:247: AssertionError\n==================================== PASSES ====================================\n_________________________ TestOWNomogram.test_tooltip __________________________\n----------------------------- Captured stderr call -----------------------------\nThis plugin does not support raise()\n=========================== short test summary info ============================\nPASSED r2e_tests/test_1.py::TestOWNomogram::test_adjust_scale\nPASSED r2e_tests/test_1.py::TestOWNomogram::test_constant_feature_cont\nPASSED r2e_tests/test_1.py::TestOWNomogram::test_constant_feature_disc\nPASSED r2e_tests/test_1.py::TestOWNomogram::test_dots_stop_flashing\nPASSED r2e_tests/test_1.py::TestOWNomogram::test_image_export\nPASSED r2e_tests/test_1.py::TestOWNomogram::test_input_instance\nPASSED r2e_tests/test_1.py::TestOWNomogram::test_input_invalid_cls\nPASSED r2e_tests/test_1.py::TestOWNomogram::test_input_lr_cls\nPASSED r2e_tests/test_1.py::TestOWNomogram::test_input_nb_cls\nPASSED r2e_tests/test_1.py::TestOWNomogram::test_minimum_size\nPASSED r2e_tests/test_1.py::TestOWNomogram::test_msg_base_class\nPASSED r2e_tests/test_1.py::TestOWNomogram::test_nomogram_nb\nPASSED r2e_tests/test_1.py::TestOWNomogram::test_reconstruct_domain\nPASSED r2e_tests/test_1.py::TestOWNomogram::test_reset_settings\nPASSED r2e_tests/test_1.py::TestOWNomogram::test_summary\nPASSED r2e_tests/test_1.py::TestOWNomogram::test_target_values\nPASSED r2e_tests/test_1.py::TestOWNomogram::test_tooltip\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\nERROR r2e_tests/test_1.py::test_filename\nFAILED r2e_tests/test_1.py::TestOWNomogram::test_nomogram_lr - AssertionError...\nFAILED r2e_tests/test_1.py::TestOWNomogram::test_nomogram_lr_multiclass - Ass...\nFAILED r2e_tests/test_1.py::TestOWNomogram::test_nomogram_nb_multiclass - Ass...\nFAILED r2e_tests/test_1.py::TestOWNomogram::test_nomogram_with_instance_lr - ...\nFAILED r2e_tests/test_1.py::TestOWNomogram::test_nomogram_with_instance_nb - ...\nFAILED r2e_tests/test_1.py::TestOWNomogram::test_output - AssertionError: [Di...\n=============== 6 failed, 17 passed, 3 skipped, 1 error in 5.48s ===============\nWidgets left: 0    Max widgets: 2591 \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_b173cd294c0a273862501d3fb9129c16fc8eb3df\ncollected 27 items\n\nr2e_tests/test_1.py Esss...........FF.FFFFF....                          [100%]\n\n==================================== ERRORS ====================================\n_______________________ ERROR at setup of test_filename ________________________\nfile /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_b173cd294c0a273862501d3fb9129c16fc8eb3df/Orange/tests/__init__.py, line 65\n  def test_filename(path):\nE       fixture 'path' not found\n>       available fixtures: cache, capfd, capfdbinary, caplog, capsys, capsysbinary, doctest_namespace, monkeypatch, pytestconfig, record_property, record_testsuite_property, record_xml_attribute, recwarn, tmp_path, tmp_path_factory, tmpdir, tmpdir_factory\n>       use 'pytest --fixtures [testpath]' for help on them.\n\n/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_b173cd294c0a273862501d3fb9129c16fc8eb3df/Orange/tests/__init__.py:65\n=================================== FAILURES ===================================\n_______________________ TestOWNomogram.test_nomogram_lr ________________________\n\nself = <r2e_tests.test_1.TestOWNomogram testMethod=test_nomogram_lr>\n\n    def test_nomogram_lr(self):\n        \"\"\"Check probabilities for logistic regression classifier for various\n        values of classes and radio buttons\"\"\"\n        self.widget.display_index = 0  # show ALL features\n>       self._test_helper(self.lr_cls, [58, 42])\n\nr2e_tests/test_1.py:92: \n_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ \nr2e_tests/test_1.py:169: in _test_helper\n    self._test_helper_check_probability(values[i])\nr2e_tests/test_1.py:200: in _test_helper_check_probability\n    prob_marker.get_tooltip_text())\nE   AssertionError: 'Probability: 58' not found in '<html><head><style type=\"text/css\">ul {margin-top: 1px; margin-bottom: 1px;}</style>\\n    </head><body><b>diameter narrowing=\\'0\\'</b><hr/>Total: -0.0 <br/>Probability: 0%</body></html>\\n    '\n__________________ TestOWNomogram.test_nomogram_lr_multiclass __________________\n\nself = <r2e_tests.test_1.TestOWNomogram testMethod=test_nomogram_lr_multiclass>\n\n    def test_nomogram_lr_multiclass(self):\n        \"\"\"Check probabilities for logistic regression classifier for various\n        values of classes and radio buttons for multiclass data\"\"\"\n        cls = LogisticRegressionLearner(\n            multi_class=\"ovr\", solver=\"liblinear\"\n        )(self.lenses)\n>       self._test_helper(cls, [9, 45, 52])\n\nr2e_tests/test_1.py:106: \n_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ \nr2e_tests/test_1.py:169: in _test_helper\n    self._test_helper_check_probability(values[i])\nr2e_tests/test_1.py:200: in _test_helper_check_probability\n    prob_marker.get_tooltip_text())\nE   AssertionError: 'Probability: 9' not found in '<html><head><style type=\"text/css\">ul {margin-top: 1px; margin-bottom: 1px;}</style>\\n    </head><body><b>lenses=\\'hard\\'</b><hr/>Total: -0.0 <br/>Probability: 2%</body></html>\\n    '\n__________________ TestOWNomogram.test_nomogram_nb_multiclass __________________\n\nself = <r2e_tests.test_1.TestOWNomogram testMethod=test_nomogram_nb_multiclass>\n\n    def test_nomogram_nb_multiclass(self):\n        \"\"\"Check probabilities for naive bayes classifier for various values\n        of classes and radio buttons for multiclass data\"\"\"\n        cls = NaiveBayesLearner()(self.lenses)\n>       self._test_helper(cls, [19, 53, 13])\n\nr2e_tests/test_1.py:98: \n_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ \nr2e_tests/test_1.py:169: in _test_helper\n    self._test_helper_check_probability(values[i])\nr2e_tests/test_1.py:200: in _test_helper_check_probability\n    prob_marker.get_tooltip_text())\nE   AssertionError: 'Probability: 53' not found in '<html><head><style type=\"text/css\">ul {margin-top: 1px; margin-bottom: 1px;}</style>\\n    </head><body><b>lenses=\\'none\\'</b><hr/>Total: -0.0 <br/>Probability: 59%</body></html>\\n    '\n________________ TestOWNomogram.test_nomogram_with_instance_lr _________________\n\nself = <r2e_tests.test_1.TestOWNomogram testMethod=test_nomogram_with_instance_lr>\n\n    def test_nomogram_with_instance_lr(self):\n        \"\"\"Check initialized marker values and feature sorting for logistic\n        regression classifier and data on input\"\"\"\n        cls = LogisticRegressionLearner()(self.titanic)\n        data = self.titanic[10:11]\n        self.send_signal(self.widget.Inputs.classifier, cls)\n        self.send_signal(self.widget.Inputs.data, data)\n>       self._check_values(data.domain.attributes, data)\n\nr2e_tests/test_1.py:129: \n_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ \nr2e_tests/test_1.py:208: in _check_values\n    self.assertIn(value, item.dot.get_tooltip_text())\nE   AssertionError: 'first: 100%' not found in '<html><head><style type=\"text/css\">ul {margin-top: 1px; margin-bottom: 1px;}</style>\\n    </head><body><b>Points: 1.74</b><hr/>second: 0%<br/>third: 100%</body></html>\\n    '\n________________ TestOWNomogram.test_nomogram_with_instance_nb _________________\n\nself = <r2e_tests.test_1.TestOWNomogram testMethod=test_nomogram_with_instance_nb>\n\n    def test_nomogram_with_instance_nb(self):\n        \"\"\"Check initialized marker values and feature sorting for naive bayes\n        classifier and data on input\"\"\"\n        cls = NaiveBayesLearner()(self.titanic)\n        data = self.titanic[10:11]\n        self.send_signal(self.widget.Inputs.classifier, cls)\n        self.send_signal(self.widget.Inputs.data, data)\n>       self._check_values(data.domain.attributes, data)\n\nr2e_tests/test_1.py:115: \n_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ \nr2e_tests/test_1.py:208: in _check_values\n    self.assertIn(value, item.dot.get_tooltip_text())\nE   AssertionError: 'first: 100%' not found in '<html><head><style type=\"text/css\">ul {margin-top: 1px; margin-bottom: 1px;}</style>\\n    </head><body><b>Points: 0.41</b><hr/>third: 0%<br/>crew: 100%</body></html>\\n    '\n__________________________ TestOWNomogram.test_output __________________________\n\nself = <r2e_tests.test_1.TestOWNomogram testMethod=test_output>\n\n    def test_output(self):\n        cls = LogisticRegressionLearner()(self.titanic)\n        data = self.titanic[10:11]\n        status, age, sex = data.domain.attributes\n        self.send_signal(self.widget.Inputs.classifier, cls)\n        self.widget.sort_combo.setCurrentIndex(1)\n        self.widget.sort_combo.activated.emit(1)\n    \n        # Output more attributer than there are -> output all\n        self.widget.n_attributes = 5\n        self.widget.n_spin.valueChanged.emit(5)\n        attrs = self.get_output(self.widget.Outputs.features)\n        self.assertEqual(attrs, [age, sex, status])\n    \n        # Output the first two\n        self.widget.n_attributes = 2\n        self.widget.n_spin.valueChanged.emit(2)\n        attrs = self.get_output(self.widget.Outputs.features)\n        self.assertEqual(attrs, [age, sex])\n    \n        # Set to output all\n        self.widget.display_index = 0\n        self.widget.controls.display_index.group.buttonClicked[int].emit(0)\n        attrs = self.get_output(self.widget.Outputs.features)\n>       self.assertEqual(attrs, [age, sex, status])\nE       AssertionError: [Disc[103 chars]le'))] != [Disc[103 chars]le')), DiscreteVariable(name='status', values=[33 chars]d'))]\n\nr2e_tests/test_1.py:247: AssertionError\n____________________ TestOWNomogram.test_reconstruct_domain ____________________\n\nself = <r2e_tests.test_1.TestOWNomogram testMethod=test_reconstruct_domain>\n\n    def test_reconstruct_domain(self):\n        data = Table(\"heart_disease\")\n        cls = LogisticRegressionLearner()(data)\n        domain = OWNomogram.reconstruct_domain(cls.original_domain, cls.domain)\n        transformed_data = cls.original_data.transform(domain)\n        self.assertEqual(transformed_data.X.shape, data.X.shape)\n        self.assertFalse(np.isnan(transformed_data.X[0]).any())\n    \n        scaled_data = Scale()(data)\n        cls = LogisticRegressionLearner()(scaled_data)\n        domain = OWNomogram.reconstruct_domain(cls.original_domain, cls.domain)\n        transformed_data = cls.original_data.transform(domain)\n        self.assertEqual(transformed_data.X.shape, scaled_data.X.shape)\n>       self.assertFalse(np.isnan(transformed_data.X[0]).any())\nE       AssertionError: True is not false\n\nr2e_tests/test_1.py:323: AssertionError\n==================================== PASSES ====================================\n_________________________ TestOWNomogram.test_tooltip __________________________\n----------------------------- Captured stderr call -----------------------------\nThis plugin does not support raise()\n=========================== short test summary info ============================\nPASSED r2e_tests/test_1.py::TestOWNomogram::test_adjust_scale\nPASSED r2e_tests/test_1.py::TestOWNomogram::test_constant_feature_cont\nPASSED r2e_tests/test_1.py::TestOWNomogram::test_constant_feature_disc\nPASSED r2e_tests/test_1.py::TestOWNomogram::test_dots_stop_flashing\nPASSED r2e_tests/test_1.py::TestOWNomogram::test_image_export\nPASSED r2e_tests/test_1.py::TestOWNomogram::test_input_instance\nPASSED r2e_tests/test_1.py::TestOWNomogram::test_input_invalid_cls\nPASSED r2e_tests/test_1.py::TestOWNomogram::test_input_lr_cls\nPASSED r2e_tests/test_1.py::TestOWNomogram::test_input_nb_cls\nPASSED r2e_tests/test_1.py::TestOWNomogram::test_minimum_size\nPASSED r2e_tests/test_1.py::TestOWNomogram::test_msg_base_class\nPASSED r2e_tests/test_1.py::TestOWNomogram::test_nomogram_nb\nPASSED r2e_tests/test_1.py::TestOWNomogram::test_reset_settings\nPASSED r2e_tests/test_1.py::TestOWNomogram::test_summary\nPASSED r2e_tests/test_1.py::TestOWNomogram::test_target_values\nPASSED r2e_tests/test_1.py::TestOWNomogram::test_tooltip\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\nERROR r2e_tests/test_1.py::test_filename\nFAILED r2e_tests/test_1.py::TestOWNomogram::test_nomogram_lr - AssertionError...\nFAILED r2e_tests/test_1.py::TestOWNomogram::test_nomogram_lr_multiclass - Ass...\nFAILED r2e_tests/test_1.py::TestOWNomogram::test_nomogram_nb_multiclass - Ass...\nFAILED r2e_tests/test_1.py::TestOWNomogram::test_nomogram_with_instance_lr - ...\nFAILED r2e_tests/test_1.py::TestOWNomogram::test_nomogram_with_instance_nb - ...\nFAILED r2e_tests/test_1.py::TestOWNomogram::test_output - AssertionError: [Di...\nFAILED r2e_tests/test_1.py::TestOWNomogram::test_reconstruct_domain - Asserti...\n=============== 7 failed, 16 passed, 3 skipped, 1 error in 3.58s ===============\nWidgets left: 0    Max widgets: 2595 \n\n",
    "old_commit_res_stderr": ""
}
Orange/widgets/visualize/ownomogram.py Orange/widgets/visualize/tests/test_ownomogram.py
We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
[ISSUE]
**Title:** Reconstructing Domain in OWNomogram Introduces NaN Values in Transformed Data

**Description:**
When attempting to reconstruct the domain and transform data using the `OWNomogram` widget, the transformed data contains `NaN` values in the first row. This issue causes the transformation to produce invalid results, leading to downstream errors in data processing.

**Example Code:**
```python
def test_reconstruct_domain(self):
    data = Table("heart_disease")
    cls = LogisticRegressionLearner()(data)
    domain = OWNomogram.reconstruct_domain(cls.original_domain, cls.domain)
    transformed_data = cls.original_data.transform(domain)
    # Unexpected NaN values in transformed_data.X
```

**Expected Behavior:**
The `transformed_data.X` should have the same shape as the original `data.X` and should not contain any `NaN` values. The domain reconstruction should accurately retain all original compute values without introducing invalid entries.

**Actual Behavior:**
After reconstructing the domain and transforming the data, the first row of `transformed_data.X` contains `NaN` values:
```
AssertionError: True is not false
```
This indicates that `np.isnan(transformed_data.X[0]).any()` returns `True`, meaning there are `NaN` values where there should be valid numerical data.

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

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

--- BEGIN FILE ---
```
### Orange/widgets/visualize/ownomogram.py
import time
import warnings
from itertools import chain
from typing import List
from functools import singledispatch
from enum import IntEnum
from collections import OrderedDict

import numpy as np

from AnyQt.QtWidgets import (
    QGraphicsView, QGraphicsScene, QGraphicsItem, QGraphicsSimpleTextItem,
    QGraphicsTextItem, QGraphicsLineItem, QGraphicsWidget, QGraphicsRectItem,
    QGraphicsEllipseItem, QGraphicsLinearLayout, QGridLayout, QLabel, QFrame,
    QSizePolicy, QDesktopWidget,
)
from AnyQt.QtGui import QColor, QPainter, QFont, QPen, QBrush
from AnyQt.QtCore import Qt, QRectF, QSize, QPropertyAnimation, QObject, \
    pyqtProperty

from Orange.data import Table, Domain, DiscreteVariable, ContinuousVariable
from Orange.statistics.util import nanmin, nanmax, nanmean, unique
from Orange.classification import Model
from Orange.classification.naive_bayes import NaiveBayesModel
from Orange.classification.logistic_regression import \
    LogisticRegressionClassifier
from Orange.widgets.settings import Setting, ContextSetting, \
    ClassValuesContextHandler
from Orange.widgets.utils.widgetpreview import WidgetPreview
from Orange.widgets.utils.state_summary import format_summary_details
from Orange.widgets.widget import OWWidget, Msg, Input, Output, AttributeList
from Orange.widgets import gui


def collides(item, items):
    return any(item.collidesWithItem(i) for i in items)


class SortBy(IntEnum):
    NO_SORTING, NAME, ABSOLUTE, POSITIVE, NEGATIVE = 0, 1, 2, 3, 4

    @staticmethod
    def items():
        return ["No sorting", "Name", "Absolute importance",
                "Positive influence", "Negative influence"]


class MovableToolTip(QLabel):
    def __init__(self):
        super().__init__()
        self.setFrameShape(QFrame.StyledPanel)
        self.setWindowFlags(Qt.ToolTip)
        self.hide()

    def show(self, pos, text, change_y=True):
        self.setText(text)
        self.adjustSize()

        x, y = pos.x(), (pos.y() + 15 if change_y else self.y())
        avail = QDesktopWidget().availableGeometry(self)
        if x + self.width() > avail.right():
            x -= self.width()
        if y + self.height() > avail.bottom():
            y = (pos.y() - 10 - self.height() if change_y else self.y() - self.height())
        self.move(x, y)
        super().show()


DOT_COLOR = QColor(170, 220, 255, 255)


class DotItem(QGraphicsEllipseItem):
    TOOLTIP_STYLE = """ul {margin-top: 1px; margin-bottom: 1px;}"""
    TOOLTIP_TEMPLATE = """<html><head><style type="text/css">{}</style>
    </head><body><b>{}</b><hr/>{}</body></html>
    """

    def __init__(self, radius, scale, offset, min_x, max_x):
        super().__init__(0, 0, radius, radius)
        self._min_x = min_x * scale - radius / 2 + offset
        self._max_x = max_x * scale - radius / 2 + offset
        self._scale = scale
        self._offset = offset
        self.setPos(0, - radius / 2)
        self.setFlag(QGraphicsItem.ItemIsMovable)
        self.setBrush(DOT_COLOR)
        self.setPen(QPen(QBrush(QColor(20, 130, 250, 255)), 2))
        self.setZValue(100)
        self.tool_tip = MovableToolTip()
        self.setAcceptHoverEvents(True)

    @property
    def value(self):
        return (self.x() + self.rect().width() / 2 - self._offset) / self._scale

    def move(self, x):
        self.setX(x)

    def move_to_val(self, val):
        x = np.clip(self._scale * val - self.rect().width() / 2 + self._offset,
                    self._min_x, self._max_x)
        self.move(x)

    def hoverEnterEvent(self, event):
        self.tool_tip.show(event.screenPos(), self.get_tooltip_text())

    def hoverLeaveEvent(self, event):
        self.tool_tip.hide()

    def mouseMoveEvent(self, _):
        # Prevent click-moving of these items
        return


class ProbabilitiesDotItem(DotItem):
    def __init__(self, radius, scale, offset, min_x, max_x, title,
                 get_probabilities):
        self.title = title
        self.get_probabilities = get_probabilities
        self.movable_dot_items = []
        self._invisible_sum = 0
        super().__init__(radius, scale, offset, min_x, max_x)
        self.setBrush(QColor(150, 150, 150, 255))
        self.setPen(QPen(QBrush(QColor(75, 75, 75, 255)), 2))

    def move_to_sum(self, invisible_sum: float = None):
        total = sum(item.value for item in self.movable_dot_items)

        if invisible_sum is not None:
            self._invisible_sum = invisible_sum
        total += self._invisible_sum

        self.move_to_val(total)
        self.parentItem().rescale()

    def get_tooltip_text(self):
        text = "Total: {} <br/>Probability: {:.0%}".format(
            np.round(self.value, 2),
            np.round(self.get_probabilities(self.value), 2))
        return self.TOOLTIP_TEMPLATE.format(
            self.TOOLTIP_STYLE, self.title, text)


class MovableDotItem(DotItem):
    def __init__(self, radius, scale, offset, min_x, max_x):
        self.tooltip_labels = []
        self.tooltip_values = []
        super().__init__(radius, scale, offset, min_x, max_x)
        self._x = min_x * scale - radius / 2 + offset
        self._point_dot = None
        self._total_dot = None
        self._probs_dot = None
        self._vertical_line = None
        self._mousePressFunc = None

    @property
    def vertical_line(self):
        return self._vertical_line

    @vertical_line.setter
    def vertical_line(self, line):
        line.setVisible(False)
        self._vertical_line = line

    @property
    def point_dot(self):
        return self._point_dot

    @point_dot.setter
    def point_dot(self, dot):
        dot.setVisible(False)
        self._point_dot = dot

    @property
    def total_dot(self):
        return self._total_dot

    @total_dot.setter
    def total_dot(self, dot):
        self._total_dot = dot
        self._total_dot.movable_dot_items.append(self)

    @property
    def probs_dot(self):
        return self._probs_dot

    @probs_dot.setter
    def probs_dot(self, dot):
        self._probs_dot = dot
        self._probs_dot.movable_dot_items.append(self)

    def hookOnMousePress(self, func):
        self._mousePressFunc = func

    def unhookOnMousePress(self):
        self._mousePressFunc = None

    def mousePressEvent(self, event):
        if self._mousePressFunc:
            self._mousePressFunc()
            self._mousePressFunc = None
        self.tool_tip.show(event.screenPos(), self.get_tooltip_text(), False)
        self._x = event.pos().x()
        self.setBrush(QColor(50, 180, 250, 255))
        self._show_vertical_line_and_point_dot()
        return super().mousePressEvent(event)

    def mouseMoveEvent(self, event):
        self.tool_tip.show(event.screenPos(), self.get_tooltip_text(), False)
        delta_x = event.pos().x() - self._x
        if self._min_x <= self.x() + delta_x <= self._max_x:
            self.move(self.x() + delta_x)
            mod_tooltip_values = [0] + list(self.tooltip_values)
            if np.round(self.value, 1) in np.round(mod_tooltip_values, 1):
                index = np.where(np.round(mod_tooltip_values, 1) ==
                                 np.round(self.value, 1))
                time.sleep(0.05)
                self.move_to_val(mod_tooltip_values[index[0][0]])
        elif self.x() + delta_x < self._min_x:
            self.move(self._min_x)
        elif self.x() + delta_x > self._max_x:
            self.move(self._max_x)
        self._show_vertical_line_and_point_dot()
        self.probs_dot.move_to_sum()

    def mouseReleaseEvent(self, event):
        self.tool_tip.hide()
        self.setBrush(QColor(170, 220, 255, 255))
        self.point_dot.setVisible(False)
        self.vertical_line.setVisible(False)
        return super().mousePressEvent(event)

    def _show_vertical_line_and_point_dot(self):
        self.vertical_line.setX(self.x() + self.rect().width() / 2 - self._offset)
        self.vertical_line.setVisible(True)
        self.point_dot.move_to_val(self.value)
        self.point_dot.setVisible(True)


class DiscreteMovableDotItem(MovableDotItem):
    def get_tooltip_text(self):
        labels = self._get_tooltip_labels_with_percentages()
        return self.TOOLTIP_TEMPLATE.format(
            self.TOOLTIP_STYLE, "Points: {}".format(np.round(self.value, 2)),
            "".join("{}: {:.0%}<br/>".format(l, v) for l, v in labels)[:-5])

    def _get_tooltip_labels_with_percentages(self):
        if not len(self.tooltip_labels):
            return []
        for i, val in enumerate(self.tooltip_values):
            if val > self.value:
                break
        # pylint: disable=undefined-loop-variable
        diff = self.tooltip_values[i] - self.tooltip_values[i - 1]
        p1 = 0 if diff < 1e-6 else (-self.value + self.tooltip_values[i]) / diff
        return [(self.tooltip_labels[i - 1].replace("<", "&lt;"), abs(p1)),
                (self.tooltip_labels[i].replace("<", "&lt;"), abs(1 - p1))]


class GraphicsColorAnimator(QObject):
    @pyqtProperty(QColor)
    def brushColor(self):
        return self.__brushColor

    @brushColor.setter
    def brushColor(self, value):
        self.__brushColor = value
        for item in self.__items:
            item.setBrush(value)

    def __init__(self, parent, duration, keyValues):
        super().__init__(parent)
        self.__items = []
        self.__defaultColor = defaultColor = keyValues[0][1]
        self.__brushColor = defaultColor

        self.__animation = QPropertyAnimation(self, b'brushColor', self)
        self.__animation.setStartValue(defaultColor)
        self.__animation.setEndValue(defaultColor)
        self.__animation.setDuration(duration)
        self.__animation.setKeyValues(keyValues)
        self.__animation.setLoopCount(-1)

    def setGraphicsItems(self, items):
        if self.__animation.state() == QPropertyAnimation.Running:
            self.__animation.stop()
        self.__items = items
        for item in items:
            item.hookOnMousePress(self.stop)

    def start(self):
        self.__animation.start()

    def stop(self):
        if self.__animation.state() != QPropertyAnimation.Running:
            return
        self.__animation.stop()
        for item in self.__items:
            item.setBrush(self.__defaultColor)

    def clear(self):
        for item in self.__items:
            item.unhookOnMousePress()
        self.__items = []



class ContinuousItemMixin:
    def get_tooltip_text(self):
        return self.TOOLTIP_TEMPLATE.format(
            self.TOOLTIP_STYLE, "Points: {}".format(np.round(self.value, 2)),
            "Value: {}".format(np.round(self._get_tooltip_label_value(), 1)))

    def _get_tooltip_label_value(self):
        if not len(self.tooltip_labels):
            return self.value
        start = float(self.tooltip_labels[0])
        stop = float(self.tooltip_labels[-1])
        delta = (self.tooltip_values[-1] - self.tooltip_values[0])
        if not delta:
            return np.nan
        return start + self.value * (stop - start) / delta


class ContinuousMovableDotItem(MovableDotItem, ContinuousItemMixin):
    pass


class Continuous2DMovableDotItem(MovableDotItem, ContinuousItemMixin):
    def __init__(self, radius, scale, offset, min_x, max_x, min_y, max_y):
        super().__init__(radius, scale, offset, min_x, max_x)
        self._min_y = min_y
        self._max_y = max_y
        self._horizontal_line = None

    @property
    def horizontal_line(self):
        return self._horizontal_line

    @horizontal_line.setter
    def horizontal_line(self, line):
        line.setVisible(False)
        self._horizontal_line = line

    def move(self, x):
        super().move(x)
        diff_ = np.nan_to_num(self._max_x - self._min_x)
        k = (x - self._min_x) / diff_ if diff_ else 0
        self.setY(self._min_y - self.rect().width() / 2 + (self._max_y - self._min_y) * k)

    def mousePressEvent(self, event):
        self._show_horizontal_line()
        return super().mousePressEvent(event)

    def mouseMoveEvent(self, event):
        super().mouseMoveEvent(event)
        self._show_horizontal_line()

    def mouseReleaseEvent(self, event):
        self.horizontal_line.setVisible(False)
        return super().mouseReleaseEvent(event)

    def _show_horizontal_line(self):
        self.horizontal_line.setY(self.y() + self.rect().width() / 2 -
                                  abs(self._max_y - self._min_y) / 2)
        self.horizontal_line.setVisible(True)


class RulerItem(QGraphicsWidget):
    tick_height = 6
    tick_width = 0
    DOT_RADIUS = 12
    half_tick_height = 3
    bold_label = True
    DOT_ITEM_CLS = DotItem

    def __init__(self, name, values, scale, name_offset, offset, labels=None):
        super().__init__()

        # leading label
        font = name.document().defaultFont()
        if self.bold_label:
            font.setWeight(QFont.Bold)
        name.setFont(font)
        name.setPos(name_offset, -10)
        name.setParentItem(self)

        # prediction marker
        self.dot = self.DOT_ITEM_CLS(self.DOT_RADIUS, scale, offset, values[0],
                                     values[-1])
        self.dot.setParentItem(self)

        # pylint: disable=unused-variable
        # line
        line = QGraphicsLineItem(min(values) * scale + offset, 0,
                                 max(values) * scale + offset, 0,
                                 self)

        if labels is None:
            labels = [str(abs(v) if v == -0 else v) for v in values]

        old_x_tick = None
        shown_items = []
        w = QGraphicsSimpleTextItem(labels[0]).boundingRect().width()
        text_finish = values[0] * scale - w + offset - 10
        for i, (label, value) in enumerate(zip(labels, values)):
            text = QGraphicsSimpleTextItem(label)
            x_text = value * scale - text.boundingRect().width() / 2 + offset
            if text_finish > x_text - 10:
                y_text, y_tick = self.DOT_RADIUS * 0.7, 0
                text_finish = values[0] * scale + offset
            else:
                y_text = - text.boundingRect().height() - self.DOT_RADIUS * 0.7
                y_tick = - self.tick_height
                text_finish = x_text + text.boundingRect().width()
            text.setPos(x_text, y_text)
            if not collides(text, shown_items):
                text.setParentItem(self)
                shown_items.append(text)

            x_tick = value * scale - self.tick_width / 2 + offset
            tick = QGraphicsRectItem(
                x_tick, y_tick, self.tick_width, self.tick_height,
                self)
            tick.setBrush(QColor(Qt.black))

            if self.half_tick_height and i:
                x = x_tick - (x_tick - old_x_tick) / 2
                half_tick = QGraphicsLineItem(x, - self.half_tick_height, x, 0,
                                              self)
            old_x_tick = x_tick


class ProbabilitiesRulerItem(QGraphicsWidget):
    tick_height = 6
    DOT_RADIUS = 14
    y_diff = 4

    def __init__(self, name, values, scale, name_offset, offset, get_points,
                 title, get_probabilities):
        super().__init__()
        self.scale = scale
        self.offset = offset
        self.get_points = get_points
        self.min_val = min(values)
        self.max_val = max(values)

        # leading labels
        font = name.document().defaultFont()
        font.setWeight(QFont.Bold)
        name_total = QGraphicsTextItem("Total", self)
        name_total.setFont(font)
        name_total.setPos(name_offset, -25)
        name.setFont(font)
        name.setPos(name_offset, 10)
        name.setParentItem(self)

        # prediction marker
        self.dot = ProbabilitiesDotItem(
            self.DOT_RADIUS, scale, offset, values[0], values[-1],
            title, get_probabilities)
        self.dot.setPos(0, (- self.DOT_RADIUS + self.y_diff) / 2)
        self.dot.setParentItem(self)

        # pylint: disable=unused-variable
        # two lines
        t_line = QGraphicsLineItem(self.min_val * scale + offset, 0,
                                   self.max_val * scale + offset, 0,
                                   self)
        p_line = QGraphicsLineItem(self.min_val * scale + offset, self.y_diff,
                                   self.max_val * scale + offset, self.y_diff,
                                   self)

        # ticks and labels
        old_x_tick = values[0] * scale + offset
        for i, value in enumerate(values[1:]):
            x_tick = value * scale + offset
            x = x_tick - (x_tick - old_x_tick) / 2
            half_tick = QGraphicsLineItem(x, - self.tick_height / 2, x, 0,
                                          self)
            old_x_tick = x_tick
            if i == len(values) - 2:
                break
            text = QGraphicsTextItem(str(abs(value) if value == -0 else value),
                                     self)
            x_text = value * scale - text.boundingRect().width() / 2 + offset
            y_text = - text.boundingRect().height() - self.DOT_RADIUS * 0.7
            text.setPos(x_text, y_text)
            tick = QGraphicsLineItem(x_tick, -self.tick_height, x_tick, 0,
                                     self)

        self.prob_items = [
            (i / 10, QGraphicsTextItem(" " + str(i * 10) + " "),
             QGraphicsLineItem(0, 0, 0, 0)) for i in range(1, 10)]

    def rescale(self):
        shown_items = []
        for prob, text, tick in self.prob_items:
            pts = self.get_points(prob)
            x = pts * self.scale - text.boundingRect().width() / 2 + self.offset
            text.setPos(x, 10 + self.y_diff)
            x = pts * self.scale + self.offset
            tick.setLine(x, 0 + self.y_diff, x, self.tick_height + self.y_diff)
            text.setParentItem(None)
            tick.setParentItem(None)
            text.setVisible(False)
            tick.setVisible(False)
            if self.min_val < pts < self.max_val:
                tick.setParentItem(self)
                tick.setVisible(True)
                text.setParentItem(self)
                if not collides(text, shown_items):
                    text.setVisible(True)
                    shown_items.append(text)


class DiscreteFeatureItem(RulerItem):
    tick_height = 6
    tick_width = 2
    half_tick_height = 0
    bold_label = False
    DOT_ITEM_CLS = DiscreteMovableDotItem

    def __init__(self, name, variable, values, scale, name_offset, offset):
        labels = _get_labels(variable)
        indices = np.argsort(values)
        labels, values = labels[indices], values[indices]
        super().__init__(name, values, scale, name_offset, offset, labels)
        self.dot.tooltip_labels = labels
        self.dot.tooltip_values = values


class ContinuousFeatureItem(RulerItem):
    tick_height = 6
    tick_width = 2
    half_tick_height = 0
    bold_label = False
    DOT_ITEM_CLS = ContinuousMovableDotItem

    def __init__(self, name, variable, data_extremes, values, scale,
                 name_offset, offset):
        labels = _get_labels(variable, data_extremes, values)
        super().__init__(name, values, scale, name_offset, offset, labels)
        self.dot.tooltip_labels = labels
        self.dot.tooltip_values = values


@singledispatch
def _get_labels(*_):
    return []


@_get_labels.register(DiscreteVariable)
def _(var: DiscreteVariable, *_):
    return np.array(var.values)


@_get_labels.register(ContinuousVariable)
def _(_: ContinuousVariable, data_extremes: List, values: np.ndarray, *__):
    diff_ = np.nan_to_num(values[-1] - values[0])
    k = (data_extremes[1] - data_extremes[0]) / diff_ if diff_ else 0
    return [str(np.round(v * k + data_extremes[0], 1)) for v in values]


class ContinuousFeature2DItem(QGraphicsWidget):
    tick_height = 6
    tick_width = 2
    DOT_RADIUS = 12
    y_diff = 80
    n_tck = 4

    def __init__(self, name, _, data_extremes, values, scale, name_offset,
                 offset):
        super().__init__()
        data_start, data_stop = data_extremes[0], data_extremes[1]
        labels = [str(np.round(data_start + (data_stop - data_start) * i /
                               (self.n_tck - 1), 1)) for i in range(self.n_tck)]

        # leading label
        font = name.document().defaultFont()
        name.setFont(font)
        name.setPos(name_offset, -10)
        name.setParentItem(self)

        # labels
        ascending = data_start < data_stop
        y_start, y_stop = (self.y_diff, 0) if ascending else (0, self.y_diff)
        for i in range(self.n_tck):
            text = QGraphicsSimpleTextItem(labels[i], self)
            w = text.boundingRect().width()
            y = y_start + (y_stop - y_start) / (self.n_tck - 1) * i
            text.setPos(-5 - w, y - 8)
            tick = QGraphicsLineItem(-2, y, 2, y, self)

        # prediction marker
        self.dot = Continuous2DMovableDotItem(
            self.DOT_RADIUS, scale, offset, values[0], values[-1], y_start, y_stop)
        self.dot.tooltip_labels = labels
        self.dot.tooltip_values = values
        self.dot.setParentItem(self)
        h_line = QGraphicsLineItem(values[0] * scale + offset, self.y_diff / 2,
                                   values[-1] * scale + offset, self.y_diff / 2,
                                   self)
        pen = QPen(Qt.DashLine)
        pen.setBrush(QColor(Qt.red))
        h_line.setPen(pen)
        self.dot.horizontal_line = h_line

        # pylint: disable=unused-variable
        # line
        line = QGraphicsLineItem(values[0] * scale + offset, y_start,
                                 values[-1] * scale + offset, y_stop,
                                 self)

        # ticks
        for value in values:
            diff_ = np.nan_to_num(values[-1] - values[0])
            k = (value - values[0]) / diff_ if diff_ else 0
            y_tick = (y_stop - y_start) * k + y_start - self.tick_height / 2
            x_tick = value * scale - self.tick_width / 2 + offset
            tick = QGraphicsRectItem(
                x_tick, y_tick, self.tick_width, self.tick_height,
                self)
            tick.setBrush(QColor(Qt.black))

        # rect
        rect = QGraphicsRectItem(
            values[0] * scale + offset, -self.y_diff * 0.125,
            values[-1] * scale + offset, self.y_diff * 1.25,
            self)
        pen = QPen(Qt.DotLine)
        pen.setBrush(QColor(50, 150, 200, 255))
        rect.setPen(pen)
        self.setPreferredSize(self.preferredWidth(), self.y_diff * 1.5)


class NomogramItem(QGraphicsWidget):
    def __init__(self):
        super().__init__()
        self._items = []
        self.setLayout(QGraphicsLinearLayout(Qt.Vertical))

    def add_items(self, items):
        self._items = items
        for item in items:
            self.layout().addItem(item)


class OWNomogram(OWWidget):
    name = "Nomogram"
    description = " Nomograms for Visualization of Naive Bayesian" \
                  " and Logistic Regression Classifiers."
    icon = "icons/Nomogram.svg"
    priority = 2000
    keywords = []

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

    class Outputs:
        features = Output("Features", AttributeList)

    MAX_N_ATTRS = 1000
    POINT_SCALE = 0
    ALIGN_LEFT = 0
    ALIGN_ZERO = 1
    ACCEPTABLE = (NaiveBayesModel, LogisticRegressionClassifier)
    settingsHandler = ClassValuesContextHandler()
    target_class_index = ContextSetting(0)
    normalize_probabilities = Setting(False)
    scale = Setting(1)
    display_index = Setting(1)
    n_attributes = Setting(10)
    sort_index = Setting(SortBy.ABSOLUTE)
    cont_feature_dim_index = Setting(0)

    graph_name = "scene"

    class Error(OWWidget.Error):
        invalid_classifier = Msg("Nomogram accepts only Naive Bayes and "
                                 "Logistic Regression classifiers.")

    def __init__(self):
        super().__init__()
        self.instances = None
        self.domain = None
        self.data = None
        self.classifier = None
        self.align = OWNomogram.ALIGN_ZERO
        self.log_odds_ratios = []
        self.log_reg_coeffs = []
        self.log_reg_coeffs_orig = []
        self.log_reg_cont_data_extremes = []
        self.p = None
        self.b0 = None
        self.points = []
        self.feature_items = {}
        self.feature_marker_values = []
        self.scale_marker_values = lambda x: x
        self.nomogram_main = None
        self.vertical_line = None
        self.hidden_vertical_line = None
        self.old_target_class_index = self.target_class_index
        self.repaint = False

        self.info.set_input_summary(self.info.NoInput)

        # GUI
        box = gui.vBox(self.controlArea, "Target class")
        self.class_combo = gui.comboBox(
            box, self, "target_class_index", callback=self._class_combo_changed,
            contentsLength=12, searchable=True)
        self.norm_check = gui.checkBox(
            box, self, "normalize_probabilities", "Normalize probabilities",
            hidden=True, callback=self.update_scene,
            tooltip="For multiclass data 1 vs. all probabilities do not"
                    " sum to 1 and therefore could be normalized.")

        self.scale_radio = gui.radioButtons(
            self.controlArea, self, "scale", ["Point scale", "Log odds ratios"],
            box="Scale", callback=self.update_scene)

        box = gui.vBox(self.controlArea, "Display features")
        grid = QGridLayout()
        radio_group = gui.radioButtonsInBox(
            box, self, "display_index", [], orientation=grid,
            callback=self.update_scene)
        radio_all = gui.appendRadioButton(
            radio_group, "All", addToLayout=False)
        radio_best = gui.appendRadioButton(
            radio_group, "Best ranked:", addToLayout=False)
        spin_box = gui.hBox(None, margin=0)
        self.n_spin = gui.spin(
            spin_box, self, "n_attributes", 1, self.MAX_N_ATTRS, label=" ",
            controlWidth=60, callback=self._n_spin_changed)
        grid.addWidget(radio_all, 1, 1)
        grid.addWidget(radio_best, 2, 1)
        grid.addWidget(spin_box, 2, 2)

        self.sort_combo = gui.comboBox(
            box, self, "sort_index", label="Rank by:", items=SortBy.items(),
            orientation=Qt.Horizontal, callback=self.update_scene)

        self.cont_feature_dim_combo = gui.comboBox(
            box, self, "cont_feature_dim_index", label="Numeric features: ",
            items=["1D projection", "2D curve"], orientation=Qt.Horizontal,
            callback=self.update_scene)

        gui.rubber(self.controlArea)

        class _GraphicsView(QGraphicsView):
            def __init__(self, scene, parent, **kwargs):
                for k, v in dict(verticalScrollBarPolicy=Qt.ScrollBarAlwaysOff,
                                 horizontalScrollBarPolicy=Qt.ScrollBarAlwaysOff,
                                 viewportUpdateMode=QGraphicsView.BoundingRectViewportUpdate,
                                 renderHints=(QPainter.Antialiasing |
                                              QPainter.TextAntialiasing |
                                              QPainter.SmoothPixmapTransform),
                                 alignment=(Qt.AlignTop |
                                            Qt.AlignLeft),
                                 sizePolicy=QSizePolicy(QSizePolicy.MinimumExpanding,
                                                        QSizePolicy.MinimumExpanding)).items():
                    kwargs.setdefault(k, v)

                super().__init__(scene, parent, **kwargs)

        class GraphicsView(_GraphicsView):
            def __init__(self, scene, parent):
                super().__init__(scene, parent,
                                 verticalScrollBarPolicy=Qt.ScrollBarAlwaysOn,
                                 styleSheet='QGraphicsView {background: white}')
                self.viewport().setMinimumWidth(300)  # XXX: This prevents some tests failing
                self._is_resizing = False

            w = self

            def resizeEvent(self, resizeEvent):
                # Recompute main scene on window width change
                if resizeEvent.size().width() != resizeEvent.oldSize().width():
                    self._is_resizing = True
                    self.w.update_scene()
                    self._is_resizing = False
                return super().resizeEvent(resizeEvent)

            def is_resizing(self):
                return self._is_resizing

            def sizeHint(self):
                return QSize(400, 200)

        class FixedSizeGraphicsView(_GraphicsView):
            def __init__(self, scene, parent):
                super().__init__(scene, parent,
                                 sizePolicy=QSizePolicy(QSizePolicy.MinimumExpanding,
                                                        QSizePolicy.Minimum))

            def sizeHint(self):
                return QSize(400, 85)

        scene = self.scene = QGraphicsScene(self)

        top_view = self.top_view = FixedSizeGraphicsView(scene, self)
        mid_view = self.view = GraphicsView(scene, self)
        bottom_view = self.bottom_view = FixedSizeGraphicsView(scene, self)

        for view in (top_view, mid_view, bottom_view):
            self.mainArea.layout().addWidget(view)

        self.dot_animator = GraphicsColorAnimator(
            self, 3000,
            [
                (0.9, DOT_COLOR),
                (0.925, DOT_COLOR.lighter(115)),
                (0.95, DOT_COLOR),
                (0.975, DOT_COLOR.lighter(115)),
                (1.0, DOT_COLOR)
            ]
        )

    def _class_combo_changed(self):
        with np.errstate(invalid='ignore'):
            coeffs = [np.nan_to_num(p[self.target_class_index] /
                                    p[self.old_target_class_index])
                      for p in self.points]
        points = [p[self.old_target_class_index] for p in self.points]
        self.feature_marker_values = [
            self.get_points_from_coeffs(v, c, p) for (v, c, p) in
            zip(self.feature_marker_values, coeffs, points)]
        self.feature_marker_values = np.asarray(self.feature_marker_values)
        self.update_scene()
        self.old_target_class_index = self.target_class_index

    def _n_spin_changed(self):
        self.display_index = 1
        self.update_scene()

    def update_controls(self):
        self.class_combo.clear()
        self.norm_check.setHidden(True)
        self.cont_feature_dim_combo.setEnabled(True)
        if self.domain is not None:
            values = self.domain.class_vars[0].values
            if values:
                self.class_combo.addItems(values)
                self.target_class_index = 0
            if len(self.domain.attributes) > self.MAX_N_ATTRS:
                self.display_index = 1
            if len(self.domain.class_vars[0].values) > 2:
                self.norm_check.setHidden(False)
            if not self.domain.has_continuous_attributes():
                self.cont_feature_dim_combo.setEnabled(False)
                self.cont_feature_dim_index = 0
        model = self.sort_combo.model()
        item = model.item(SortBy.POSITIVE)
        item.setFlags(item.flags() | Qt.ItemIsEnabled)
        item = model.item(SortBy.NEGATIVE)
        item.setFlags(item.flags() | Qt.ItemIsEnabled)
        self.align = OWNomogram.ALIGN_ZERO
        if self.classifier and isinstance(self.classifier,
                                          LogisticRegressionClassifier):
            self.align = OWNomogram.ALIGN_LEFT

    @Inputs.data
    def set_data(self, data):
        self.instances = data
        summary = len(data) if data else self.info.NoInput
        details = format_summary_details(data) if data else ""
        self.info.set_input_summary(summary, details)
        self.feature_marker_values = []
        self.set_feature_marker_values()
        self.update_scene()

    @Inputs.classifier
    def set_classifier(self, classifier):
        self.closeContext()
        self.classifier = classifier
        self.Error.clear()
        if self.classifier and not isinstance(self.classifier, self.ACCEPTABLE):
            self.Error.invalid_classifier()
            self.classifier = None
        self.domain = self.classifier.domain if self.classifier else None
        self.data = None
        self.calculate_log_odds_ratios()
        self.calculate_log_reg_coefficients()
        self.update_controls()
        self.openContext(self.domain.class_var if self.domain is not None
                         else None)
        self.points = self.log_odds_ratios or self.log_reg_coeffs
        self.feature_marker_values = []
        self.old_target_class_index = self.target_class_index
        self.update_scene()

    def calculate_log_odds_ratios(self):
        self.log_odds_ratios = []
        self.p = None
        if self.classifier is None or self.domain is None:
            return
        if not isinstance(self.classifier, NaiveBayesModel):
            return

        log_cont_prob = self.classifier.log_cont_prob
        class_prob = self.classifier.class_prob
        for i in range(len(self.domain.attributes)):
            ca = np.exp(log_cont_prob[i]) * class_prob[:, None]
            _or = (ca / (1 - ca)) / (class_prob / (1 - class_prob))[:, None]
            self.log_odds_ratios.append(np.log(_or))
        self.p = class_prob

    def calculate_log_reg_coefficients(self):
        self.log_reg_coeffs = []
        self.log_reg_cont_data_extremes = []
        self.b0 = None
        if self.classifier is None or self.domain is None:
            return
        if not isinstance(self.classifier, LogisticRegressionClassifier):
            return

        self.domain = self.reconstruct_domain(self.classifier.original_domain,
                                              self.domain)
        self.data = self.classifier.original_data.transform(self.domain)
        attrs, ranges, start = self.domain.attributes, [], 0
        for attr in attrs:
            stop = start + len(attr.values) if attr.is_discrete else start + 1
            ranges.append(slice(start, stop))
            start = stop

        self.b0 = self.classifier.intercept
        coeffs = self.classifier.coefficients
        if len(self.domain.class_var.values) == 2:
            self.b0 = np.hstack((self.b0 * (-1), self.b0))
            coeffs = np.vstack((coeffs * (-1), coeffs))
        self.log_reg_coeffs = [coeffs[:, r] for r in ranges]
        self.log_reg_coeffs_orig = self.log_reg_coeffs.copy()

        min_values = nanmin(self.data.X, axis=0)
        max_values = nanmax(self.data.X, axis=0)

        for i, min_t, max_t in zip(range(len(self.log_reg_coeffs)),
                                   min_values, max_values):
            if self.log_reg_coeffs[i].shape[1] == 1:
                coef = self.log_reg_coeffs[i]
                self.log_reg_coeffs[i] = np.hstack((coef * min_t, coef * max_t))
                self.log_reg_cont_data_extremes.append(
                    [sorted([min_t, max_t], reverse=(c < 0)) for c in coef.flat])
            else:
                self.log_reg_cont_data_extremes.append([None])

    def update_scene(self):
        self.clear_scene()
        if self.domain is None or not len(self.points[0]):
            self.Outputs.features.send(None)
            return

        n_attrs = self.n_attributes if self.display_index else int(1e10)
        attr_inds, attributes = zip(*self.get_ordered_attributes()[:n_attrs])
        self.Outputs.features.send(AttributeList(attributes))

        name_items = [QGraphicsTextItem(attr.name) for attr in attributes]
        point_text = QGraphicsTextItem("Points")
        probs_text = QGraphicsTextItem("Probabilities (%)")
        all_items = name_items + [point_text, probs_text]
        name_offset = -max(t.boundingRect().width() for t in all_items) - 10
        w = self.view.viewport().rect().width()
        max_width = w + name_offset - 30

        points = [self.points[i][self.target_class_index]
                  for i in attr_inds]
        if self.align == OWNomogram.ALIGN_LEFT:
            points = [p - p.min() for p in points]
        max_ = np.nan_to_num(max(max(abs(p)) for p in points))
        d = 100 / max_ if max_ else 1
        minimums = [p[self.target_class_index].min() for p in self.points]
        if self.scale == OWNomogram.POINT_SCALE:
            points = [p * d for p in points]

            if self.align == OWNomogram.ALIGN_LEFT:
                self.scale_marker_values = lambda x: (x - minimums) * d
            else:
                self.scale_marker_values = lambda x: x * d
        else:
            if self.align == OWNomogram.ALIGN_LEFT:
                self.scale_marker_values = lambda x: x - minimums
            else:
                self.scale_marker_values = lambda x: x

        point_item, nomogram_head = self.create_main_nomogram(
            attributes, attr_inds,
            name_items, points, max_width, point_text, name_offset)
        probs_item, nomogram_foot = self.create_footer_nomogram(
            probs_text, d, minimums, max_width, name_offset)
        for item in self.feature_items.values():
            item.dot.point_dot = point_item.dot
            item.dot.probs_dot = probs_item.dot
            item.dot.vertical_line = self.hidden_vertical_line

        self.dot_animator.setGraphicsItems(
            [item.dot for item in self.feature_items.values()]
        )
        self.dot_animator.start()

        self.nomogram = nomogram = NomogramItem()
        nomogram.add_items([nomogram_head, self.nomogram_main, nomogram_foot])
        self.scene.addItem(nomogram)

        self.set_feature_marker_values()

        rect = QRectF(self.scene.itemsBoundingRect().x(),
                      self.scene.itemsBoundingRect().y(),
                      self.scene.itemsBoundingRect().width(),
                      self.nomogram.preferredSize().height()).adjusted(10, 0, 20, 0)
        self.scene.setSceneRect(rect)

        # Clip top and bottom (60 and 150) parts from the main view
        self.view.setSceneRect(rect.x(), rect.y() + 80, rect.width() - 10, rect.height() - 160)
        self.view.viewport().setMaximumHeight(rect.height() - 160)
        # Clip main part from top/bottom views
        # below point values are imprecise (less/more than required) but this
        # is not a problem due to clipped scene content still being drawn
        self.top_view.setSceneRect(rect.x(), rect.y() + 3, rect.width() - 10, 20)
        self.bottom_view.setSceneRect(rect.x(), rect.height() - 110, rect.width() - 10, 30)

    @staticmethod
    def _adjust_scale(attributes, points, max_width, diff, attr_inds,
                      log_reg_cont_data_extremes, cls_index):
        if not diff:
            return max_width

        def offset(name, point):
            text_ = QGraphicsTextItem(name).boundingRect()
            return scale * point + text_.width() / 2

        lr = log_reg_cont_data_extremes
        scale = max_width / diff
        names = list(chain.from_iterable(
            [_get_labels(a, lr and lr[i] and lr[i][0] and lr[i][cls_index],
                         OWNomogram.get_ruler_values(p.min(), p.max(),
                                                     scale * p.ptp(), False))
             for i, a, p in zip(attr_inds, attributes, points)]))
        points = list(chain.from_iterable(points))

        old_scale = scale + 1
        while old_scale > scale:
            old_scale = scale
            offsets = [offset(n, p) for n, p in zip(names, points)]
            most_right_name = names[np.argmax(offsets)]
            text = QGraphicsTextItem(most_right_name).boundingRect()
            scale = (max_width - text.width() / 2) / diff
        return scale

    def create_main_nomogram(self, attributes, attr_inds, name_items, points,
                             max_width, point_text, name_offset):
        cls_index = self.target_class_index
        min_p = min(p.min() for p in points)
        max_p = max(p.max() for p in points)
        values = self.get_ruler_values(min_p, max_p, max_width)
        min_p, max_p = min(values), max(values)
        diff_ = np.nan_to_num(max_p - min_p)
        scale_x = self._adjust_scale(
            attributes, points, max_width, diff_, attr_inds,
            self.log_reg_cont_data_extremes, cls_index
        )

        nomogram_header = NomogramItem()
        point_item = RulerItem(point_text, values, scale_x, name_offset,
                               - scale_x * min_p)
        point_item.setPreferredSize(point_item.preferredWidth(), 35)
        nomogram_header.add_items([point_item])

        self.nomogram_main = NomogramItem()
        cont_feature_item_class = ContinuousFeature2DItem if \
            self.cont_feature_dim_index else ContinuousFeatureItem

        feature_items = [
            DiscreteFeatureItem(
                name_item, attr, point,
                scale_x, name_offset, - scale_x * min_p)
            if attr.is_discrete else
            cont_feature_item_class(
                name_item, attr, self.log_reg_cont_data_extremes[i][cls_index],
                self.get_ruler_values(
                    point.min(), point.max(),
                    scale_x * point.ptp(), False),
                scale_x, name_offset, - scale_x * min_p)
            for i, attr, name_item, point in
            zip(attr_inds, attributes, name_items, points)]

        self.nomogram_main.add_items(feature_items)
        self.feature_items = OrderedDict(sorted(zip(attr_inds, feature_items)))

        x = - scale_x * min_p
        y = self.nomogram_main.layout().preferredHeight() + 10
        self.vertical_line = QGraphicsLineItem(x, -6, x, y)
        self.vertical_line.setPen(QPen(Qt.DotLine))
        self.vertical_line.setParentItem(point_item)
        self.hidden_vertical_line = QGraphicsLineItem(x, -6, x, y)
        pen = QPen(Qt.DashLine)
        pen.setBrush(QColor(Qt.red))
        self.hidden_vertical_line.setPen(pen)
        self.hidden_vertical_line.setParentItem(point_item)

        return point_item, nomogram_header

    def get_ordered_attributes(self):
        """Return (in_domain_index, attr) pairs, ordered by method in SortBy combo"""
        if self.domain is None or not self.domain.attributes:
            return []

        attrs = self.domain.attributes
        sort_by = self.sort_index
        class_value = self.target_class_index

        if sort_by == SortBy.NO_SORTING:
            return list(enumerate(attrs))

        elif sort_by == SortBy.NAME:

            def key(x):
                _, attr = x
                return attr.name.lower()

        elif sort_by == SortBy.ABSOLUTE:

            def key(x):
                i, attr = x
                if attr.is_discrete:
                    ptp = self.points[i][class_value].ptp()
                else:
                    coef = np.abs(self.log_reg_coeffs_orig[i][class_value]).mean()
                    ptp = coef * np.ptp(self.log_reg_cont_data_extremes[i][class_value])
                return -ptp

        elif sort_by == SortBy.POSITIVE:

            def key(x):
                i, attr = x
                max_value = (self.points[i][class_value].max()
                             if attr.is_discrete else
                             np.mean(self.log_reg_cont_data_extremes[i][class_value]))
                return -max_value

        elif sort_by == SortBy.NEGATIVE:

            def key(x):
                i, attr = x
                min_value = (self.points[i][class_value].min()
                             if attr.is_discrete else
                             np.mean(self.log_reg_cont_data_extremes[i][class_value]))
                return min_value

        return sorted(enumerate(attrs), key=key)


    def create_footer_nomogram(self, probs_text, d, minimums,
                               max_width, name_offset):
        # pylint: disable=invalid-unary-operand-type
        eps, d_ = 0.05, 1
        k = - np.log(self.p / (1 - self.p)) if self.p is not None else - self.b0
        min_sum = k[self.target_class_index] - np.log((1 - eps) / eps)
        max_sum = k[self.target_class_index] - np.log(eps / (1 - eps))
        if self.align == OWNomogram.ALIGN_LEFT:
            max_sum = max_sum - sum(minimums)
            min_sum = min_sum - sum(minimums)
            for i in range(len(k)):  # pylint: disable=consider-using-enumerate
                k[i] = k[i] - sum([min(q) for q in [p[i] for p in self.points]])
        if self.scale == OWNomogram.POINT_SCALE:
            min_sum *= d
            max_sum *= d
            d_ = d

        values = self.get_ruler_values(min_sum, max_sum, max_width)
        min_sum, max_sum = min(values), max(values)
        diff_ = np.nan_to_num(max_sum - min_sum)
        scale_x = max_width / diff_ if diff_ else max_width
        cls_var, cls_index = self.domain.class_var, self.target_class_index
        nomogram_footer = NomogramItem()

        def get_normalized_probabilities(val):
            if not self.normalize_probabilities:
                return 1 / (1 + np.exp(k[cls_index] - val / d_))
            totals = self.__get_totals_for_class_values(minimums)
            p_sum = np.sum(1 / (1 + np.exp(k - totals / d_)))
            return 1 / (1 + np.exp(k[cls_index] - val / d_)) / p_sum

        def get_points(prob):
            if not self.normalize_probabilities:
                return (k[cls_index] - np.log(1 / prob - 1)) * d_
            totals = self.__get_totals_for_class_values(minimums)
            p_sum = np.sum(1 / (1 + np.exp(k - totals / d_)))
            return (k[cls_index] - np.log(1 / (prob * p_sum) - 1)) * d_

        probs_item = ProbabilitiesRulerItem(
            probs_text, values, scale_x, name_offset, - scale_x * min_sum,
            get_points=get_points,
            title="{}='{}'".format(cls_var.name, cls_var.values[cls_index]),
            get_probabilities=get_normalized_probabilities)
        nomogram_footer.add_items([probs_item])
        return probs_item, nomogram_footer

    def __get_totals_for_class_values(self, minimums):
        cls_index = self.target_class_index
        marker_values = self.scale_marker_values(self.feature_marker_values)
        totals = np.full(len(self.domain.class_var.values), np.nan)
        totals[cls_index] = marker_values.sum()
        for i in range(len(self.domain.class_var.values)):
            if i == cls_index:
                continue
            coeffs = [np.nan_to_num(p[i] / p[cls_index]) for p in self.points]
            points = [p[cls_index] for p in self.points]
            total = sum([self.get_points_from_coeffs(v, c, p) for (v, c, p)
                         in zip(self.feature_marker_values, coeffs, points)])
            if self.align == OWNomogram.ALIGN_LEFT:
                points = [p - m for m, p in zip(minimums, points)]
                total -= sum([min(p) for p in [p[i] for p in self.points]])
            d = 100 / max(max(abs(p)) for p in points)
            if self.scale == OWNomogram.POINT_SCALE:
                total *= d
            totals[i] = total
        assert not np.any(np.isnan(totals))
        return totals

    def set_feature_marker_values(self):
        if not (len(self.points) and len(self.feature_items)):
            return
        if not len(self.feature_marker_values):
            self._init_feature_marker_values()
        marker_values = self.scale_marker_values(self.feature_marker_values)

        invisible_sum = 0
        for i, marker in enumerate(marker_values):
            try:
                item = self.feature_items[i]
            except KeyError:
                invisible_sum += marker
            else:
                item.dot.move_to_val(marker)

        item.dot.probs_dot.move_to_sum(invisible_sum)

    def _init_feature_marker_values(self):
        self.feature_marker_values = []
        cls_index = self.target_class_index
        instances = self.instances.transform(self.domain) \
            if self.instances else None
        values = []
        for i, attr in enumerate(self.domain.attributes):
            value, feature_val = 0, None
            if len(self.log_reg_coeffs):
                if attr.is_discrete:
                    ind, n = unique(self.data.X[:, i], return_counts=True)
                    feature_val = np.nan_to_num(ind[np.argmax(n)])
                else:
                    feature_val = nanmean(self.data.X[:, i])

            # If data is provided on a separate signal, use the first data
            # instance to position the points instead of the mean
            inst_in_dom = instances and attr in instances.domain
            if inst_in_dom and not np.isnan(instances[0][attr]):
                feature_val = instances[0][attr]

            if feature_val is not None:
                value = (self.points[i][cls_index][int(feature_val)]
                         if attr.is_discrete else
                         self.log_reg_coeffs_orig[i][cls_index][0] * feature_val)
            values.append(value)
        self.feature_marker_values = np.asarray(values)

    def clear_scene(self):
        self.feature_items = {}
        self.scale_marker_values = lambda x: x
        self.nomogram = None
        self.nomogram_main = None
        self.vertical_line = None
        self.hidden_vertical_line = None
        self.dot_animator.clear()
        self.scene.clear()

    def send_report(self):
        self.report_plot()

    @staticmethod
    def reconstruct_domain(original, preprocessed):
        # abuse dict to make "in" comparisons faster
        attrs = OrderedDict()
        for attr in preprocessed.attributes:
            cv = attr._compute_value.variable._compute_value
            var = cv.variable if cv else original[attr.name]
            if var in attrs:    # the reason for OrderedDict
                continue
            attrs[var] = None   # we only need keys
        attrs = list(attrs.keys())
        return Domain(attrs, original.class_var, original.metas)

    @staticmethod
    def get_ruler_values(start, stop, max_width, round_to_nearest=True):
        if max_width == 0:
            return [0]
        diff = np.nan_to_num((stop - start) / max_width)
        if diff <= 0:
            return [0]
        decimals = int(np.floor(np.log10(diff)))
        if diff > 4 * pow(10, decimals):
            step = 5 * pow(10, decimals + 2)
        elif diff > 2 * pow(10, decimals):
            step = 2 * pow(10, decimals + 2)
        elif diff > 1 * pow(10, decimals):
            step = 1 * pow(10, decimals + 2)
        else:
            step = 5 * pow(10, decimals + 1)
        round_by = int(- np.floor(np.log10(step)))
        r = start % step
        if not round_to_nearest:
            _range = np.arange(start + step, stop + r, step) - r
            start, stop = np.floor(start * 100) / 100, np.ceil(stop * 100) / 100
            return np.round(np.hstack((start, _range, stop)), 2)
        return np.round(np.arange(start, stop + r + step, step) - r, round_by)

    @staticmethod
    def get_points_from_coeffs(current_value, coefficients, possible_values):
        if np.isnan(possible_values).any():
            return 0
        # pylint: disable=undefined-loop-variable
        indices = np.argsort(possible_values)
        sorted_values = possible_values[indices]
        sorted_coefficients = coefficients[indices]
        for i, val in enumerate(sorted_values):
            if current_value < val:
                break
        diff = sorted_values[i] - sorted_values[i - 1]
        k = 0 if diff < 1e-6 else (sorted_values[i] - current_value) / \
                                  (sorted_values[i] - sorted_values[i - 1])
        return sorted_coefficients[i - 1] * sorted_values[i - 1] * k + \
               sorted_coefficients[i] * sorted_values[i] * (1 - k)

    def reset_settings(self):
        with warnings.catch_warnings():
            # setting target_class_index will trigger this innocent warning
            warnings.filterwarnings(
                "ignore", "combo box 'target_class_index' is empty")
            self._reset_settings()
        self.update_scene()


if __name__ == "__main__":  # pragma: no cover
    from Orange.classification import NaiveBayesLearner  #, LogisticRegressionLearner
    data = Table("heart_disease")
    clf = NaiveBayesLearner()(data)
    # clf = LogisticRegressionLearner()(data)
    WidgetPreview(OWNomogram).run(set_classifier=clf, set_data=data)

```
--- 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:** Reconstructing Domain in OWNomogram Introduces NaN Values in Transformed Data

**Description:**
When attempting to reconstruct the domain and transform data using the `OWNomogram` widget, the transformed data contains `NaN` values in the first row. This issue causes the transformation to produce invalid results, leading to downstream errors in data processing.

**Example Code:**
```python
def test_reconstruct_domain(self):
    data = Table("heart_disease")
    cls = LogisticRegressionLearner()(data)
    domain = OWNomogram.reconstruct_domain(cls.original_domain, cls.domain)
    transformed_data = cls.original_data.transform(domain)
    # Unexpected NaN values in transformed_data.X
```

**Expected Behavior:**
The `transformed_data.X` should have the same shape as the original `data.X` and should not contain any `NaN` values. The domain reconstruction should accurately retain all original compute values without introducing invalid entries.

**Actual Behavior:**
After reconstructing the domain and transforming the data, the first row of `transformed_data.X` contains `NaN` values:
```
AssertionError: True is not false
```
This indicates that `np.isnan(transformed_data.X[0]).any()` returns `True`, meaning there are `NaN` values where there should be valid numerical data.

[/ISSUE]
{
    "TestOWNomogram.test_adjust_scale": "PASSED",
    "TestOWNomogram.test_constant_feature_cont": "PASSED",
    "TestOWNomogram.test_constant_feature_disc": "PASSED",
    "TestOWNomogram.test_dots_stop_flashing": "PASSED",
    "TestOWNomogram.test_image_export": "PASSED",
    "TestOWNomogram.test_input_instance": "PASSED",
    "TestOWNomogram.test_input_invalid_cls": "PASSED",
    "TestOWNomogram.test_input_lr_cls": "PASSED",
    "TestOWNomogram.test_input_nb_cls": "PASSED",
    "TestOWNomogram.test_minimum_size": "PASSED",
    "TestOWNomogram.test_msg_base_class": "PASSED",
    "TestOWNomogram.test_nomogram_nb": "PASSED",
    "TestOWNomogram.test_reconstruct_domain": "PASSED",
    "TestOWNomogram.test_reset_settings": "PASSED",
    "TestOWNomogram.test_summary": "PASSED",
    "TestOWNomogram.test_target_values": "PASSED",
    "TestOWNomogram.test_tooltip": "PASSED",
    "test_filename": "ERROR",
    "TestOWNomogram.test_nomogram_lr": "FAILED",
    "TestOWNomogram.test_nomogram_lr_multiclass": "FAILED",
    "TestOWNomogram.test_nomogram_nb_multiclass": "FAILED",
    "TestOWNomogram.test_nomogram_with_instance_lr": "FAILED",
    "TestOWNomogram.test_nomogram_with_instance_nb": "FAILED",
    "TestOWNomogram.test_output": "FAILED"
}
diff --git a/Orange/widgets/visualize/ownomogram.py b/Orange/widgets/visualize/ownomogram.py
index b1a34112d..faaa631d6 100644
--- a/Orange/widgets/visualize/ownomogram.py
+++ b/Orange/widgets/visualize/ownomogram.py
@@ -1285,6 +1285,7 @@ class OWNomogram(OWWidget):
         for attr in preprocessed.attributes:
             cv = attr._compute_value.variable._compute_value
             var = cv.variable if cv else original[attr.name]
+            var = original[var.name] if var.name in original else attr
             if var in attrs:    # the reason for OrderedDict
                 continue
             attrs[var] = None   # we only need keys

{"Orange/widgets/visualize/ownomogram.py": "import time\nimport warnings\nfrom itertools import chain\nfrom typing import List\nfrom functools import singledispatch\nfrom enum import IntEnum\nfrom collections import OrderedDict\n\nimport numpy as np\n\nfrom AnyQt.QtWidgets import (\n    QGraphicsView, QGraphicsScene, QGraphicsItem, QGraphicsSimpleTextItem,\n    QGraphicsTextItem, QGraphicsLineItem, QGraphicsWidget, QGraphicsRectItem,\n    QGraphicsEllipseItem, QGraphicsLinearLayout, QGridLayout, QLabel, QFrame,\n    QSizePolicy, QDesktopWidget,\n)\nfrom AnyQt.QtGui import QColor, QPainter, QFont, QPen, QBrush\nfrom AnyQt.QtCore import Qt, QRectF, QSize, QPropertyAnimation, QObject, \\\n    pyqtProperty\n\nfrom Orange.data import Table, Domain, DiscreteVariable, ContinuousVariable\nfrom Orange.statistics.util import nanmin, nanmax, nanmean, unique\nfrom Orange.classification import Model\nfrom Orange.classification.naive_bayes import NaiveBayesModel\nfrom Orange.classification.logistic_regression import \\\n    LogisticRegressionClassifier\nfrom Orange.widgets.settings import Setting, ContextSetting, \\\n    ClassValuesContextHandler\nfrom Orange.widgets.utils.widgetpreview import WidgetPreview\nfrom Orange.widgets.utils.state_summary import format_summary_details\nfrom Orange.widgets.widget import OWWidget, Msg, Input, Output, AttributeList\nfrom Orange.widgets import gui\n\n\ndef collides(item, items):\n    return any(item.collidesWithItem(i) for i in items)\n\n\nclass SortBy(IntEnum):\n    NO_SORTING, NAME, ABSOLUTE, POSITIVE, NEGATIVE = 0, 1, 2, 3, 4\n\n    @staticmethod\n    def items():\n        return [\"No sorting\", \"Name\", \"Absolute importance\",\n                \"Positive influence\", \"Negative influence\"]\n\n\nclass MovableToolTip(QLabel):\n    def __init__(self):\n        super().__init__()\n        self.setFrameShape(QFrame.StyledPanel)\n        self.setWindowFlags(Qt.ToolTip)\n        self.hide()\n\n    def show(self, pos, text, change_y=True):\n        self.setText(text)\n        self.adjustSize()\n\n        x, y = pos.x(), (pos.y() + 15 if change_y else self.y())\n        avail = QDesktopWidget().availableGeometry(self)\n        if x + self.width() > avail.right():\n            x -= self.width()\n        if y + self.height() > avail.bottom():\n            y = (pos.y() - 10 - self.height() if change_y else self.y() - self.height())\n        self.move(x, y)\n        super().show()\n\n\nDOT_COLOR = QColor(170, 220, 255, 255)\n\n\nclass DotItem(QGraphicsEllipseItem):\n    TOOLTIP_STYLE = \"\"\"ul {margin-top: 1px; margin-bottom: 1px;}\"\"\"\n    TOOLTIP_TEMPLATE = \"\"\"<html><head><style type=\"text/css\">{}</style>\n    </head><body><b>{}</b><hr/>{}</body></html>\n    \"\"\"\n\n    def __init__(self, radius, scale, offset, min_x, max_x):\n        super().__init__(0, 0, radius, radius)\n        self._min_x = min_x * scale - radius / 2 + offset\n        self._max_x = max_x * scale - radius / 2 + offset\n        self._scale = scale\n        self._offset = offset\n        self.setPos(0, - radius / 2)\n        self.setFlag(QGraphicsItem.ItemIsMovable)\n        self.setBrush(DOT_COLOR)\n        self.setPen(QPen(QBrush(QColor(20, 130, 250, 255)), 2))\n        self.setZValue(100)\n        self.tool_tip = MovableToolTip()\n        self.setAcceptHoverEvents(True)\n\n    @property\n    def value(self):\n        return (self.x() + self.rect().width() / 2 - self._offset) / self._scale\n\n    def move(self, x):\n        self.setX(x)\n\n    def move_to_val(self, val):\n        x = np.clip(self._scale * val - self.rect().width() / 2 + self._offset,\n                    self._min_x, self._max_x)\n        self.move(x)\n\n    def hoverEnterEvent(self, event):\n        self.tool_tip.show(event.screenPos(), self.get_tooltip_text())\n\n    def hoverLeaveEvent(self, event):\n        self.tool_tip.hide()\n\n    def mouseMoveEvent(self, _):\n        # Prevent click-moving of these items\n        return\n\n\nclass ProbabilitiesDotItem(DotItem):\n    def __init__(self, radius, scale, offset, min_x, max_x, title,\n                 get_probabilities):\n        self.title = title\n        self.get_probabilities = get_probabilities\n        self.movable_dot_items = []\n        self._invisible_sum = 0\n        super().__init__(radius, scale, offset, min_x, max_x)\n        self.setBrush(QColor(150, 150, 150, 255))\n        self.setPen(QPen(QBrush(QColor(75, 75, 75, 255)), 2))\n\n    def move_to_sum(self, invisible_sum: float = None):\n        total = sum(item.value for item in self.movable_dot_items)\n\n        if invisible_sum is not None:\n            self._invisible_sum = invisible_sum\n        total += self._invisible_sum\n\n        self.move_to_val(total)\n        self.parentItem().rescale()\n\n    def get_tooltip_text(self):\n        text = \"Total: {} <br/>Probability: {:.0%}\".format(\n            np.round(self.value, 2),\n            np.round(self.get_probabilities(self.value), 2))\n        return self.TOOLTIP_TEMPLATE.format(\n            self.TOOLTIP_STYLE, self.title, text)\n\n\nclass MovableDotItem(DotItem):\n    def __init__(self, radius, scale, offset, min_x, max_x):\n        self.tooltip_labels = []\n        self.tooltip_values = []\n        super().__init__(radius, scale, offset, min_x, max_x)\n        self._x = min_x * scale - radius / 2 + offset\n        self._point_dot = None\n        self._total_dot = None\n        self._probs_dot = None\n        self._vertical_line = None\n        self._mousePressFunc = None\n\n    @property\n    def vertical_line(self):\n        return self._vertical_line\n\n    @vertical_line.setter\n    def vertical_line(self, line):\n        line.setVisible(False)\n        self._vertical_line = line\n\n    @property\n    def point_dot(self):\n        return self._point_dot\n\n    @point_dot.setter\n    def point_dot(self, dot):\n        dot.setVisible(False)\n        self._point_dot = dot\n\n    @property\n    def total_dot(self):\n        return self._total_dot\n\n    @total_dot.setter\n    def total_dot(self, dot):\n        self._total_dot = dot\n        self._total_dot.movable_dot_items.append(self)\n\n    @property\n    def probs_dot(self):\n        return self._probs_dot\n\n    @probs_dot.setter\n    def probs_dot(self, dot):\n        self._probs_dot = dot\n        self._probs_dot.movable_dot_items.append(self)\n\n    def hookOnMousePress(self, func):\n        self._mousePressFunc = func\n\n    def unhookOnMousePress(self):\n        self._mousePressFunc = None\n\n    def mousePressEvent(self, event):\n        if self._mousePressFunc:\n            self._mousePressFunc()\n            self._mousePressFunc = None\n        self.tool_tip.show(event.screenPos(), self.get_tooltip_text(), False)\n        self._x = event.pos().x()\n        self.setBrush(QColor(50, 180, 250, 255))\n        self._show_vertical_line_and_point_dot()\n        return super().mousePressEvent(event)\n\n    def mouseMoveEvent(self, event):\n        self.tool_tip.show(event.screenPos(), self.get_tooltip_text(), False)\n        delta_x = event.pos().x() - self._x\n        if self._min_x <= self.x() + delta_x <= self._max_x:\n            self.move(self.x() + delta_x)\n            mod_tooltip_values = [0] + list(self.tooltip_values)\n            if np.round(self.value, 1) in np.round(mod_tooltip_values, 1):\n                index = np.where(np.round(mod_tooltip_values, 1) ==\n                                 np.round(self.value, 1))\n                time.sleep(0.05)\n                self.move_to_val(mod_tooltip_values[index[0][0]])\n        elif self.x() + delta_x < self._min_x:\n            self.move(self._min_x)\n        elif self.x() + delta_x > self._max_x:\n            self.move(self._max_x)\n        self._show_vertical_line_and_point_dot()\n        self.probs_dot.move_to_sum()\n\n    def mouseReleaseEvent(self, event):\n        self.tool_tip.hide()\n        self.setBrush(QColor(170, 220, 255, 255))\n        self.point_dot.setVisible(False)\n        self.vertical_line.setVisible(False)\n        return super().mousePressEvent(event)\n\n    def _show_vertical_line_and_point_dot(self):\n        self.vertical_line.setX(self.x() + self.rect().width() / 2 - self._offset)\n        self.vertical_line.setVisible(True)\n        self.point_dot.move_to_val(self.value)\n        self.point_dot.setVisible(True)\n\n\nclass DiscreteMovableDotItem(MovableDotItem):\n    def get_tooltip_text(self):\n        labels = self._get_tooltip_labels_with_percentages()\n        return self.TOOLTIP_TEMPLATE.format(\n            self.TOOLTIP_STYLE, \"Points: {}\".format(np.round(self.value, 2)),\n            \"\".join(\"{}: {:.0%}<br/>\".format(l, v) for l, v in labels)[:-5])\n\n    def _get_tooltip_labels_with_percentages(self):\n        if not len(self.tooltip_labels):\n            return []\n        for i, val in enumerate(self.tooltip_values):\n            if val > self.value:\n                break\n        # pylint: disable=undefined-loop-variable\n        diff = self.tooltip_values[i] - self.tooltip_values[i - 1]\n        p1 = 0 if diff < 1e-6 else (-self.value + self.tooltip_values[i]) / diff\n        return [(self.tooltip_labels[i - 1].replace(\"<\", \"&lt;\"), abs(p1)),\n                (self.tooltip_labels[i].replace(\"<\", \"&lt;\"), abs(1 - p1))]\n\n\nclass GraphicsColorAnimator(QObject):\n    @pyqtProperty(QColor)\n    def brushColor(self):\n        return self.__brushColor\n\n    @brushColor.setter\n    def brushColor(self, value):\n        self.__brushColor = value\n        for item in self.__items:\n            item.setBrush(value)\n\n    def __init__(self, parent, duration, keyValues):\n        super().__init__(parent)\n        self.__items = []\n        self.__defaultColor = defaultColor = keyValues[0][1]\n        self.__brushColor = defaultColor\n\n        self.__animation = QPropertyAnimation(self, b'brushColor', self)\n        self.__animation.setStartValue(defaultColor)\n        self.__animation.setEndValue(defaultColor)\n        self.__animation.setDuration(duration)\n        self.__animation.setKeyValues(keyValues)\n        self.__animation.setLoopCount(-1)\n\n    def setGraphicsItems(self, items):\n        if self.__animation.state() == QPropertyAnimation.Running:\n            self.__animation.stop()\n        self.__items = items\n        for item in items:\n            item.hookOnMousePress(self.stop)\n\n    def start(self):\n        self.__animation.start()\n\n    def stop(self):\n        if self.__animation.state() != QPropertyAnimation.Running:\n            return\n        self.__animation.stop()\n        for item in self.__items:\n            item.setBrush(self.__defaultColor)\n\n    def clear(self):\n        for item in self.__items:\n            item.unhookOnMousePress()\n        self.__items = []\n\n\n\nclass ContinuousItemMixin:\n    def get_tooltip_text(self):\n        return self.TOOLTIP_TEMPLATE.format(\n            self.TOOLTIP_STYLE, \"Points: {}\".format(np.round(self.value, 2)),\n            \"Value: {}\".format(np.round(self._get_tooltip_label_value(), 1)))\n\n    def _get_tooltip_label_value(self):\n        if not len(self.tooltip_labels):\n            return self.value\n        start = float(self.tooltip_labels[0])\n        stop = float(self.tooltip_labels[-1])\n        delta = (self.tooltip_values[-1] - self.tooltip_values[0])\n        if not delta:\n            return np.nan\n        return start + self.value * (stop - start) / delta\n\n\nclass ContinuousMovableDotItem(MovableDotItem, ContinuousItemMixin):\n    pass\n\n\nclass Continuous2DMovableDotItem(MovableDotItem, ContinuousItemMixin):\n    def __init__(self, radius, scale, offset, min_x, max_x, min_y, max_y):\n        super().__init__(radius, scale, offset, min_x, max_x)\n        self._min_y = min_y\n        self._max_y = max_y\n        self._horizontal_line = None\n\n    @property\n    def horizontal_line(self):\n        return self._horizontal_line\n\n    @horizontal_line.setter\n    def horizontal_line(self, line):\n        line.setVisible(False)\n        self._horizontal_line = line\n\n    def move(self, x):\n        super().move(x)\n        diff_ = np.nan_to_num(self._max_x - self._min_x)\n        k = (x - self._min_x) / diff_ if diff_ else 0\n        self.setY(self._min_y - self.rect().width() / 2 + (self._max_y - self._min_y) * k)\n\n    def mousePressEvent(self, event):\n        self._show_horizontal_line()\n        return super().mousePressEvent(event)\n\n    def mouseMoveEvent(self, event):\n        super().mouseMoveEvent(event)\n        self._show_horizontal_line()\n\n    def mouseReleaseEvent(self, event):\n        self.horizontal_line.setVisible(False)\n        return super().mouseReleaseEvent(event)\n\n    def _show_horizontal_line(self):\n        self.horizontal_line.setY(self.y() + self.rect().width() / 2 -\n                                  abs(self._max_y - self._min_y) / 2)\n        self.horizontal_line.setVisible(True)\n\n\nclass RulerItem(QGraphicsWidget):\n    tick_height = 6\n    tick_width = 0\n    DOT_RADIUS = 12\n    half_tick_height = 3\n    bold_label = True\n    DOT_ITEM_CLS = DotItem\n\n    def __init__(self, name, values, scale, name_offset, offset, labels=None):\n        super().__init__()\n\n        # leading label\n        font = name.document().defaultFont()\n        if self.bold_label:\n            font.setWeight(QFont.Bold)\n        name.setFont(font)\n        name.setPos(name_offset, -10)\n        name.setParentItem(self)\n\n        # prediction marker\n        self.dot = self.DOT_ITEM_CLS(self.DOT_RADIUS, scale, offset, values[0],\n                                     values[-1])\n        self.dot.setParentItem(self)\n\n        # pylint: disable=unused-variable\n        # line\n        line = QGraphicsLineItem(min(values) * scale + offset, 0,\n                                 max(values) * scale + offset, 0,\n                                 self)\n\n        if labels is None:\n            labels = [str(abs(v) if v == -0 else v) for v in values]\n\n        old_x_tick = None\n        shown_items = []\n        w = QGraphicsSimpleTextItem(labels[0]).boundingRect().width()\n        text_finish = values[0] * scale - w + offset - 10\n        for i, (label, value) in enumerate(zip(labels, values)):\n            text = QGraphicsSimpleTextItem(label)\n            x_text = value * scale - text.boundingRect().width() / 2 + offset\n            if text_finish > x_text - 10:\n                y_text, y_tick = self.DOT_RADIUS * 0.7, 0\n                text_finish = values[0] * scale + offset\n            else:\n                y_text = - text.boundingRect().height() - self.DOT_RADIUS * 0.7\n                y_tick = - self.tick_height\n                text_finish = x_text + text.boundingRect().width()\n            text.setPos(x_text, y_text)\n            if not collides(text, shown_items):\n                text.setParentItem(self)\n                shown_items.append(text)\n\n            x_tick = value * scale - self.tick_width / 2 + offset\n            tick = QGraphicsRectItem(\n                x_tick, y_tick, self.tick_width, self.tick_height,\n                self)\n            tick.setBrush(QColor(Qt.black))\n\n            if self.half_tick_height and i:\n                x = x_tick - (x_tick - old_x_tick) / 2\n                half_tick = QGraphicsLineItem(x, - self.half_tick_height, x, 0,\n                                              self)\n            old_x_tick = x_tick\n\n\nclass ProbabilitiesRulerItem(QGraphicsWidget):\n    tick_height = 6\n    DOT_RADIUS = 14\n    y_diff = 4\n\n    def __init__(self, name, values, scale, name_offset, offset, get_points,\n                 title, get_probabilities):\n        super().__init__()\n        self.scale = scale\n        self.offset = offset\n        self.get_points = get_points\n        self.min_val = min(values)\n        self.max_val = max(values)\n\n        # leading labels\n        font = name.document().defaultFont()\n        font.setWeight(QFont.Bold)\n        name_total = QGraphicsTextItem(\"Total\", self)\n        name_total.setFont(font)\n        name_total.setPos(name_offset, -25)\n        name.setFont(font)\n        name.setPos(name_offset, 10)\n        name.setParentItem(self)\n\n        # prediction marker\n        self.dot = ProbabilitiesDotItem(\n            self.DOT_RADIUS, scale, offset, values[0], values[-1],\n            title, get_probabilities)\n        self.dot.setPos(0, (- self.DOT_RADIUS + self.y_diff) / 2)\n        self.dot.setParentItem(self)\n\n        # pylint: disable=unused-variable\n        # two lines\n        t_line = QGraphicsLineItem(self.min_val * scale + offset, 0,\n                                   self.max_val * scale + offset, 0,\n                                   self)\n        p_line = QGraphicsLineItem(self.min_val * scale + offset, self.y_diff,\n                                   self.max_val * scale + offset, self.y_diff,\n                                   self)\n\n        # ticks and labels\n        old_x_tick = values[0] * scale + offset\n        for i, value in enumerate(values[1:]):\n            x_tick = value * scale + offset\n            x = x_tick - (x_tick - old_x_tick) / 2\n            half_tick = QGraphicsLineItem(x, - self.tick_height / 2, x, 0,\n                                          self)\n            old_x_tick = x_tick\n            if i == len(values) - 2:\n                break\n            text = QGraphicsTextItem(str(abs(value) if value == -0 else value),\n                                     self)\n            x_text = value * scale - text.boundingRect().width() / 2 + offset\n            y_text = - text.boundingRect().height() - self.DOT_RADIUS * 0.7\n            text.setPos(x_text, y_text)\n            tick = QGraphicsLineItem(x_tick, -self.tick_height, x_tick, 0,\n                                     self)\n\n        self.prob_items = [\n            (i / 10, QGraphicsTextItem(\" \" + str(i * 10) + \" \"),\n             QGraphicsLineItem(0, 0, 0, 0)) for i in range(1, 10)]\n\n    def rescale(self):\n        shown_items = []\n        for prob, text, tick in self.prob_items:\n            pts = self.get_points(prob)\n            x = pts * self.scale - text.boundingRect().width() / 2 + self.offset\n            text.setPos(x, 10 + self.y_diff)\n            x = pts * self.scale + self.offset\n            tick.setLine(x, 0 + self.y_diff, x, self.tick_height + self.y_diff)\n            text.setParentItem(None)\n            tick.setParentItem(None)\n            text.setVisible(False)\n            tick.setVisible(False)\n            if self.min_val < pts < self.max_val:\n                tick.setParentItem(self)\n                tick.setVisible(True)\n                text.setParentItem(self)\n                if not collides(text, shown_items):\n                    text.setVisible(True)\n                    shown_items.append(text)\n\n\nclass DiscreteFeatureItem(RulerItem):\n    tick_height = 6\n    tick_width = 2\n    half_tick_height = 0\n    bold_label = False\n    DOT_ITEM_CLS = DiscreteMovableDotItem\n\n    def __init__(self, name, variable, values, scale, name_offset, offset):\n        labels = _get_labels(variable)\n        indices = np.argsort(values)\n        labels, values = labels[indices], values[indices]\n        super().__init__(name, values, scale, name_offset, offset, labels)\n        self.dot.tooltip_labels = labels\n        self.dot.tooltip_values = values\n\n\nclass ContinuousFeatureItem(RulerItem):\n    tick_height = 6\n    tick_width = 2\n    half_tick_height = 0\n    bold_label = False\n    DOT_ITEM_CLS = ContinuousMovableDotItem\n\n    def __init__(self, name, variable, data_extremes, values, scale,\n                 name_offset, offset):\n        labels = _get_labels(variable, data_extremes, values)\n        super().__init__(name, values, scale, name_offset, offset, labels)\n        self.dot.tooltip_labels = labels\n        self.dot.tooltip_values = values\n\n\n@singledispatch\ndef _get_labels(*_):\n    return []\n\n\n@_get_labels.register(DiscreteVariable)\ndef _(var: DiscreteVariable, *_):\n    return np.array(var.values)\n\n\n@_get_labels.register(ContinuousVariable)\ndef _(_: ContinuousVariable, data_extremes: List, values: np.ndarray, *__):\n    diff_ = np.nan_to_num(values[-1] - values[0])\n    k = (data_extremes[1] - data_extremes[0]) / diff_ if diff_ else 0\n    return [str(np.round(v * k + data_extremes[0], 1)) for v in values]\n\n\nclass ContinuousFeature2DItem(QGraphicsWidget):\n    tick_height = 6\n    tick_width = 2\n    DOT_RADIUS = 12\n    y_diff = 80\n    n_tck = 4\n\n    def __init__(self, name, _, data_extremes, values, scale, name_offset,\n                 offset):\n        super().__init__()\n        data_start, data_stop = data_extremes[0], data_extremes[1]\n        labels = [str(np.round(data_start + (data_stop - data_start) * i /\n                               (self.n_tck - 1), 1)) for i in range(self.n_tck)]\n\n        # leading label\n        font = name.document().defaultFont()\n        name.setFont(font)\n        name.setPos(name_offset, -10)\n        name.setParentItem(self)\n\n        # labels\n        ascending = data_start < data_stop\n        y_start, y_stop = (self.y_diff, 0) if ascending else (0, self.y_diff)\n        for i in range(self.n_tck):\n            text = QGraphicsSimpleTextItem(labels[i], self)\n            w = text.boundingRect().width()\n            y = y_start + (y_stop - y_start) / (self.n_tck - 1) * i\n            text.setPos(-5 - w, y - 8)\n            tick = QGraphicsLineItem(-2, y, 2, y, self)\n\n        # prediction marker\n        self.dot = Continuous2DMovableDotItem(\n            self.DOT_RADIUS, scale, offset, values[0], values[-1], y_start, y_stop)\n        self.dot.tooltip_labels = labels\n        self.dot.tooltip_values = values\n        self.dot.setParentItem(self)\n        h_line = QGraphicsLineItem(values[0] * scale + offset, self.y_diff / 2,\n                                   values[-1] * scale + offset, self.y_diff / 2,\n                                   self)\n        pen = QPen(Qt.DashLine)\n        pen.setBrush(QColor(Qt.red))\n        h_line.setPen(pen)\n        self.dot.horizontal_line = h_line\n\n        # pylint: disable=unused-variable\n        # line\n        line = QGraphicsLineItem(values[0] * scale + offset, y_start,\n                                 values[-1] * scale + offset, y_stop,\n                                 self)\n\n        # ticks\n        for value in values:\n            diff_ = np.nan_to_num(values[-1] - values[0])\n            k = (value - values[0]) / diff_ if diff_ else 0\n            y_tick = (y_stop - y_start) * k + y_start - self.tick_height / 2\n            x_tick = value * scale - self.tick_width / 2 + offset\n            tick = QGraphicsRectItem(\n                x_tick, y_tick, self.tick_width, self.tick_height,\n                self)\n            tick.setBrush(QColor(Qt.black))\n\n        # rect\n        rect = QGraphicsRectItem(\n            values[0] * scale + offset, -self.y_diff * 0.125,\n            values[-1] * scale + offset, self.y_diff * 1.25,\n            self)\n        pen = QPen(Qt.DotLine)\n        pen.setBrush(QColor(50, 150, 200, 255))\n        rect.setPen(pen)\n        self.setPreferredSize(self.preferredWidth(), self.y_diff * 1.5)\n\n\nclass NomogramItem(QGraphicsWidget):\n    def __init__(self):\n        super().__init__()\n        self._items = []\n        self.setLayout(QGraphicsLinearLayout(Qt.Vertical))\n\n    def add_items(self, items):\n        self._items = items\n        for item in items:\n            self.layout().addItem(item)\n\n\nclass OWNomogram(OWWidget):\n    name = \"Nomogram\"\n    description = \" Nomograms for Visualization of Naive Bayesian\" \\\n                  \" and Logistic Regression Classifiers.\"\n    icon = \"icons/Nomogram.svg\"\n    priority = 2000\n    keywords = []\n\n    class Inputs:\n        classifier = Input(\"Classifier\", Model)\n        data = Input(\"Data\", Table)\n\n    class Outputs:\n        features = Output(\"Features\", AttributeList)\n\n    MAX_N_ATTRS = 1000\n    POINT_SCALE = 0\n    ALIGN_LEFT = 0\n    ALIGN_ZERO = 1\n    ACCEPTABLE = (NaiveBayesModel, LogisticRegressionClassifier)\n    settingsHandler = ClassValuesContextHandler()\n    target_class_index = ContextSetting(0)\n    normalize_probabilities = Setting(False)\n    scale = Setting(1)\n    display_index = Setting(1)\n    n_attributes = Setting(10)\n    sort_index = Setting(SortBy.ABSOLUTE)\n    cont_feature_dim_index = Setting(0)\n\n    graph_name = \"scene\"\n\n    class Error(OWWidget.Error):\n        invalid_classifier = Msg(\"Nomogram accepts only Naive Bayes and \"\n                                 \"Logistic Regression classifiers.\")\n\n    def __init__(self):\n        super().__init__()\n        self.instances = None\n        self.domain = None\n        self.data = None\n        self.classifier = None\n        self.align = OWNomogram.ALIGN_ZERO\n        self.log_odds_ratios = []\n        self.log_reg_coeffs = []\n        self.log_reg_coeffs_orig = []\n        self.log_reg_cont_data_extremes = []\n        self.p = None\n        self.b0 = None\n        self.points = []\n        self.feature_items = {}\n        self.feature_marker_values = []\n        self.scale_marker_values = lambda x: x\n        self.nomogram_main = None\n        self.vertical_line = None\n        self.hidden_vertical_line = None\n        self.old_target_class_index = self.target_class_index\n        self.repaint = False\n\n        self.info.set_input_summary(self.info.NoInput)\n\n        # GUI\n        box = gui.vBox(self.controlArea, \"Target class\")\n        self.class_combo = gui.comboBox(\n            box, self, \"target_class_index\", callback=self._class_combo_changed,\n            contentsLength=12, searchable=True)\n        self.norm_check = gui.checkBox(\n            box, self, \"normalize_probabilities\", \"Normalize probabilities\",\n            hidden=True, callback=self.update_scene,\n            tooltip=\"For multiclass data 1 vs. all probabilities do not\"\n                    \" sum to 1 and therefore could be normalized.\")\n\n        self.scale_radio = gui.radioButtons(\n            self.controlArea, self, \"scale\", [\"Point scale\", \"Log odds ratios\"],\n            box=\"Scale\", callback=self.update_scene)\n\n        box = gui.vBox(self.controlArea, \"Display features\")\n        grid = QGridLayout()\n        radio_group = gui.radioButtonsInBox(\n            box, self, \"display_index\", [], orientation=grid,\n            callback=self.update_scene)\n        radio_all = gui.appendRadioButton(\n            radio_group, \"All\", addToLayout=False)\n        radio_best = gui.appendRadioButton(\n            radio_group, \"Best ranked:\", addToLayout=False)\n        spin_box = gui.hBox(None, margin=0)\n        self.n_spin = gui.spin(\n            spin_box, self, \"n_attributes\", 1, self.MAX_N_ATTRS, label=\" \",\n            controlWidth=60, callback=self._n_spin_changed)\n        grid.addWidget(radio_all, 1, 1)\n        grid.addWidget(radio_best, 2, 1)\n        grid.addWidget(spin_box, 2, 2)\n\n        self.sort_combo = gui.comboBox(\n            box, self, \"sort_index\", label=\"Rank by:\", items=SortBy.items(),\n            orientation=Qt.Horizontal, callback=self.update_scene)\n\n        self.cont_feature_dim_combo = gui.comboBox(\n            box, self, \"cont_feature_dim_index\", label=\"Numeric features: \",\n            items=[\"1D projection\", \"2D curve\"], orientation=Qt.Horizontal,\n            callback=self.update_scene)\n\n        gui.rubber(self.controlArea)\n\n        class _GraphicsView(QGraphicsView):\n            def __init__(self, scene, parent, **kwargs):\n                for k, v in dict(verticalScrollBarPolicy=Qt.ScrollBarAlwaysOff,\n                                 horizontalScrollBarPolicy=Qt.ScrollBarAlwaysOff,\n                                 viewportUpdateMode=QGraphicsView.BoundingRectViewportUpdate,\n                                 renderHints=(QPainter.Antialiasing |\n                                              QPainter.TextAntialiasing |\n                                              QPainter.SmoothPixmapTransform),\n                                 alignment=(Qt.AlignTop |\n                                            Qt.AlignLeft),\n                                 sizePolicy=QSizePolicy(QSizePolicy.MinimumExpanding,\n                                                        QSizePolicy.MinimumExpanding)).items():\n                    kwargs.setdefault(k, v)\n\n                super().__init__(scene, parent, **kwargs)\n\n        class GraphicsView(_GraphicsView):\n            def __init__(self, scene, parent):\n                super().__init__(scene, parent,\n                                 verticalScrollBarPolicy=Qt.ScrollBarAlwaysOn,\n                                 styleSheet='QGraphicsView {background: white}')\n                self.viewport().setMinimumWidth(300)  # XXX: This prevents some tests failing\n                self._is_resizing = False\n\n            w = self\n\n            def resizeEvent(self, resizeEvent):\n                # Recompute main scene on window width change\n                if resizeEvent.size().width() != resizeEvent.oldSize().width():\n                    self._is_resizing = True\n                    self.w.update_scene()\n                    self._is_resizing = False\n                return super().resizeEvent(resizeEvent)\n\n            def is_resizing(self):\n                return self._is_resizing\n\n            def sizeHint(self):\n                return QSize(400, 200)\n\n        class FixedSizeGraphicsView(_GraphicsView):\n            def __init__(self, scene, parent):\n                super().__init__(scene, parent,\n                                 sizePolicy=QSizePolicy(QSizePolicy.MinimumExpanding,\n                                                        QSizePolicy.Minimum))\n\n            def sizeHint(self):\n                return QSize(400, 85)\n\n        scene = self.scene = QGraphicsScene(self)\n\n        top_view = self.top_view = FixedSizeGraphicsView(scene, self)\n        mid_view = self.view = GraphicsView(scene, self)\n        bottom_view = self.bottom_view = FixedSizeGraphicsView(scene, self)\n\n        for view in (top_view, mid_view, bottom_view):\n            self.mainArea.layout().addWidget(view)\n\n        self.dot_animator = GraphicsColorAnimator(\n            self, 3000,\n            [\n                (0.9, DOT_COLOR),\n                (0.925, DOT_COLOR.lighter(115)),\n                (0.95, DOT_COLOR),\n                (0.975, DOT_COLOR.lighter(115)),\n                (1.0, DOT_COLOR)\n            ]\n        )\n\n    def _class_combo_changed(self):\n        with np.errstate(invalid='ignore'):\n            coeffs = [np.nan_to_num(p[self.target_class_index] /\n                                    p[self.old_target_class_index])\n                      for p in self.points]\n        points = [p[self.old_target_class_index] for p in self.points]\n        self.feature_marker_values = [\n            self.get_points_from_coeffs(v, c, p) for (v, c, p) in\n            zip(self.feature_marker_values, coeffs, points)]\n        self.feature_marker_values = np.asarray(self.feature_marker_values)\n        self.update_scene()\n        self.old_target_class_index = self.target_class_index\n\n    def _n_spin_changed(self):\n        self.display_index = 1\n        self.update_scene()\n\n    def update_controls(self):\n        self.class_combo.clear()\n        self.norm_check.setHidden(True)\n        self.cont_feature_dim_combo.setEnabled(True)\n        if self.domain is not None:\n            values = self.domain.class_vars[0].values\n            if values:\n                self.class_combo.addItems(values)\n                self.target_class_index = 0\n            if len(self.domain.attributes) > self.MAX_N_ATTRS:\n                self.display_index = 1\n            if len(self.domain.class_vars[0].values) > 2:\n                self.norm_check.setHidden(False)\n            if not self.domain.has_continuous_attributes():\n                self.cont_feature_dim_combo.setEnabled(False)\n                self.cont_feature_dim_index = 0\n        model = self.sort_combo.model()\n        item = model.item(SortBy.POSITIVE)\n        item.setFlags(item.flags() | Qt.ItemIsEnabled)\n        item = model.item(SortBy.NEGATIVE)\n        item.setFlags(item.flags() | Qt.ItemIsEnabled)\n        self.align = OWNomogram.ALIGN_ZERO\n        if self.classifier and isinstance(self.classifier,\n                                          LogisticRegressionClassifier):\n            self.align = OWNomogram.ALIGN_LEFT\n\n    @Inputs.data\n    def set_data(self, data):\n        self.instances = data\n        summary = len(data) if data else self.info.NoInput\n        details = format_summary_details(data) if data else \"\"\n        self.info.set_input_summary(summary, details)\n        self.feature_marker_values = []\n        self.set_feature_marker_values()\n        self.update_scene()\n\n    @Inputs.classifier\n    def set_classifier(self, classifier):\n        self.closeContext()\n        self.classifier = classifier\n        self.Error.clear()\n        if self.classifier and not isinstance(self.classifier, self.ACCEPTABLE):\n            self.Error.invalid_classifier()\n            self.classifier = None\n        self.domain = self.classifier.domain if self.classifier else None\n        self.data = None\n        self.calculate_log_odds_ratios()\n        self.calculate_log_reg_coefficients()\n        self.update_controls()\n        self.openContext(self.domain.class_var if self.domain is not None\n                         else None)\n        self.points = self.log_odds_ratios or self.log_reg_coeffs\n        self.feature_marker_values = []\n        self.old_target_class_index = self.target_class_index\n        self.update_scene()\n\n    def calculate_log_odds_ratios(self):\n        self.log_odds_ratios = []\n        self.p = None\n        if self.classifier is None or self.domain is None:\n            return\n        if not isinstance(self.classifier, NaiveBayesModel):\n            return\n\n        log_cont_prob = self.classifier.log_cont_prob\n        class_prob = self.classifier.class_prob\n        for i in range(len(self.domain.attributes)):\n            ca = np.exp(log_cont_prob[i]) * class_prob[:, None]\n            _or = (ca / (1 - ca)) / (class_prob / (1 - class_prob))[:, None]\n            self.log_odds_ratios.append(np.log(_or))\n        self.p = class_prob\n\n    def calculate_log_reg_coefficients(self):\n        self.log_reg_coeffs = []\n        self.log_reg_cont_data_extremes = []\n        self.b0 = None\n        if self.classifier is None or self.domain is None:\n            return\n        if not isinstance(self.classifier, LogisticRegressionClassifier):\n            return\n\n        self.domain = self.reconstruct_domain(self.classifier.original_domain,\n                                              self.domain)\n        self.data = self.classifier.original_data.transform(self.domain)\n        attrs, ranges, start = self.domain.attributes, [], 0\n        for attr in attrs:\n            stop = start + len(attr.values) if attr.is_discrete else start + 1\n            ranges.append(slice(start, stop))\n            start = stop\n\n        self.b0 = self.classifier.intercept\n        coeffs = self.classifier.coefficients\n        if len(self.domain.class_var.values) == 2:\n            self.b0 = np.hstack((self.b0 * (-1), self.b0))\n            coeffs = np.vstack((coeffs * (-1), coeffs))\n        self.log_reg_coeffs = [coeffs[:, r] for r in ranges]\n        self.log_reg_coeffs_orig = self.log_reg_coeffs.copy()\n\n        min_values = nanmin(self.data.X, axis=0)\n        max_values = nanmax(self.data.X, axis=0)\n\n        for i, min_t, max_t in zip(range(len(self.log_reg_coeffs)),\n                                   min_values, max_values):\n            if self.log_reg_coeffs[i].shape[1] == 1:\n                coef = self.log_reg_coeffs[i]\n                self.log_reg_coeffs[i] = np.hstack((coef * min_t, coef * max_t))\n                self.log_reg_cont_data_extremes.append(\n                    [sorted([min_t, max_t], reverse=(c < 0)) for c in coef.flat])\n            else:\n                self.log_reg_cont_data_extremes.append([None])\n\n    def update_scene(self):\n        self.clear_scene()\n        if self.domain is None or not len(self.points[0]):\n            self.Outputs.features.send(None)\n            return\n\n        n_attrs = self.n_attributes if self.display_index else int(1e10)\n        attr_inds, attributes = zip(*self.get_ordered_attributes()[:n_attrs])\n        self.Outputs.features.send(AttributeList(attributes))\n\n        name_items = [QGraphicsTextItem(attr.name) for attr in attributes]\n        point_text = QGraphicsTextItem(\"Points\")\n        probs_text = QGraphicsTextItem(\"Probabilities (%)\")\n        all_items = name_items + [point_text, probs_text]\n        name_offset = -max(t.boundingRect().width() for t in all_items) - 10\n        w = self.view.viewport().rect().width()\n        max_width = w + name_offset - 30\n\n        points = [self.points[i][self.target_class_index]\n                  for i in attr_inds]\n        if self.align == OWNomogram.ALIGN_LEFT:\n            points = [p - p.min() for p in points]\n        max_ = np.nan_to_num(max(max(abs(p)) for p in points))\n        d = 100 / max_ if max_ else 1\n        minimums = [p[self.target_class_index].min() for p in self.points]\n        if self.scale == OWNomogram.POINT_SCALE:\n            points = [p * d for p in points]\n\n            if self.align == OWNomogram.ALIGN_LEFT:\n                self.scale_marker_values = lambda x: (x - minimums) * d\n            else:\n                self.scale_marker_values = lambda x: x * d\n        else:\n            if self.align == OWNomogram.ALIGN_LEFT:\n                self.scale_marker_values = lambda x: x - minimums\n            else:\n                self.scale_marker_values = lambda x: x\n\n        point_item, nomogram_head = self.create_main_nomogram(\n            attributes, attr_inds,\n            name_items, points, max_width, point_text, name_offset)\n        probs_item, nomogram_foot = self.create_footer_nomogram(\n            probs_text, d, minimums, max_width, name_offset)\n        for item in self.feature_items.values():\n            item.dot.point_dot = point_item.dot\n            item.dot.probs_dot = probs_item.dot\n            item.dot.vertical_line = self.hidden_vertical_line\n\n        self.dot_animator.setGraphicsItems(\n            [item.dot for item in self.feature_items.values()]\n        )\n        self.dot_animator.start()\n\n        self.nomogram = nomogram = NomogramItem()\n        nomogram.add_items([nomogram_head, self.nomogram_main, nomogram_foot])\n        self.scene.addItem(nomogram)\n\n        self.set_feature_marker_values()\n\n        rect = QRectF(self.scene.itemsBoundingRect().x(),\n                      self.scene.itemsBoundingRect().y(),\n                      self.scene.itemsBoundingRect().width(),\n                      self.nomogram.preferredSize().height()).adjusted(10, 0, 20, 0)\n        self.scene.setSceneRect(rect)\n\n        # Clip top and bottom (60 and 150) parts from the main view\n        self.view.setSceneRect(rect.x(), rect.y() + 80, rect.width() - 10, rect.height() - 160)\n        self.view.viewport().setMaximumHeight(rect.height() - 160)\n        # Clip main part from top/bottom views\n        # below point values are imprecise (less/more than required) but this\n        # is not a problem due to clipped scene content still being drawn\n        self.top_view.setSceneRect(rect.x(), rect.y() + 3, rect.width() - 10, 20)\n        self.bottom_view.setSceneRect(rect.x(), rect.height() - 110, rect.width() - 10, 30)\n\n    @staticmethod\n    def _adjust_scale(attributes, points, max_width, diff, attr_inds,\n                      log_reg_cont_data_extremes, cls_index):\n        if not diff:\n            return max_width\n\n        def offset(name, point):\n            text_ = QGraphicsTextItem(name).boundingRect()\n            return scale * point + text_.width() / 2\n\n        lr = log_reg_cont_data_extremes\n        scale = max_width / diff\n        names = list(chain.from_iterable(\n            [_get_labels(a, lr and lr[i] and lr[i][0] and lr[i][cls_index],\n                         OWNomogram.get_ruler_values(p.min(), p.max(),\n                                                     scale * p.ptp(), False))\n             for i, a, p in zip(attr_inds, attributes, points)]))\n        points = list(chain.from_iterable(points))\n\n        old_scale = scale + 1\n        while old_scale > scale:\n            old_scale = scale\n            offsets = [offset(n, p) for n, p in zip(names, points)]\n            most_right_name = names[np.argmax(offsets)]\n            text = QGraphicsTextItem(most_right_name).boundingRect()\n            scale = (max_width - text.width() / 2) / diff\n        return scale\n\n    def create_main_nomogram(self, attributes, attr_inds, name_items, points,\n                             max_width, point_text, name_offset):\n        cls_index = self.target_class_index\n        min_p = min(p.min() for p in points)\n        max_p = max(p.max() for p in points)\n        values = self.get_ruler_values(min_p, max_p, max_width)\n        min_p, max_p = min(values), max(values)\n        diff_ = np.nan_to_num(max_p - min_p)\n        scale_x = self._adjust_scale(\n            attributes, points, max_width, diff_, attr_inds,\n            self.log_reg_cont_data_extremes, cls_index\n        )\n\n        nomogram_header = NomogramItem()\n        point_item = RulerItem(point_text, values, scale_x, name_offset,\n                               - scale_x * min_p)\n        point_item.setPreferredSize(point_item.preferredWidth(), 35)\n        nomogram_header.add_items([point_item])\n\n        self.nomogram_main = NomogramItem()\n        cont_feature_item_class = ContinuousFeature2DItem if \\\n            self.cont_feature_dim_index else ContinuousFeatureItem\n\n        feature_items = [\n            DiscreteFeatureItem(\n                name_item, attr, point,\n                scale_x, name_offset, - scale_x * min_p)\n            if attr.is_discrete else\n            cont_feature_item_class(\n                name_item, attr, self.log_reg_cont_data_extremes[i][cls_index],\n                self.get_ruler_values(\n                    point.min(), point.max(),\n                    scale_x * point.ptp(), False),\n                scale_x, name_offset, - scale_x * min_p)\n            for i, attr, name_item, point in\n            zip(attr_inds, attributes, name_items, points)]\n\n        self.nomogram_main.add_items(feature_items)\n        self.feature_items = OrderedDict(sorted(zip(attr_inds, feature_items)))\n\n        x = - scale_x * min_p\n        y = self.nomogram_main.layout().preferredHeight() + 10\n        self.vertical_line = QGraphicsLineItem(x, -6, x, y)\n        self.vertical_line.setPen(QPen(Qt.DotLine))\n        self.vertical_line.setParentItem(point_item)\n        self.hidden_vertical_line = QGraphicsLineItem(x, -6, x, y)\n        pen = QPen(Qt.DashLine)\n        pen.setBrush(QColor(Qt.red))\n        self.hidden_vertical_line.setPen(pen)\n        self.hidden_vertical_line.setParentItem(point_item)\n\n        return point_item, nomogram_header\n\n    def get_ordered_attributes(self):\n        \"\"\"Return (in_domain_index, attr) pairs, ordered by method in SortBy combo\"\"\"\n        if self.domain is None or not self.domain.attributes:\n            return []\n\n        attrs = self.domain.attributes\n        sort_by = self.sort_index\n        class_value = self.target_class_index\n\n        if sort_by == SortBy.NO_SORTING:\n            return list(enumerate(attrs))\n\n        elif sort_by == SortBy.NAME:\n\n            def key(x):\n                _, attr = x\n                return attr.name.lower()\n\n        elif sort_by == SortBy.ABSOLUTE:\n\n            def key(x):\n                i, attr = x\n                if attr.is_discrete:\n                    ptp = self.points[i][class_value].ptp()\n                else:\n                    coef = np.abs(self.log_reg_coeffs_orig[i][class_value]).mean()\n                    ptp = coef * np.ptp(self.log_reg_cont_data_extremes[i][class_value])\n                return -ptp\n\n        elif sort_by == SortBy.POSITIVE:\n\n            def key(x):\n                i, attr = x\n                max_value = (self.points[i][class_value].max()\n                             if attr.is_discrete else\n                             np.mean(self.log_reg_cont_data_extremes[i][class_value]))\n                return -max_value\n\n        elif sort_by == SortBy.NEGATIVE:\n\n            def key(x):\n                i, attr = x\n                min_value = (self.points[i][class_value].min()\n                             if attr.is_discrete else\n                             np.mean(self.log_reg_cont_data_extremes[i][class_value]))\n                return min_value\n\n        return sorted(enumerate(attrs), key=key)\n\n\n    def create_footer_nomogram(self, probs_text, d, minimums,\n                               max_width, name_offset):\n        # pylint: disable=invalid-unary-operand-type\n        eps, d_ = 0.05, 1\n        k = - np.log(self.p / (1 - self.p)) if self.p is not None else - self.b0\n        min_sum = k[self.target_class_index] - np.log((1 - eps) / eps)\n        max_sum = k[self.target_class_index] - np.log(eps / (1 - eps))\n        if self.align == OWNomogram.ALIGN_LEFT:\n            max_sum = max_sum - sum(minimums)\n            min_sum = min_sum - sum(minimums)\n            for i in range(len(k)):  # pylint: disable=consider-using-enumerate\n                k[i] = k[i] - sum([min(q) for q in [p[i] for p in self.points]])\n        if self.scale == OWNomogram.POINT_SCALE:\n            min_sum *= d\n            max_sum *= d\n            d_ = d\n\n        values = self.get_ruler_values(min_sum, max_sum, max_width)\n        min_sum, max_sum = min(values), max(values)\n        diff_ = np.nan_to_num(max_sum - min_sum)\n        scale_x = max_width / diff_ if diff_ else max_width\n        cls_var, cls_index = self.domain.class_var, self.target_class_index\n        nomogram_footer = NomogramItem()\n\n        def get_normalized_probabilities(val):\n            if not self.normalize_probabilities:\n                return 1 / (1 + np.exp(k[cls_index] - val / d_))\n            totals = self.__get_totals_for_class_values(minimums)\n            p_sum = np.sum(1 / (1 + np.exp(k - totals / d_)))\n            return 1 / (1 + np.exp(k[cls_index] - val / d_)) / p_sum\n\n        def get_points(prob):\n            if not self.normalize_probabilities:\n                return (k[cls_index] - np.log(1 / prob - 1)) * d_\n            totals = self.__get_totals_for_class_values(minimums)\n            p_sum = np.sum(1 / (1 + np.exp(k - totals / d_)))\n            return (k[cls_index] - np.log(1 / (prob * p_sum) - 1)) * d_\n\n        probs_item = ProbabilitiesRulerItem(\n            probs_text, values, scale_x, name_offset, - scale_x * min_sum,\n            get_points=get_points,\n            title=\"{}='{}'\".format(cls_var.name, cls_var.values[cls_index]),\n            get_probabilities=get_normalized_probabilities)\n        nomogram_footer.add_items([probs_item])\n        return probs_item, nomogram_footer\n\n    def __get_totals_for_class_values(self, minimums):\n        cls_index = self.target_class_index\n        marker_values = self.scale_marker_values(self.feature_marker_values)\n        totals = np.full(len(self.domain.class_var.values), np.nan)\n        totals[cls_index] = marker_values.sum()\n        for i in range(len(self.domain.class_var.values)):\n            if i == cls_index:\n                continue\n            coeffs = [np.nan_to_num(p[i] / p[cls_index]) for p in self.points]\n            points = [p[cls_index] for p in self.points]\n            total = sum([self.get_points_from_coeffs(v, c, p) for (v, c, p)\n                         in zip(self.feature_marker_values, coeffs, points)])\n            if self.align == OWNomogram.ALIGN_LEFT:\n                points = [p - m for m, p in zip(minimums, points)]\n                total -= sum([min(p) for p in [p[i] for p in self.points]])\n            d = 100 / max(max(abs(p)) for p in points)\n            if self.scale == OWNomogram.POINT_SCALE:\n                total *= d\n            totals[i] = total\n        assert not np.any(np.isnan(totals))\n        return totals\n\n    def set_feature_marker_values(self):\n        if not (len(self.points) and len(self.feature_items)):\n            return\n        if not len(self.feature_marker_values):\n            self._init_feature_marker_values()\n        marker_values = self.scale_marker_values(self.feature_marker_values)\n\n        invisible_sum = 0\n        for i, marker in enumerate(marker_values):\n            try:\n                item = self.feature_items[i]\n            except KeyError:\n                invisible_sum += marker\n            else:\n                item.dot.move_to_val(marker)\n\n        item.dot.probs_dot.move_to_sum(invisible_sum)\n\n    def _init_feature_marker_values(self):\n        self.feature_marker_values = []\n        cls_index = self.target_class_index\n        instances = self.instances.transform(self.domain) \\\n            if self.instances else None\n        values = []\n        for i, attr in enumerate(self.domain.attributes):\n            value, feature_val = 0, None\n            if len(self.log_reg_coeffs):\n                if attr.is_discrete:\n                    ind, n = unique(self.data.X[:, i], return_counts=True)\n                    feature_val = np.nan_to_num(ind[np.argmax(n)])\n                else:\n                    feature_val = nanmean(self.data.X[:, i])\n\n            # If data is provided on a separate signal, use the first data\n            # instance to position the points instead of the mean\n            inst_in_dom = instances and attr in instances.domain\n            if inst_in_dom and not np.isnan(instances[0][attr]):\n                feature_val = instances[0][attr]\n\n            if feature_val is not None:\n                value = (self.points[i][cls_index][int(feature_val)]\n                         if attr.is_discrete else\n                         self.log_reg_coeffs_orig[i][cls_index][0] * feature_val)\n            values.append(value)\n        self.feature_marker_values = np.asarray(values)\n\n    def clear_scene(self):\n        self.feature_items = {}\n        self.scale_marker_values = lambda x: x\n        self.nomogram = None\n        self.nomogram_main = None\n        self.vertical_line = None\n        self.hidden_vertical_line = None\n        self.dot_animator.clear()\n        self.scene.clear()\n\n    def send_report(self):\n        self.report_plot()\n\n    @staticmethod\n    def reconstruct_domain(original, preprocessed):\n        # abuse dict to make \"in\" comparisons faster\n        attrs = OrderedDict()\n        for attr in preprocessed.attributes:\n            cv = attr._compute_value.variable._compute_value\n            var = cv.variable if cv else original[attr.name]\n            if var in attrs:    # the reason for OrderedDict\n                continue\n            attrs[var] = None   # we only need keys\n        attrs = list(attrs.keys())\n        return Domain(attrs, original.class_var, original.metas)\n\n    @staticmethod\n    def get_ruler_values(start, stop, max_width, round_to_nearest=True):\n        if max_width == 0:\n            return [0]\n        diff = np.nan_to_num((stop - start) / max_width)\n        if diff <= 0:\n            return [0]\n        decimals = int(np.floor(np.log10(diff)))\n        if diff > 4 * pow(10, decimals):\n            step = 5 * pow(10, decimals + 2)\n        elif diff > 2 * pow(10, decimals):\n            step = 2 * pow(10, decimals + 2)\n        elif diff > 1 * pow(10, decimals):\n            step = 1 * pow(10, decimals + 2)\n        else:\n            step = 5 * pow(10, decimals + 1)\n        round_by = int(- np.floor(np.log10(step)))\n        r = start % step\n        if not round_to_nearest:\n            _range = np.arange(start + step, stop + r, step) - r\n            start, stop = np.floor(start * 100) / 100, np.ceil(stop * 100) / 100\n            return np.round(np.hstack((start, _range, stop)), 2)\n        return np.round(np.arange(start, stop + r + step, step) - r, round_by)\n\n    @staticmethod\n    def get_points_from_coeffs(current_value, coefficients, possible_values):\n        if np.isnan(possible_values).any():\n            return 0\n        # pylint: disable=undefined-loop-variable\n        indices = np.argsort(possible_values)\n        sorted_values = possible_values[indices]\n        sorted_coefficients = coefficients[indices]\n        for i, val in enumerate(sorted_values):\n            if current_value < val:\n                break\n        diff = sorted_values[i] - sorted_values[i - 1]\n        k = 0 if diff < 1e-6 else (sorted_values[i] - current_value) / \\\n                                  (sorted_values[i] - sorted_values[i - 1])\n        return sorted_coefficients[i - 1] * sorted_values[i - 1] * k + \\\n               sorted_coefficients[i] * sorted_values[i] * (1 - k)\n\n    def reset_settings(self):\n        with warnings.catch_warnings():\n            # setting target_class_index will trigger this innocent warning\n            warnings.filterwarnings(\n                \"ignore\", \"combo box 'target_class_index' is empty\")\n            self._reset_settings()\n        self.update_scene()\n\n\nif __name__ == \"__main__\":  # pragma: no cover\n    from Orange.classification import NaiveBayesLearner  #, LogisticRegressionLearner\n    data = Table(\"heart_disease\")\n    clf = NaiveBayesLearner()(data)\n    # clf = LogisticRegressionLearner()(data)\n    WidgetPreview(OWNomogram).run(set_classifier=clf, set_data=data)\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:** Reconstructing Domain in OWNomogram Introduces NaN Values in Transformed Data

**Description:**
When attempting to reconstruct the domain and transform data using the `OWNomogram` widget, the transformed data contains `NaN` values in the first row. This issue causes the transformation to produce invalid results, leading to downstream errors in data processing.

**Example Code:**
```python
def test_reconstruct_domain(self):
    data = Table("heart_disease")
    cls = LogisticRegressionLearner()(data)
    domain = OWNomogram.reconstruct_domain(cls.original_domain, cls.domain)
    transformed_data = cls.original_data.transform(domain)
    # Unexpected NaN values in transformed_data.X
```

**Expected Behavior:**
The `transformed_data.X` should have the same shape as the original `data.X` and should not contain any `NaN` values. The domain reconstruction should accurately retain all original compute values without introducing invalid entries.

**Actual Behavior:**
After reconstructing the domain and transforming the data, the first row of `transformed_data.X` contains `NaN` values:
```
AssertionError: True is not false
```
This indicates that `np.isnan(transformed_data.X[0]).any()` returns `True`, meaning there are `NaN` values where there should be valid numerical data.

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

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

--- BEGIN FILE ---
```
### Orange/widgets/visualize/ownomogram.py
import time
import warnings
from itertools import chain
from typing import List
from functools import singledispatch
from enum import IntEnum
from collections import OrderedDict

import numpy as np

from AnyQt.QtWidgets import (
    QGraphicsView, QGraphicsScene, QGraphicsItem, QGraphicsSimpleTextItem,
    QGraphicsTextItem, QGraphicsLineItem, QGraphicsWidget, QGraphicsRectItem,
    QGraphicsEllipseItem, QGraphicsLinearLayout, QGridLayout, QLabel, QFrame,
    QSizePolicy, QDesktopWidget,
)
from AnyQt.QtGui import QColor, QPainter, QFont, QPen, QBrush
from AnyQt.QtCore import Qt, QRectF, QSize, QPropertyAnimation, QObject, \
    pyqtProperty

from Orange.data import Table, Domain, DiscreteVariable, ContinuousVariable
from Orange.statistics.util import nanmin, nanmax, nanmean, unique
from Orange.classification import Model
from Orange.classification.naive_bayes import NaiveBayesModel
from Orange.classification.logistic_regression import \
    LogisticRegressionClassifier
from Orange.widgets.settings import Setting, ContextSetting, \
    ClassValuesContextHandler
from Orange.widgets.utils.widgetpreview import WidgetPreview
from Orange.widgets.utils.state_summary import format_summary_details
from Orange.widgets.widget import OWWidget, Msg, Input, Output, AttributeList
from Orange.widgets import gui


def collides(item, items):
    return any(item.collidesWithItem(i) for i in items)


class SortBy(IntEnum):
    NO_SORTING, NAME, ABSOLUTE, POSITIVE, NEGATIVE = 0, 1, 2, 3, 4

    @staticmethod
    def items():
        return ["No sorting", "Name", "Absolute importance",
                "Positive influence", "Negative influence"]


class MovableToolTip(QLabel):
    def __init__(self):
        super().__init__()
        self.setFrameShape(QFrame.StyledPanel)
        self.setWindowFlags(Qt.ToolTip)
        self.hide()

    def show(self, pos, text, change_y=True):
        self.setText(text)
        self.adjustSize()

        x, y = pos.x(), (pos.y() + 15 if change_y else self.y())
        avail = QDesktopWidget().availableGeometry(self)
        if x + self.width() > avail.right():
            x -= self.width()
        if y + self.height() > avail.bottom():
            y = (pos.y() - 10 - self.height() if change_y else self.y() - self.height())
        self.move(x, y)
        super().show()


DOT_COLOR = QColor(170, 220, 255, 255)


class DotItem(QGraphicsEllipseItem):
    TOOLTIP_STYLE = """ul {margin-top: 1px; margin-bottom: 1px;}"""
    TOOLTIP_TEMPLATE = """<html><head><style type="text/css">{}</style>
    </head><body><b>{}</b><hr/>{}</body></html>
    """

    def __init__(self, radius, scale, offset, min_x, max_x):
        super().__init__(0, 0, radius, radius)
        self._min_x = min_x * scale - radius / 2 + offset
        self._max_x = max_x * scale - radius / 2 + offset
        self._scale = scale
        self._offset = offset
        self.setPos(0, - radius / 2)
        self.setFlag(QGraphicsItem.ItemIsMovable)
        self.setBrush(DOT_COLOR)
        self.setPen(QPen(QBrush(QColor(20, 130, 250, 255)), 2))
        self.setZValue(100)
        self.tool_tip = MovableToolTip()
        self.setAcceptHoverEvents(True)

    @property
    def value(self):
        return (self.x() + self.rect().width() / 2 - self._offset) / self._scale

    def move(self, x):
        self.setX(x)

    def move_to_val(self, val):
        x = np.clip(self._scale * val - self.rect().width() / 2 + self._offset,
                    self._min_x, self._max_x)
        self.move(x)

    def hoverEnterEvent(self, event):
        self.tool_tip.show(event.screenPos(), self.get_tooltip_text())

    def hoverLeaveEvent(self, event):
        self.tool_tip.hide()

    def mouseMoveEvent(self, _):
        # Prevent click-moving of these items
        return


class ProbabilitiesDotItem(DotItem):
    def __init__(self, radius, scale, offset, min_x, max_x, title,
                 get_probabilities):
        self.title = title
        self.get_probabilities = get_probabilities
        self.movable_dot_items = []
        self._invisible_sum = 0
        super().__init__(radius, scale, offset, min_x, max_x)
        self.setBrush(QColor(150, 150, 150, 255))
        self.setPen(QPen(QBrush(QColor(75, 75, 75, 255)), 2))

    def move_to_sum(self, invisible_sum: float = None):
        total = sum(item.value for item in self.movable_dot_items)

        if invisible_sum is not None:
            self._invisible_sum = invisible_sum
        total += self._invisible_sum

        self.move_to_val(total)
        self.parentItem().rescale()

    def get_tooltip_text(self):
        text = "Total: {} <br/>Probability: {:.0%}".format(
            np.round(self.value, 2),
            np.round(self.get_probabilities(self.value), 2))
        return self.TOOLTIP_TEMPLATE.format(
            self.TOOLTIP_STYLE, self.title, text)


class MovableDotItem(DotItem):
    def __init__(self, radius, scale, offset, min_x, max_x):
        self.tooltip_labels = []
        self.tooltip_values = []
        super().__init__(radius, scale, offset, min_x, max_x)
        self._x = min_x * scale - radius / 2 + offset
        self._point_dot = None
        self._total_dot = None
        self._probs_dot = None
        self._vertical_line = None
        self._mousePressFunc = None

    @property
    def vertical_line(self):
        return self._vertical_line

    @vertical_line.setter
    def vertical_line(self, line):
        line.setVisible(False)
        self._vertical_line = line

    @property
    def point_dot(self):
        return self._point_dot

    @point_dot.setter
    def point_dot(self, dot):
        dot.setVisible(False)
        self._point_dot = dot

    @property
    def total_dot(self):
        return self._total_dot

    @total_dot.setter
    def total_dot(self, dot):
        self._total_dot = dot
        self._total_dot.movable_dot_items.append(self)

    @property
    def probs_dot(self):
        return self._probs_dot

    @probs_dot.setter
    def probs_dot(self, dot):
        self._probs_dot = dot
        self._probs_dot.movable_dot_items.append(self)

    def hookOnMousePress(self, func):
        self._mousePressFunc = func

    def unhookOnMousePress(self):
        self._mousePressFunc = None

    def mousePressEvent(self, event):
        if self._mousePressFunc:
            self._mousePressFunc()
            self._mousePressFunc = None
        self.tool_tip.show(event.screenPos(), self.get_tooltip_text(), False)
        self._x = event.pos().x()
        self.setBrush(QColor(50, 180, 250, 255))
        self._show_vertical_line_and_point_dot()
        return super().mousePressEvent(event)

    def mouseMoveEvent(self, event):
        self.tool_tip.show(event.screenPos(), self.get_tooltip_text(), False)
        delta_x = event.pos().x() - self._x
        if self._min_x <= self.x() + delta_x <= self._max_x:
            self.move(self.x() + delta_x)
            mod_tooltip_values = [0] + list(self.tooltip_values)
            if np.round(self.value, 1) in np.round(mod_tooltip_values, 1):
                index = np.where(np.round(mod_tooltip_values, 1) ==
                                 np.round(self.value, 1))
                time.sleep(0.05)
                self.move_to_val(mod_tooltip_values[index[0][0]])
        elif self.x() + delta_x < self._min_x:
            self.move(self._min_x)
        elif self.x() + delta_x > self._max_x:
            self.move(self._max_x)
        self._show_vertical_line_and_point_dot()
        self.probs_dot.move_to_sum()

    def mouseReleaseEvent(self, event):
        self.tool_tip.hide()
        self.setBrush(QColor(170, 220, 255, 255))
        self.point_dot.setVisible(False)
        self.vertical_line.setVisible(False)
        return super().mousePressEvent(event)

    def _show_vertical_line_and_point_dot(self):
        self.vertical_line.setX(self.x() + self.rect().width() / 2 - self._offset)
        self.vertical_line.setVisible(True)
        self.point_dot.move_to_val(self.value)
        self.point_dot.setVisible(True)


class DiscreteMovableDotItem(MovableDotItem):
    def get_tooltip_text(self):
        labels = self._get_tooltip_labels_with_percentages()
        return self.TOOLTIP_TEMPLATE.format(
            self.TOOLTIP_STYLE, "Points: {}".format(np.round(self.value, 2)),
            "".join("{}: {:.0%}<br/>".format(l, v) for l, v in labels)[:-5])

    def _get_tooltip_labels_with_percentages(self):
        if not len(self.tooltip_labels):
            return []
        for i, val in enumerate(self.tooltip_values):
            if val > self.value:
                break
        # pylint: disable=undefined-loop-variable
        diff = self.tooltip_values[i] - self.tooltip_values[i - 1]
        p1 = 0 if diff < 1e-6 else (-self.value + self.tooltip_values[i]) / diff
        return [(self.tooltip_labels[i - 1].replace("<", "&lt;"), abs(p1)),
                (self.tooltip_labels[i].replace("<", "&lt;"), abs(1 - p1))]


class GraphicsColorAnimator(QObject):
    @pyqtProperty(QColor)
    def brushColor(self):
        return self.__brushColor

    @brushColor.setter
    def brushColor(self, value):
        self.__brushColor = value
        for item in self.__items:
            item.setBrush(value)

    def __init__(self, parent, duration, keyValues):
        super().__init__(parent)
        self.__items = []
        self.__defaultColor = defaultColor = keyValues[0][1]
        self.__brushColor = defaultColor

        self.__animation = QPropertyAnimation(self, b'brushColor', self)
        self.__animation.setStartValue(defaultColor)
        self.__animation.setEndValue(defaultColor)
        self.__animation.setDuration(duration)
        self.__animation.setKeyValues(keyValues)
        self.__animation.setLoopCount(-1)

    def setGraphicsItems(self, items):
        if self.__animation.state() == QPropertyAnimation.Running:
            self.__animation.stop()
        self.__items = items
        for item in items:
            item.hookOnMousePress(self.stop)

    def start(self):
        self.__animation.start()

    def stop(self):
        if self.__animation.state() != QPropertyAnimation.Running:
            return
        self.__animation.stop()
        for item in self.__items:
            item.setBrush(self.__defaultColor)

    def clear(self):
        for item in self.__items:
            item.unhookOnMousePress()
        self.__items = []



class ContinuousItemMixin:
    def get_tooltip_text(self):
        return self.TOOLTIP_TEMPLATE.format(
            self.TOOLTIP_STYLE, "Points: {}".format(np.round(self.value, 2)),
            "Value: {}".format(np.round(self._get_tooltip_label_value(), 1)))

    def _get_tooltip_label_value(self):
        if not len(self.tooltip_labels):
            return self.value
        start = float(self.tooltip_labels[0])
        stop = float(self.tooltip_labels[-1])
        delta = (self.tooltip_values[-1] - self.tooltip_values[0])
        if not delta:
            return np.nan
        return start + self.value * (stop - start) / delta


class ContinuousMovableDotItem(MovableDotItem, ContinuousItemMixin):
    pass


class Continuous2DMovableDotItem(MovableDotItem, ContinuousItemMixin):
    def __init__(self, radius, scale, offset, min_x, max_x, min_y, max_y):
        super().__init__(radius, scale, offset, min_x, max_x)
        self._min_y = min_y
        self._max_y = max_y
        self._horizontal_line = None

    @property
    def horizontal_line(self):
        return self._horizontal_line

    @horizontal_line.setter
    def horizontal_line(self, line):
        line.setVisible(False)
        self._horizontal_line = line

    def move(self, x):
        super().move(x)
        diff_ = np.nan_to_num(self._max_x - self._min_x)
        k = (x - self._min_x) / diff_ if diff_ else 0
        self.setY(self._min_y - self.rect().width() / 2 + (self._max_y - self._min_y) * k)

    def mousePressEvent(self, event):
        self._show_horizontal_line()
        return super().mousePressEvent(event)

    def mouseMoveEvent(self, event):
        super().mouseMoveEvent(event)
        self._show_horizontal_line()

    def mouseReleaseEvent(self, event):
        self.horizontal_line.setVisible(False)
        return super().mouseReleaseEvent(event)

    def _show_horizontal_line(self):
        self.horizontal_line.setY(self.y() + self.rect().width() / 2 -
                                  abs(self._max_y - self._min_y) / 2)
        self.horizontal_line.setVisible(True)


class RulerItem(QGraphicsWidget):
    tick_height = 6
    tick_width = 0
    DOT_RADIUS = 12
    half_tick_height = 3
    bold_label = True
    DOT_ITEM_CLS = DotItem

    def __init__(self, name, values, scale, name_offset, offset, labels=None):
        super().__init__()

        # leading label
        font = name.document().defaultFont()
        if self.bold_label:
            font.setWeight(QFont.Bold)
        name.setFont(font)
        name.setPos(name_offset, -10)
        name.setParentItem(self)

        # prediction marker
        self.dot = self.DOT_ITEM_CLS(self.DOT_RADIUS, scale, offset, values[0],
                                     values[-1])
        self.dot.setParentItem(self)

        # pylint: disable=unused-variable
        # line
        line = QGraphicsLineItem(min(values) * scale + offset, 0,
                                 max(values) * scale + offset, 0,
                                 self)

        if labels is None:
            labels = [str(abs(v) if v == -0 else v) for v in values]

        old_x_tick = None
        shown_items = []
        w = QGraphicsSimpleTextItem(labels[0]).boundingRect().width()
        text_finish = values[0] * scale - w + offset - 10
        for i, (label, value) in enumerate(zip(labels, values)):
            text = QGraphicsSimpleTextItem(label)
            x_text = value * scale - text.boundingRect().width() / 2 + offset
            if text_finish > x_text - 10:
                y_text, y_tick = self.DOT_RADIUS * 0.7, 0
                text_finish = values[0] * scale + offset
            else:
                y_text = - text.boundingRect().height() - self.DOT_RADIUS * 0.7
                y_tick = - self.tick_height
                text_finish = x_text + text.boundingRect().width()
            text.setPos(x_text, y_text)
            if not collides(text, shown_items):
                text.setParentItem(self)
                shown_items.append(text)

            x_tick = value * scale - self.tick_width / 2 + offset
            tick = QGraphicsRectItem(
                x_tick, y_tick, self.tick_width, self.tick_height,
                self)
            tick.setBrush(QColor(Qt.black))

            if self.half_tick_height and i:
                x = x_tick - (x_tick - old_x_tick) / 2
                half_tick = QGraphicsLineItem(x, - self.half_tick_height, x, 0,
                                              self)
            old_x_tick = x_tick


class ProbabilitiesRulerItem(QGraphicsWidget):
    tick_height = 6
    DOT_RADIUS = 14
    y_diff = 4

    def __init__(self, name, values, scale, name_offset, offset, get_points,
                 title, get_probabilities):
        super().__init__()
        self.scale = scale
        self.offset = offset
        self.get_points = get_points
        self.min_val = min(values)
        self.max_val = max(values)

        # leading labels
        font = name.document().defaultFont()
        font.setWeight(QFont.Bold)
        name_total = QGraphicsTextItem("Total", self)
        name_total.setFont(font)
        name_total.setPos(name_offset, -25)
        name.setFont(font)
        name.setPos(name_offset, 10)
        name.setParentItem(self)

        # prediction marker
        self.dot = ProbabilitiesDotItem(
            self.DOT_RADIUS, scale, offset, values[0], values[-1],
            title, get_probabilities)
        self.dot.setPos(0, (- self.DOT_RADIUS + self.y_diff) / 2)
        self.dot.setParentItem(self)

        # pylint: disable=unused-variable
        # two lines
        t_line = QGraphicsLineItem(self.min_val * scale + offset, 0,
                                   self.max_val * scale + offset, 0,
                                   self)
        p_line = QGraphicsLineItem(self.min_val * scale + offset, self.y_diff,
                                   self.max_val * scale + offset, self.y_diff,
                                   self)

        # ticks and labels
        old_x_tick = values[0] * scale + offset
        for i, value in enumerate(values[1:]):
            x_tick = value * scale + offset
            x = x_tick - (x_tick - old_x_tick) / 2
            half_tick = QGraphicsLineItem(x, - self.tick_height / 2, x, 0,
                                          self)
            old_x_tick = x_tick
            if i == len(values) - 2:
                break
            text = QGraphicsTextItem(str(abs(value) if value == -0 else value),
                                     self)
            x_text = value * scale - text.boundingRect().width() / 2 + offset
            y_text = - text.boundingRect().height() - self.DOT_RADIUS * 0.7
            text.setPos(x_text, y_text)
            tick = QGraphicsLineItem(x_tick, -self.tick_height, x_tick, 0,
                                     self)

        self.prob_items = [
            (i / 10, QGraphicsTextItem(" " + str(i * 10) + " "),
             QGraphicsLineItem(0, 0, 0, 0)) for i in range(1, 10)]

    def rescale(self):
        shown_items = []
        for prob, text, tick in self.prob_items:
            pts = self.get_points(prob)
            x = pts * self.scale - text.boundingRect().width() / 2 + self.offset
            text.setPos(x, 10 + self.y_diff)
            x = pts * self.scale + self.offset
            tick.setLine(x, 0 + self.y_diff, x, self.tick_height + self.y_diff)
            text.setParentItem(None)
            tick.setParentItem(None)
            text.setVisible(False)
            tick.setVisible(False)
            if self.min_val < pts < self.max_val:
                tick.setParentItem(self)
                tick.setVisible(True)
                text.setParentItem(self)
                if not collides(text, shown_items):
                    text.setVisible(True)
                    shown_items.append(text)


class DiscreteFeatureItem(RulerItem):
    tick_height = 6
    tick_width = 2
    half_tick_height = 0
    bold_label = False
    DOT_ITEM_CLS = DiscreteMovableDotItem

    def __init__(self, name, variable, values, scale, name_offset, offset):
        labels = _get_labels(variable)
        indices = np.argsort(values)
        labels, values = labels[indices], values[indices]
        super().__init__(name, values, scale, name_offset, offset, labels)
        self.dot.tooltip_labels = labels
        self.dot.tooltip_values = values


class ContinuousFeatureItem(RulerItem):
    tick_height = 6
    tick_width = 2
    half_tick_height = 0
    bold_label = False
    DOT_ITEM_CLS = ContinuousMovableDotItem

    def __init__(self, name, variable, data_extremes, values, scale,
                 name_offset, offset):
        labels = _get_labels(variable, data_extremes, values)
        super().__init__(name, values, scale, name_offset, offset, labels)
        self.dot.tooltip_labels = labels
        self.dot.tooltip_values = values


@singledispatch
def _get_labels(*_):
    return []


@_get_labels.register(DiscreteVariable)
def _(var: DiscreteVariable, *_):
    return np.array(var.values)


@_get_labels.register(ContinuousVariable)
def _(_: ContinuousVariable, data_extremes: List, values: np.ndarray, *__):
    diff_ = np.nan_to_num(values[-1] - values[0])
    k = (data_extremes[1] - data_extremes[0]) / diff_ if diff_ else 0
    return [str(np.round(v * k + data_extremes[0], 1)) for v in values]


class ContinuousFeature2DItem(QGraphicsWidget):
    tick_height = 6
    tick_width = 2
    DOT_RADIUS = 12
    y_diff = 80
    n_tck = 4

    def __init__(self, name, _, data_extremes, values, scale, name_offset,
                 offset):
        super().__init__()
        data_start, data_stop = data_extremes[0], data_extremes[1]
        labels = [str(np.round(data_start + (data_stop - data_start) * i /
                               (self.n_tck - 1), 1)) for i in range(self.n_tck)]

        # leading label
        font = name.document().defaultFont()
        name.setFont(font)
        name.setPos(name_offset, -10)
        name.setParentItem(self)

        # labels
        ascending = data_start < data_stop
        y_start, y_stop = (self.y_diff, 0) if ascending else (0, self.y_diff)
        for i in range(self.n_tck):
            text = QGraphicsSimpleTextItem(labels[i], self)
            w = text.boundingRect().width()
            y = y_start + (y_stop - y_start) / (self.n_tck - 1) * i
            text.setPos(-5 - w, y - 8)
            tick = QGraphicsLineItem(-2, y, 2, y, self)

        # prediction marker
        self.dot = Continuous2DMovableDotItem(
            self.DOT_RADIUS, scale, offset, values[0], values[-1], y_start, y_stop)
        self.dot.tooltip_labels = labels
        self.dot.tooltip_values = values
        self.dot.setParentItem(self)
        h_line = QGraphicsLineItem(values[0] * scale + offset, self.y_diff / 2,
                                   values[-1] * scale + offset, self.y_diff / 2,
                                   self)
        pen = QPen(Qt.DashLine)
        pen.setBrush(QColor(Qt.red))
        h_line.setPen(pen)
        self.dot.horizontal_line = h_line

        # pylint: disable=unused-variable
        # line
        line = QGraphicsLineItem(values[0] * scale + offset, y_start,
                                 values[-1] * scale + offset, y_stop,
                                 self)

        # ticks
        for value in values:
            diff_ = np.nan_to_num(values[-1] - values[0])
            k = (value - values[0]) / diff_ if diff_ else 0
            y_tick = (y_stop - y_start) * k + y_start - self.tick_height / 2
            x_tick = value * scale - self.tick_width / 2 + offset
            tick = QGraphicsRectItem(
                x_tick, y_tick, self.tick_width, self.tick_height,
                self)
            tick.setBrush(QColor(Qt.black))

        # rect
        rect = QGraphicsRectItem(
            values[0] * scale + offset, -self.y_diff * 0.125,
            values[-1] * scale + offset, self.y_diff * 1.25,
            self)
        pen = QPen(Qt.DotLine)
        pen.setBrush(QColor(50, 150, 200, 255))
        rect.setPen(pen)
        self.setPreferredSize(self.preferredWidth(), self.y_diff * 1.5)


class NomogramItem(QGraphicsWidget):
    def __init__(self):
        super().__init__()
        self._items = []
        self.setLayout(QGraphicsLinearLayout(Qt.Vertical))

    def add_items(self, items):
        self._items = items
        for item in items:
            self.layout().addItem(item)


class OWNomogram(OWWidget):
    name = "Nomogram"
    description = " Nomograms for Visualization of Naive Bayesian" \
                  " and Logistic Regression Classifiers."
    icon = "icons/Nomogram.svg"
    priority = 2000
    keywords = []

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

    class Outputs:
        features = Output("Features", AttributeList)

    MAX_N_ATTRS = 1000
    POINT_SCALE = 0
    ALIGN_LEFT = 0
    ALIGN_ZERO = 1
    ACCEPTABLE = (NaiveBayesModel, LogisticRegressionClassifier)
    settingsHandler = ClassValuesContextHandler()
    target_class_index = ContextSetting(0)
    normalize_probabilities = Setting(False)
    scale = Setting(1)
    display_index = Setting(1)
    n_attributes = Setting(10)
    sort_index = Setting(SortBy.ABSOLUTE)
    cont_feature_dim_index = Setting(0)

    graph_name = "scene"

    class Error(OWWidget.Error):
        invalid_classifier = Msg("Nomogram accepts only Naive Bayes and "
                                 "Logistic Regression classifiers.")

    def __init__(self):
        super().__init__()
        self.instances = None
        self.domain = None
        self.data = None
        self.classifier = None
        self.align = OWNomogram.ALIGN_ZERO
        self.log_odds_ratios = []
        self.log_reg_coeffs = []
        self.log_reg_coeffs_orig = []
        self.log_reg_cont_data_extremes = []
        self.p = None
        self.b0 = None
        self.points = []
        self.feature_items = {}
        self.feature_marker_values = []
        self.scale_marker_values = lambda x: x
        self.nomogram_main = None
        self.vertical_line = None
        self.hidden_vertical_line = None
        self.old_target_class_index = self.target_class_index
        self.repaint = False

        self.info.set_input_summary(self.info.NoInput)

        # GUI
        box = gui.vBox(self.controlArea, "Target class")
        self.class_combo = gui.comboBox(
            box, self, "target_class_index", callback=self._class_combo_changed,
            contentsLength=12, searchable=True)
        self.norm_check = gui.checkBox(
            box, self, "normalize_probabilities", "Normalize probabilities",
            hidden=True, callback=self.update_scene,
            tooltip="For multiclass data 1 vs. all probabilities do not"
                    " sum to 1 and therefore could be normalized.")

        self.scale_radio = gui.radioButtons(
            self.controlArea, self, "scale", ["Point scale", "Log odds ratios"],
            box="Scale", callback=self.update_scene)

        box = gui.vBox(self.controlArea, "Display features")
        grid = QGridLayout()
        radio_group = gui.radioButtonsInBox(
            box, self, "display_index", [], orientation=grid,
            callback=self.update_scene)
        radio_all = gui.appendRadioButton(
            radio_group, "All", addToLayout=False)
        radio_best = gui.appendRadioButton(
            radio_group, "Best ranked:", addToLayout=False)
        spin_box = gui.hBox(None, margin=0)
        self.n_spin = gui.spin(
            spin_box, self, "n_attributes", 1, self.MAX_N_ATTRS, label=" ",
            controlWidth=60, callback=self._n_spin_changed)
        grid.addWidget(radio_all, 1, 1)
        grid.addWidget(radio_best, 2, 1)
        grid.addWidget(spin_box, 2, 2)

        self.sort_combo = gui.comboBox(
            box, self, "sort_index", label="Rank by:", items=SortBy.items(),
            orientation=Qt.Horizontal, callback=self.update_scene)

        self.cont_feature_dim_combo = gui.comboBox(
            box, self, "cont_feature_dim_index", label="Numeric features: ",
            items=["1D projection", "2D curve"], orientation=Qt.Horizontal,
            callback=self.update_scene)

        gui.rubber(self.controlArea)

        class _GraphicsView(QGraphicsView):
            def __init__(self, scene, parent, **kwargs):
                for k, v in dict(verticalScrollBarPolicy=Qt.ScrollBarAlwaysOff,
                                 horizontalScrollBarPolicy=Qt.ScrollBarAlwaysOff,
                                 viewportUpdateMode=QGraphicsView.BoundingRectViewportUpdate,
                                 renderHints=(QPainter.Antialiasing |
                                              QPainter.TextAntialiasing |
                                              QPainter.SmoothPixmapTransform),
                                 alignment=(Qt.AlignTop |
                                            Qt.AlignLeft),
                                 sizePolicy=QSizePolicy(QSizePolicy.MinimumExpanding,
                                                        QSizePolicy.MinimumExpanding)).items():
                    kwargs.setdefault(k, v)

                super().__init__(scene, parent, **kwargs)

        class GraphicsView(_GraphicsView):
            def __init__(self, scene, parent):
                super().__init__(scene, parent,
                                 verticalScrollBarPolicy=Qt.ScrollBarAlwaysOn,
                                 styleSheet='QGraphicsView {background: white}')
                self.viewport().setMinimumWidth(300)  # XXX: This prevents some tests failing
                self._is_resizing = False

            w = self

            def resizeEvent(self, resizeEvent):
                # Recompute main scene on window width change
                if resizeEvent.size().width() != resizeEvent.oldSize().width():
                    self._is_resizing = True
                    self.w.update_scene()
                    self._is_resizing = False
                return super().resizeEvent(resizeEvent)

            def is_resizing(self):
                return self._is_resizing

            def sizeHint(self):
                return QSize(400, 200)

        class FixedSizeGraphicsView(_GraphicsView):
            def __init__(self, scene, parent):
                super().__init__(scene, parent,
                                 sizePolicy=QSizePolicy(QSizePolicy.MinimumExpanding,
                                                        QSizePolicy.Minimum))

            def sizeHint(self):
                return QSize(400, 85)

        scene = self.scene = QGraphicsScene(self)

        top_view = self.top_view = FixedSizeGraphicsView(scene, self)
        mid_view = self.view = GraphicsView(scene, self)
        bottom_view = self.bottom_view = FixedSizeGraphicsView(scene, self)

        for view in (top_view, mid_view, bottom_view):
            self.mainArea.layout().addWidget(view)

        self.dot_animator = GraphicsColorAnimator(
            self, 3000,
            [
                (0.9, DOT_COLOR),
                (0.925, DOT_COLOR.lighter(115)),
                (0.95, DOT_COLOR),
                (0.975, DOT_COLOR.lighter(115)),
                (1.0, DOT_COLOR)
            ]
        )

    def _class_combo_changed(self):
        with np.errstate(invalid='ignore'):
            coeffs = [np.nan_to_num(p[self.target_class_index] /
                                    p[self.old_target_class_index])
                      for p in self.points]
        points = [p[self.old_target_class_index] for p in self.points]
        self.feature_marker_values = [
            self.get_points_from_coeffs(v, c, p) for (v, c, p) in
            zip(self.feature_marker_values, coeffs, points)]
        self.feature_marker_values = np.asarray(self.feature_marker_values)
        self.update_scene()
        self.old_target_class_index = self.target_class_index

    def _n_spin_changed(self):
        self.display_index = 1
        self.update_scene()

    def update_controls(self):
        self.class_combo.clear()
        self.norm_check.setHidden(True)
        self.cont_feature_dim_combo.setEnabled(True)
        if self.domain is not None:
            values = self.domain.class_vars[0].values
            if values:
                self.class_combo.addItems(values)
                self.target_class_index = 0
            if len(self.domain.attributes) > self.MAX_N_ATTRS:
                self.display_index = 1
            if len(self.domain.class_vars[0].values) > 2:
                self.norm_check.setHidden(False)
            if not self.domain.has_continuous_attributes():
                self.cont_feature_dim_combo.setEnabled(False)
                self.cont_feature_dim_index = 0
        model = self.sort_combo.model()
        item = model.item(SortBy.POSITIVE)
        item.setFlags(item.flags() | Qt.ItemIsEnabled)
        item = model.item(SortBy.NEGATIVE)
        item.setFlags(item.flags() | Qt.ItemIsEnabled)
        self.align = OWNomogram.ALIGN_ZERO
        if self.classifier and isinstance(self.classifier,
                                          LogisticRegressionClassifier):
            self.align = OWNomogram.ALIGN_LEFT

    @Inputs.data
    def set_data(self, data):
        self.instances = data
        summary = len(data) if data else self.info.NoInput
        details = format_summary_details(data) if data else ""
        self.info.set_input_summary(summary, details)
        self.feature_marker_values = []
        self.set_feature_marker_values()
        self.update_scene()

    @Inputs.classifier
    def set_classifier(self, classifier):
        self.closeContext()
        self.classifier = classifier
        self.Error.clear()
        if self.classifier and not isinstance(self.classifier, self.ACCEPTABLE):
            self.Error.invalid_classifier()
            self.classifier = None
        self.domain = self.classifier.domain if self.classifier else None
        self.data = None
        self.calculate_log_odds_ratios()
        self.calculate_log_reg_coefficients()
        self.update_controls()
        self.openContext(self.domain.class_var if self.domain is not None
                         else None)
        self.points = self.log_odds_ratios or self.log_reg_coeffs
        self.feature_marker_values = []
        self.old_target_class_index = self.target_class_index
        self.update_scene()

    def calculate_log_odds_ratios(self):
        self.log_odds_ratios = []
        self.p = None
        if self.classifier is None or self.domain is None:
            return
        if not isinstance(self.classifier, NaiveBayesModel):
            return

        log_cont_prob = self.classifier.log_cont_prob
        class_prob = self.classifier.class_prob
        for i in range(len(self.domain.attributes)):
            ca = np.exp(log_cont_prob[i]) * class_prob[:, None]
            _or = (ca / (1 - ca)) / (class_prob / (1 - class_prob))[:, None]
            self.log_odds_ratios.append(np.log(_or))
        self.p = class_prob

    def calculate_log_reg_coefficients(self):
        self.log_reg_coeffs = []
        self.log_reg_cont_data_extremes = []
        self.b0 = None
        if self.classifier is None or self.domain is None:
            return
        if not isinstance(self.classifier, LogisticRegressionClassifier):
            return

        self.domain = self.reconstruct_domain(self.classifier.original_domain,
                                              self.domain)
        self.data = self.classifier.original_data.transform(self.domain)
        attrs, ranges, start = self.domain.attributes, [], 0
        for attr in attrs:
            stop = start + len(attr.values) if attr.is_discrete else start + 1
            ranges.append(slice(start, stop))
            start = stop

        self.b0 = self.classifier.intercept
        coeffs = self.classifier.coefficients
        if len(self.domain.class_var.values) == 2:
            self.b0 = np.hstack((self.b0 * (-1), self.b0))
            coeffs = np.vstack((coeffs * (-1), coeffs))
        self.log_reg_coeffs = [coeffs[:, r] for r in ranges]
        self.log_reg_coeffs_orig = self.log_reg_coeffs.copy()

        min_values = nanmin(self.data.X, axis=0)
        max_values = nanmax(self.data.X, axis=0)

        for i, min_t, max_t in zip(range(len(self.log_reg_coeffs)),
                                   min_values, max_values):
            if self.log_reg_coeffs[i].shape[1] == 1:
                coef = self.log_reg_coeffs[i]
                self.log_reg_coeffs[i] = np.hstack((coef * min_t, coef * max_t))
                self.log_reg_cont_data_extremes.append(
                    [sorted([min_t, max_t], reverse=(c < 0)) for c in coef.flat])
            else:
                self.log_reg_cont_data_extremes.append([None])

    def update_scene(self):
        self.clear_scene()
        if self.domain is None or not len(self.points[0]):
            self.Outputs.features.send(None)
            return

        n_attrs = self.n_attributes if self.display_index else int(1e10)
        attr_inds, attributes = zip(*self.get_ordered_attributes()[:n_attrs])
        self.Outputs.features.send(AttributeList(attributes))

        name_items = [QGraphicsTextItem(attr.name) for attr in attributes]
        point_text = QGraphicsTextItem("Points")
        probs_text = QGraphicsTextItem("Probabilities (%)")
        all_items = name_items + [point_text, probs_text]
        name_offset = -max(t.boundingRect().width() for t in all_items) - 10
        w = self.view.viewport().rect().width()
        max_width = w + name_offset - 30

        points = [self.points[i][self.target_class_index]
                  for i in attr_inds]
        if self.align == OWNomogram.ALIGN_LEFT:
            points = [p - p.min() for p in points]
        max_ = np.nan_to_num(max(max(abs(p)) for p in points))
        d = 100 / max_ if max_ else 1
        minimums = [p[self.target_class_index].min() for p in self.points]
        if self.scale == OWNomogram.POINT_SCALE:
            points = [p * d for p in points]

            if self.align == OWNomogram.ALIGN_LEFT:
                self.scale_marker_values = lambda x: (x - minimums) * d
            else:
                self.scale_marker_values = lambda x: x * d
        else:
            if self.align == OWNomogram.ALIGN_LEFT:
                self.scale_marker_values = lambda x: x - minimums
            else:
                self.scale_marker_values = lambda x: x

        point_item, nomogram_head = self.create_main_nomogram(
            attributes, attr_inds,
            name_items, points, max_width, point_text, name_offset)
        probs_item, nomogram_foot = self.create_footer_nomogram(
            probs_text, d, minimums, max_width, name_offset)
        for item in self.feature_items.values():
            item.dot.point_dot = point_item.dot
            item.dot.probs_dot = probs_item.dot
            item.dot.vertical_line = self.hidden_vertical_line

        self.dot_animator.setGraphicsItems(
            [item.dot for item in self.feature_items.values()]
        )
        self.dot_animator.start()

        self.nomogram = nomogram = NomogramItem()
        nomogram.add_items([nomogram_head, self.nomogram_main, nomogram_foot])
        self.scene.addItem(nomogram)

        self.set_feature_marker_values()

        rect = QRectF(self.scene.itemsBoundingRect().x(),
                      self.scene.itemsBoundingRect().y(),
                      self.scene.itemsBoundingRect().width(),
                      self.nomogram.preferredSize().height()).adjusted(10, 0, 20, 0)
        self.scene.setSceneRect(rect)

        # Clip top and bottom (60 and 150) parts from the main view
        self.view.setSceneRect(rect.x(), rect.y() + 80, rect.width() - 10, rect.height() - 160)
        self.view.viewport().setMaximumHeight(rect.height() - 160)
        # Clip main part from top/bottom views
        # below point values are imprecise (less/more than required) but this
        # is not a problem due to clipped scene content still being drawn
        self.top_view.setSceneRect(rect.x(), rect.y() + 3, rect.width() - 10, 20)
        self.bottom_view.setSceneRect(rect.x(), rect.height() - 110, rect.width() - 10, 30)

    @staticmethod
    def _adjust_scale(attributes, points, max_width, diff, attr_inds,
                      log_reg_cont_data_extremes, cls_index):
        if not diff:
            return max_width

        def offset(name, point):
            text_ = QGraphicsTextItem(name).boundingRect()
            return scale * point + text_.width() / 2

        lr = log_reg_cont_data_extremes
        scale = max_width / diff
        names = list(chain.from_iterable(
            [_get_labels(a, lr and lr[i] and lr[i][0] and lr[i][cls_index],
                         OWNomogram.get_ruler_values(p.min(), p.max(),
                                                     scale * p.ptp(), False))
             for i, a, p in zip(attr_inds, attributes, points)]))
        points = list(chain.from_iterable(points))

        old_scale = scale + 1
        while old_scale > scale:
            old_scale = scale
            offsets = [offset(n, p) for n, p in zip(names, points)]
            most_right_name = names[np.argmax(offsets)]
            text = QGraphicsTextItem(most_right_name).boundingRect()
            scale = (max_width - text.width() / 2) / diff
        return scale

    def create_main_nomogram(self, attributes, attr_inds, name_items, points,
                             max_width, point_text, name_offset):
        cls_index = self.target_class_index
        min_p = min(p.min() for p in points)
        max_p = max(p.max() for p in points)
        values = self.get_ruler_values(min_p, max_p, max_width)
        min_p, max_p = min(values), max(values)
        diff_ = np.nan_to_num(max_p - min_p)
        scale_x = self._adjust_scale(
            attributes, points, max_width, diff_, attr_inds,
            self.log_reg_cont_data_extremes, cls_index
        )

        nomogram_header = NomogramItem()
        point_item = RulerItem(point_text, values, scale_x, name_offset,
                               - scale_x * min_p)
        point_item.setPreferredSize(point_item.preferredWidth(), 35)
        nomogram_header.add_items([point_item])

        self.nomogram_main = NomogramItem()
        cont_feature_item_class = ContinuousFeature2DItem if \
            self.cont_feature_dim_index else ContinuousFeatureItem

        feature_items = [
            DiscreteFeatureItem(
                name_item, attr, point,
                scale_x, name_offset, - scale_x * min_p)
            if attr.is_discrete else
            cont_feature_item_class(
                name_item, attr, self.log_reg_cont_data_extremes[i][cls_index],
                self.get_ruler_values(
                    point.min(), point.max(),
                    scale_x * point.ptp(), False),
                scale_x, name_offset, - scale_x * min_p)
            for i, attr, name_item, point in
            zip(attr_inds, attributes, name_items, points)]

        self.nomogram_main.add_items(feature_items)
        self.feature_items = OrderedDict(sorted(zip(attr_inds, feature_items)))

        x = - scale_x * min_p
        y = self.nomogram_main.layout().preferredHeight() + 10
        self.vertical_line = QGraphicsLineItem(x, -6, x, y)
        self.vertical_line.setPen(QPen(Qt.DotLine))
        self.vertical_line.setParentItem(point_item)
        self.hidden_vertical_line = QGraphicsLineItem(x, -6, x, y)
        pen = QPen(Qt.DashLine)
        pen.setBrush(QColor(Qt.red))
        self.hidden_vertical_line.setPen(pen)
        self.hidden_vertical_line.setParentItem(point_item)

        return point_item, nomogram_header

    def get_ordered_attributes(self):
        """Return (in_domain_index, attr) pairs, ordered by method in SortBy combo"""
        if self.domain is None or not self.domain.attributes:
            return []

        attrs = self.domain.attributes
        sort_by = self.sort_index
        class_value = self.target_class_index

        if sort_by == SortBy.NO_SORTING:
            return list(enumerate(attrs))

        elif sort_by == SortBy.NAME:

            def key(x):
                _, attr = x
                return attr.name.lower()

        elif sort_by == SortBy.ABSOLUTE:

            def key(x):
                i, attr = x
                if attr.is_discrete:
                    ptp = self.points[i][class_value].ptp()
                else:
                    coef = np.abs(self.log_reg_coeffs_orig[i][class_value]).mean()
                    ptp = coef * np.ptp(self.log_reg_cont_data_extremes[i][class_value])
                return -ptp

        elif sort_by == SortBy.POSITIVE:

            def key(x):
                i, attr = x
                max_value = (self.points[i][class_value].max()
                             if attr.is_discrete else
                             np.mean(self.log_reg_cont_data_extremes[i][class_value]))
                return -max_value

        elif sort_by == SortBy.NEGATIVE:

            def key(x):
                i, attr = x
                min_value = (self.points[i][class_value].min()
                             if attr.is_discrete else
                             np.mean(self.log_reg_cont_data_extremes[i][class_value]))
                return min_value

        return sorted(enumerate(attrs), key=key)


    def create_footer_nomogram(self, probs_text, d, minimums,
                               max_width, name_offset):
        # pylint: disable=invalid-unary-operand-type
        eps, d_ = 0.05, 1
        k = - np.log(self.p / (1 - self.p)) if self.p is not None else - self.b0
        min_sum = k[self.target_class_index] - np.log((1 - eps) / eps)
        max_sum = k[self.target_class_index] - np.log(eps / (1 - eps))
        if self.align == OWNomogram.ALIGN_LEFT:
            max_sum = max_sum - sum(minimums)
            min_sum = min_sum - sum(minimums)
            for i in range(len(k)):  # pylint: disable=consider-using-enumerate
                k[i] = k[i] - sum([min(q) for q in [p[i] for p in self.points]])
        if self.scale == OWNomogram.POINT_SCALE:
            min_sum *= d
            max_sum *= d
            d_ = d

        values = self.get_ruler_values(min_sum, max_sum, max_width)
        min_sum, max_sum = min(values), max(values)
        diff_ = np.nan_to_num(max_sum - min_sum)
        scale_x = max_width / diff_ if diff_ else max_width
        cls_var, cls_index = self.domain.class_var, self.target_class_index
        nomogram_footer = NomogramItem()

        def get_normalized_probabilities(val):
            if not self.normalize_probabilities:
                return 1 / (1 + np.exp(k[cls_index] - val / d_))
            totals = self.__get_totals_for_class_values(minimums)
            p_sum = np.sum(1 / (1 + np.exp(k - totals / d_)))
            return 1 / (1 + np.exp(k[cls_index] - val / d_)) / p_sum

        def get_points(prob):
            if not self.normalize_probabilities:
                return (k[cls_index] - np.log(1 / prob - 1)) * d_
            totals = self.__get_totals_for_class_values(minimums)
            p_sum = np.sum(1 / (1 + np.exp(k - totals / d_)))
            return (k[cls_index] - np.log(1 / (prob * p_sum) - 1)) * d_

        probs_item = ProbabilitiesRulerItem(
            probs_text, values, scale_x, name_offset, - scale_x * min_sum,
            get_points=get_points,
            title="{}='{}'".format(cls_var.name, cls_var.values[cls_index]),
            get_probabilities=get_normalized_probabilities)
        nomogram_footer.add_items([probs_item])
        return probs_item, nomogram_footer

    def __get_totals_for_class_values(self, minimums):
        cls_index = self.target_class_index
        marker_values = self.scale_marker_values(self.feature_marker_values)
        totals = np.full(len(self.domain.class_var.values), np.nan)
        totals[cls_index] = marker_values.sum()
        for i in range(len(self.domain.class_var.values)):
            if i == cls_index:
                continue
            coeffs = [np.nan_to_num(p[i] / p[cls_index]) for p in self.points]
            points = [p[cls_index] for p in self.points]
            total = sum([self.get_points_from_coeffs(v, c, p) for (v, c, p)
                         in zip(self.feature_marker_values, coeffs, points)])
            if self.align == OWNomogram.ALIGN_LEFT:
                points = [p - m for m, p in zip(minimums, points)]
                total -= sum([min(p) for p in [p[i] for p in self.points]])
            d = 100 / max(max(abs(p)) for p in points)
            if self.scale == OWNomogram.POINT_SCALE:
                total *= d
            totals[i] = total
        assert not np.any(np.isnan(totals))
        return totals

    def set_feature_marker_values(self):
        if not (len(self.points) and len(self.feature_items)):
            return
        if not len(self.feature_marker_values):
            self._init_feature_marker_values()
        marker_values = self.scale_marker_values(self.feature_marker_values)

        invisible_sum = 0
        for i, marker in enumerate(marker_values):
            try:
                item = self.feature_items[i]
            except KeyError:
                invisible_sum += marker
            else:
                item.dot.move_to_val(marker)

        item.dot.probs_dot.move_to_sum(invisible_sum)

    def _init_feature_marker_values(self):
        self.feature_marker_values = []
        cls_index = self.target_class_index
        instances = self.instances.transform(self.domain) \
            if self.instances else None
        values = []
        for i, attr in enumerate(self.domain.attributes):
            value, feature_val = 0, None
            if len(self.log_reg_coeffs):
                if attr.is_discrete:
                    ind, n = unique(self.data.X[:, i], return_counts=True)
                    feature_val = np.nan_to_num(ind[np.argmax(n)])
                else:
                    feature_val = nanmean(self.data.X[:, i])

            # If data is provided on a separate signal, use the first data
            # instance to position the points instead of the mean
            inst_in_dom = instances and attr in instances.domain
            if inst_in_dom and not np.isnan(instances[0][attr]):
                feature_val = instances[0][attr]

            if feature_val is not None:
                value = (self.points[i][cls_index][int(feature_val)]
                         if attr.is_discrete else
                         self.log_reg_coeffs_orig[i][cls_index][0] * feature_val)
            values.append(value)
        self.feature_marker_values = np.asarray(values)

    def clear_scene(self):
        self.feature_items = {}
        self.scale_marker_values = lambda x: x
        self.nomogram = None
        self.nomogram_main = None
        self.vertical_line = None
        self.hidden_vertical_line = None
        self.dot_animator.clear()
        self.scene.clear()

    def send_report(self):
        self.report_plot()

    @staticmethod
    def reconstruct_domain(original, preprocessed):
        # abuse dict to make "in" comparisons faster
        attrs = OrderedDict()
        for attr in preprocessed.attributes:
            cv = attr._compute_value.variable._compute_value
            var = cv.variable if cv else original[attr.name]
            if var in attrs:    # the reason for OrderedDict
                continue
            attrs[var] = None   # we only need keys
        attrs = list(attrs.keys())
        return Domain(attrs, original.class_var, original.metas)

    @staticmethod
    def get_ruler_values(start, stop, max_width, round_to_nearest=True):
        if max_width == 0:
            return [0]
        diff = np.nan_to_num((stop - start) / max_width)
        if diff <= 0:
            return [0]
        decimals = int(np.floor(np.log10(diff)))
        if diff > 4 * pow(10, decimals):
            step = 5 * pow(10, decimals + 2)
        elif diff > 2 * pow(10, decimals):
            step = 2 * pow(10, decimals + 2)
        elif diff > 1 * pow(10, decimals):
            step = 1 * pow(10, decimals + 2)
        else:
            step = 5 * pow(10, decimals + 1)
        round_by = int(- np.floor(np.log10(step)))
        r = start % step
        if not round_to_nearest:
            _range = np.arange(start + step, stop + r, step) - r
            start, stop = np.floor(start * 100) / 100, np.ceil(stop * 100) / 100
            return np.round(np.hstack((start, _range, stop)), 2)
        return np.round(np.arange(start, stop + r + step, step) - r, round_by)

    @staticmethod
    def get_points_from_coeffs(current_value, coefficients, possible_values):
        if np.isnan(possible_values).any():
            return 0
        # pylint: disable=undefined-loop-variable
        indices = np.argsort(possible_values)
        sorted_values = possible_values[indices]
        sorted_coefficients = coefficients[indices]
        for i, val in enumerate(sorted_values):
            if current_value < val:
                break
        diff = sorted_values[i] - sorted_values[i - 1]
        k = 0 if diff < 1e-6 else (sorted_values[i] - current_value) / \
                                  (sorted_values[i] - sorted_values[i - 1])
        return sorted_coefficients[i - 1] * sorted_values[i - 1] * k + \
               sorted_coefficients[i] * sorted_values[i] * (1 - k)

    def reset_settings(self):
        with warnings.catch_warnings():
            # setting target_class_index will trigger this innocent warning
            warnings.filterwarnings(
                "ignore", "combo box 'target_class_index' is empty")
            self._reset_settings()
        self.update_scene()


if __name__ == "__main__":  # pragma: no cover
    from Orange.classification import NaiveBayesLearner  #, LogisticRegressionLearner
    data = Table("heart_disease")
    clf = NaiveBayesLearner()(data)
    # clf = LogisticRegressionLearner()(data)
    WidgetPreview(OWNomogram).run(set_classifier=clf, set_data=data)

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