{
    "file_diffs": [
        {
            "old_file_content": "import math\nfrom itertools import chain\nimport numpy as np\n\nfrom AnyQt.QtWidgets import (\n    QGraphicsView, QGraphicsScene, QGraphicsItem, QGraphicsSimpleTextItem,\n    QGraphicsTextItem, QGraphicsItemGroup, QGraphicsLineItem,\n    QGraphicsPathItem, QGraphicsRectItem, QSizePolicy\n)\nfrom AnyQt.QtGui import QPen, QColor, QBrush, QPainterPath, QPainter, QFont\nfrom AnyQt.QtCore import Qt, QEvent, QRectF, QSize\n\nimport scipy.special\nfrom scipy.stats import f_oneway, chi2_contingency\n\nimport Orange.data\nfrom Orange.data.filter import FilterDiscrete, FilterContinuous, Values\nfrom Orange.statistics import contingency, distribution\n\nfrom Orange.widgets import widget, gui\nfrom Orange.widgets.settings import (Setting, DomainContextHandler,\n                                     ContextSetting)\nfrom Orange.widgets.utils.itemmodels import VariableListModel\nfrom Orange.widgets.utils.annotated_data import (create_annotated_table,\n                                                 ANNOTATED_DATA_SIGNAL_NAME)\nfrom Orange.widgets.utils.widgetpreview import WidgetPreview\nfrom Orange.widgets.widget import Input, Output\n\n\ndef compute_scale(min_, max_):\n    if min_ == max_:\n        return math.floor(min_), 1\n    magnitude = int(3 * math.log10(abs(max_ - min_)) + 1)\n    if magnitude % 3 == 0:\n        first_place = 1\n    elif magnitude % 3 == 1:\n        first_place = 2\n    else:\n        first_place = 5\n    magnitude = magnitude // 3 - 1\n    step = first_place * pow(10, magnitude)\n    first_val = math.ceil(min_ / step) * step\n    return first_val, step\n\n\ndef _quantiles(a, freq, q, interpolation=\"midpoint\"):\n    \"\"\"\n    Somewhat like np.quantiles, but with explicit sample frequencies.\n\n    * Only 'higher', 'lower' and 'midpoint' interpolation.\n    * `a` MUST be sorted.\n    \"\"\"\n    a = np.asarray(a)\n    freq = np.asarray(freq)\n    assert a.size > 0 and a.size == freq.size\n    cumdist = np.cumsum(freq)\n    cumdist /= cumdist[-1]\n\n    if interpolation == \"midpoint\":  # R quantile(..., type=2)\n        left = np.searchsorted(cumdist, q, side=\"left\")\n        right = np.searchsorted(cumdist, q, side=\"right\")\n        # no mid point for the right most position\n        np.clip(right, 0, a.size - 1, out=right)\n        # right and left will be different only on the `q` boundaries\n        # (excluding the right most sample)\n        return (a[left] + a[right]) / 2\n    elif interpolation == \"higher\":  # R quantile(... type=1)\n        right = np.searchsorted(cumdist, q, side=\"right\")\n        np.clip(right, 0, a.size - 1, out=right)\n        return a[right]\n    elif interpolation == \"lower\":\n        left = np.searchsorted(cumdist, q, side=\"left\")\n        return a[left]\n    else:  # pragma: no cover\n        raise ValueError(\"invalid interpolation: '{}'\".format(interpolation))\n\n\nclass BoxData:\n    def __init__(self, dist, attr, group_val_index=None, group_var=None):\n        self.dist = dist\n        self.n = n = np.sum(dist[1])\n        if n == 0:\n            return\n        self.a_min = float(dist[0, 0])\n        self.a_max = float(dist[0, -1])\n        self.mean = float(np.sum(dist[0] * dist[1]) / n)\n        self.var = float(np.sum(dist[1] * (dist[0] - self.mean) ** 2) / n)\n        self.dev = math.sqrt(self.var)\n        a, freq = np.asarray(dist)\n        q25, median, q75 = _quantiles(a, freq, [0.25, 0.5, 0.75])\n        self.median = median\n        # The code below omits the q25 or q75 in the plot when they are None\n        self.q25 = None if q25 == median else q25\n        self.q75 = None if q75 == median else q75\n        self.conditions = [FilterContinuous(attr, FilterContinuous.Between,\n                                            self.q25, self.q75)]\n        if group_val_index is not None:\n            self.conditions.append(FilterDiscrete(group_var, [group_val_index]))\n\n\nclass FilterGraphicsRectItem(QGraphicsRectItem):\n    def __init__(self, conditions, *args):\n        super().__init__(*args)\n        self.filter = Values(conditions) if conditions else None\n        self.setFlag(QGraphicsItem.ItemIsSelectable)\n\n\nclass OWBoxPlot(widget.OWWidget):\n    \"\"\"\n    Here's how the widget's functions call each other:\n\n    - `set_data` is a signal handler fills the list boxes and calls\n    `grouping_changed`.\n\n    - `grouping_changed` handles changes of grouping attribute: it enables or\n    disables the box for ordering, orders attributes and calls `attr_changed`.\n\n    - `attr_changed` handles changes of attribute. It recomputes box data by\n    calling `compute_box_data`, shows the appropriate display box\n    (discrete/continuous) and then calls`layout_changed`\n\n    - `layout_changed` constructs all the elements for the scene (as lists of\n    QGraphicsItemGroup) and calls `display_changed`. It is called when the\n    attribute or grouping is changed (by attr_changed) and on resize event.\n\n    - `display_changed` puts the elements corresponding to the current display\n    settings on the scene. It is called when the elements are reconstructed\n    (layout is changed due to selection of attributes or resize event), or\n    when the user changes display settings or colors.\n\n    For discrete attributes, the flow is a bit simpler: the elements are not\n    constructed in advance (by layout_changed). Instead, _display_changed_disc\n    draws everything.\n    \"\"\"\n    name = \"Box Plot\"\n    description = \"Visualize the distribution of feature values in a box plot.\"\n    icon = \"icons/BoxPlot.svg\"\n    priority = 100\n    keywords = [\"whisker\"]\n\n    class Inputs:\n        data = Input(\"Data\", Orange.data.Table)\n\n    class Outputs:\n        selected_data = Output(\"Selected Data\", Orange.data.Table, default=True)\n        annotated_data = Output(ANNOTATED_DATA_SIGNAL_NAME, Orange.data.Table)\n\n    #: Comparison types for continuous variables\n    CompareNone, CompareMedians, CompareMeans = 0, 1, 2\n\n    settingsHandler = DomainContextHandler()\n    conditions = ContextSetting([])\n\n    attribute = ContextSetting(None)\n    order_by_importance = Setting(False)\n    order_grouping_by_importance = Setting(False)\n    group_var = ContextSetting(None)\n    show_annotations = Setting(True)\n    compare = Setting(CompareMeans)\n    stattest = Setting(0)\n    sig_threshold = Setting(0.05)\n    stretched = Setting(True)\n    show_labels = Setting(True)\n    sort_freqs = Setting(False)\n\n    _sorting_criteria_attrs = {\n        CompareNone: \"\", CompareMedians: \"median\", CompareMeans: \"mean\"\n    }\n\n    _pen_axis_tick = QPen(Qt.white, 5)\n    _pen_axis = QPen(Qt.darkGray, 3)\n    _pen_median = QPen(QBrush(QColor(0xff, 0xff, 0x00)), 2)\n    _pen_paramet = QPen(QBrush(QColor(0x33, 0x00, 0xff)), 2)\n    _pen_dotted = QPen(QBrush(QColor(0x33, 0x00, 0xff)), 1)\n    _pen_dotted.setStyle(Qt.DotLine)\n    _post_line_pen = QPen(Qt.lightGray, 2)\n    _post_grp_pen = QPen(Qt.lightGray, 4)\n    for pen in (_pen_paramet, _pen_median, _pen_dotted,\n                _pen_axis, _pen_axis_tick, _post_line_pen, _post_grp_pen):\n        pen.setCosmetic(True)\n        pen.setCapStyle(Qt.RoundCap)\n        pen.setJoinStyle(Qt.RoundJoin)\n    _pen_axis_tick.setCapStyle(Qt.FlatCap)\n\n    _box_brush = QBrush(QColor(0x33, 0x88, 0xff, 0xc0))\n\n    _axis_font = QFont()\n    _axis_font.setPixelSize(12)\n    _label_font = QFont()\n    _label_font.setPixelSize(11)\n    _attr_brush = QBrush(QColor(0x33, 0x00, 0xff))\n\n    graph_name = \"box_scene\"\n\n    def __init__(self):\n        super().__init__()\n        self.stats = []\n        self.dataset = None\n        self.posthoc_lines = []\n\n        self.label_txts = self.mean_labels = self.boxes = self.labels = \\\n            self.label_txts_all = self.attr_labels = self.order = []\n        self.scale_x = 1\n        self.scene_min_x = self.scene_max_x = self.scene_width = 0\n        self.label_width = 0\n\n        self.attrs = VariableListModel()\n        view = gui.listView(\n            self.controlArea, self, \"attribute\", box=\"Variable\",\n            model=self.attrs, callback=self.attr_changed)\n        view.setMinimumSize(QSize(30, 30))\n        # Any other policy than Ignored will let the QListBox's scrollbar\n        # set the minimal height (see the penultimate paragraph of\n        # http://doc.qt.io/qt-4.8/qabstractscrollarea.html#addScrollBarWidget)\n        view.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Ignored)\n        gui.checkBox(\n            view.box, self, \"order_by_importance\",\n            \"Order by relevance to subgroups\",\n            tooltip=\"Order by 𝜒² or ANOVA over the subgroups\",\n            callback=self.apply_attr_sorting)\n        self.group_vars = VariableListModel(placeholder=\"None\")\n        view = gui.listView(\n            self.controlArea, self, \"group_var\", box=\"Subgroups\",\n            model=self.group_vars, callback=self.grouping_changed)\n        gui.checkBox(\n            view.box, self, \"order_grouping_by_importance\",\n            \"Order by relevance to variable\",\n            tooltip=\"Order by 𝜒² or ANOVA over the variable values\",\n            callback=self.on_group_sorting_checkbox)\n        view.setMinimumSize(QSize(30, 30))\n        # See the comment above\n        view.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Ignored)\n\n        # TODO: move Compare median/mean to grouping box\n        # The vertical size policy is needed to let only the list views expand\n        self.display_box = gui.vBox(\n            self.controlArea, \"Display\",\n            sizePolicy=(QSizePolicy.Minimum, QSizePolicy.Maximum),\n            addSpace=False)\n\n        gui.checkBox(self.display_box, self, \"show_annotations\", \"Annotate\",\n                     callback=self.display_changed)\n        self.compare_rb = gui.radioButtonsInBox(\n            self.display_box, self, 'compare',\n            btnLabels=[\"No comparison\", \"Compare medians\", \"Compare means\"],\n            callback=self.layout_changed)\n\n        # The vertical size policy is needed to let only the list views expand\n        self.stretching_box = box = gui.vBox(\n            self.controlArea, box=\"Display\",\n            sizePolicy=(QSizePolicy.Minimum, QSizePolicy.Fixed))\n        self.stretching_box.sizeHint = self.display_box.sizeHint\n        gui.checkBox(\n            box, self, 'stretched', \"Stretch bars\",\n            callback=self.display_changed)\n        gui.checkBox(\n            box, self, 'show_labels', \"Show box labels\",\n            callback=self.display_changed)\n        self.sort_cb = gui.checkBox(\n            box, self, 'sort_freqs', \"Sort by subgroup frequencies\",\n            callback=self.display_changed)\n\n        gui.vBox(self.mainArea, addSpace=True)\n        self.box_scene = QGraphicsScene()\n        self.box_scene.selectionChanged.connect(self.commit)\n        self.box_view = QGraphicsView(self.box_scene)\n        self.box_view.setRenderHints(QPainter.Antialiasing |\n                                     QPainter.TextAntialiasing |\n                                     QPainter.SmoothPixmapTransform)\n        self.box_view.viewport().installEventFilter(self)\n\n        self.mainArea.layout().addWidget(self.box_view)\n\n        gui.hBox(self.mainArea, addSpace=False)\n        self.stat_test = \"\"\n        self.mainArea.setMinimumWidth(300)\n\n        self.stats = self.dist = self.conts = []\n        self.is_continuous = False\n\n        self.update_display_box()\n\n    def sizeHint(self):\n        return QSize(900, 500)\n\n    def eventFilter(self, obj, event):\n        if obj is self.box_view.viewport() and \\\n                event.type() == QEvent.Resize:\n            self.layout_changed()\n\n        return super().eventFilter(obj, event)\n\n    @property\n    def show_stretched(self):\n        return self.stretched and self.group_var is not self.attribute\n\n    def reset_attrs(self):\n        domain = self.dataset.domain\n        self.attrs[:] = [\n            var for var in chain(\n                domain.class_vars, domain.metas, domain.attributes)\n            if var.is_primitive()]\n\n    def reset_groups(self):\n        domain = self.dataset.domain\n        self.group_vars[:] = [None] + [\n            var for var in chain(\n                domain.class_vars, domain.metas, domain.attributes)\n            if var.is_discrete]\n\n    # noinspection PyTypeChecker\n    @Inputs.data\n    def set_data(self, dataset):\n        if dataset is not None and (\n                not bool(dataset) or not len(dataset.domain) and not\n                any(var.is_primitive() for var in dataset.domain.metas)):\n            dataset = None\n        self.closeContext()\n        self.dataset = dataset\n        self.dist = self.stats = self.conts = []\n        self.group_var = None\n        self.attribute = None\n        if dataset:\n            self.reset_attrs()\n            self.reset_groups()\n            self.select_default_variables()\n            self.openContext(self.dataset)\n            self.grouping_changed()\n            self.attr_changed()\n        else:\n            self.reset_all_data()\n        self.commit()\n\n    def select_default_variables(self):\n        # visualize first non-class variable, group by class (if present)\n        domain = self.dataset.domain\n        if len(self.attrs) > len(domain.class_vars):\n            self.attribute = self.attrs[len(domain.class_vars)]\n        elif self.attrs:\n            self.attribute = self.attrs[0]\n\n        if domain.class_var and domain.class_var.is_discrete:\n            self.group_var = domain.class_var\n        else:\n            self.group_var = None  # Reset to trigger selection via callback\n\n    def apply_attr_sorting(self):\n        def compute_score(attr):\n            # This function and the one in apply_group_sorting are similar, but\n            # different in too many details, so they are kept as separate\n            # functions.\n            # If you discover a bug in this function, check the other one, too.\n            if attr is group_var:\n                return 3\n            if attr.is_continuous:\n                # One-way ANOVA\n                col = data.get_column_view(attr)[0].astype(float)\n                groups = (col[group_col == i] for i in range(n_groups))\n                groups = (col[~np.isnan(col)] for col in groups)\n                groups = [group for group in groups if len(group)]\n                p = f_oneway(*groups)[1] if len(groups) > 1 else 2\n            else:\n                p = self._chi_square(group_var, attr)[1]\n            if math.isnan(p):\n                return 2\n            return p\n\n        data = self.dataset\n        if data is None:\n            return\n        domain = data.domain\n        attribute = self.attribute\n        group_var = self.group_var\n        if self.order_by_importance and group_var is not None:\n            n_groups = len(group_var.values)\n            group_col = data.get_column_view(group_var)[0] if \\\n                domain.has_continuous_attributes(\n                    include_class=True, include_metas=True) else None\n            self.attrs.sort(key=compute_score)\n        else:\n            self.reset_attrs()\n        self.attribute = attribute  # reset selection\n        self._ensure_selection_visible(self.controls.attribute)\n\n    def on_group_sorting_checkbox(self):\n        if self.order_grouping_by_importance:\n            self.apply_group_sorting()\n        else:\n            self.reset_groups()\n            self.group_var = self.group_var  # reset selection\n            self._ensure_selection_visible(self.controls.group_var)\n\n    def apply_group_sorting(self):\n        def compute_stat(group):\n            # This function and the one in apply_attr_sorting are similar, but\n            # different in too many details, so they are kept as separate\n            # functions.\n            # If you discover a bug in this function, check the other one, too.\n            if group is attr:\n                return 3\n            if group is None:\n                return -1\n            if attr.is_continuous:\n                group_col = data.get_column_view(group)[0].astype(float)\n                groups = (attr_col[group_col == i]\n                          for i in range(len(group.values)))\n                groups = (col[~np.isnan(col)] for col in groups)\n                groups = [group for group in groups if len(group)]\n                p = f_oneway(*groups)[1] if len(groups) > 1 else 2\n            else:\n                p = self._chi_square(group, attr)[1]\n            if math.isnan(p):\n                return 2\n            return p\n\n        data = self.dataset\n        if data is None or not self.order_grouping_by_importance:\n            return\n        attr = self.attribute\n        group_var = self.group_var\n        if attr.is_continuous:\n            attr_col = data.get_column_view(attr)[0].astype(float)\n        self.group_vars.sort(key=compute_stat)\n        self.group_var = group_var  # reset selection\n        self._ensure_selection_visible(self.controls.group_var)\n\n    @staticmethod\n    def _ensure_selection_visible(view):\n        selection = view.selectedIndexes()\n        if len(selection) == 1:\n            view.scrollTo(selection[0])\n\n    def _chi_square(self, group_var, attr):\n        # Chi-square with the given distribution into groups\n        if not attr.values or not group_var.values:\n            return 0, 2, 0\n        observed = np.array(\n            contingency.get_contingency(self.dataset, group_var, attr))\n        observed = observed[observed.sum(axis=1) != 0, :]\n        observed = observed[:, observed.sum(axis=0) != 0]\n        if min(observed.shape) < 2:\n            return 0, 2, 0\n        return chi2_contingency(observed)[:3]\n\n    def reset_all_data(self):\n        self.clear_scene()\n        self.stat_test = \"\"\n        self.attrs[:] = []\n        self.group_vars[:] = [None]\n        self.is_continuous = False\n        self.update_display_box()\n\n    def grouping_changed(self):\n        self.controls.stretched.setDisabled(self.group_var is self.attribute)\n        self.apply_attr_sorting()\n        self.update_graph()\n\n    def select_box_items(self):\n        temp_cond = self.conditions.copy()\n        for box in self.box_scene.items():\n            if isinstance(box, FilterGraphicsRectItem):\n                box.setSelected(box.filter.conditions in\n                                [c.conditions for c in temp_cond])\n\n    def attr_changed(self):\n        self.controls.stretched.setDisabled(self.group_var is self.attribute)\n        self.apply_group_sorting()\n        self.update_graph()\n\n    def update_graph(self):\n        self.compute_box_data()\n        self.update_display_box()\n        self.layout_changed()\n\n        if self.is_continuous:\n            heights = 90 if self.show_annotations else 60\n            self.box_view.centerOn(self.scene_min_x + self.scene_width / 2,\n                                   -30 - len(self.stats) * heights / 2 + 45)\n        else:\n            self.box_view.centerOn(self.scene_width / 2,\n                                   -30 - len(self.boxes) * 40 / 2 + 45)\n\n    def compute_box_data(self):\n        attr = self.attribute\n        if not attr:\n            return\n        dataset = self.dataset\n        self.is_continuous = attr.is_continuous\n        if dataset is None or not self.is_continuous and not attr.values or \\\n                        self.group_var and not self.group_var.values:\n            self.stats = self.dist = self.conts = []\n            return\n        if self.group_var:\n            self.dist = []\n            self.conts = contingency.get_contingency(\n                dataset, attr, self.group_var)\n            if self.is_continuous:\n                stats, label_texts = [], []\n                for i, cont in enumerate(self.conts):\n                    if np.sum(cont[1]):\n                        stats.append(BoxData(cont, attr, i, self.group_var))\n                        label_texts.append(self.group_var.values[i])\n                self.stats = stats\n                self.label_txts_all = label_texts\n            else:\n                self.label_txts_all = \\\n                    [v for v, c in zip(\n                        self.group_var.values + [\"Missing values\"],\n                        self.conts.array_with_unknowns)\n                     if np.sum(c) > 0]\n        else:\n            self.dist = distribution.get_distribution(dataset, attr)\n            self.conts = []\n            if self.is_continuous:\n                self.stats = [BoxData(self.dist, attr, None)]\n            self.label_txts_all = [\"\"]\n        self.label_txts = [txts for stat, txts in zip(self.stats,\n                                                      self.label_txts_all)\n                           if stat.n > 0]\n        self.stats = [stat for stat in self.stats if stat.n > 0]\n\n    def update_display_box(self):\n        if self.is_continuous:\n            self.stretching_box.hide()\n            self.display_box.show()\n            self.compare_rb.setEnabled(self.group_var is not None)\n        else:\n            self.stretching_box.show()\n            self.display_box.hide()\n            self.sort_cb.setEnabled(self.group_var is not None)\n\n    def clear_scene(self):\n        self.closeContext()\n        self.box_scene.clearSelection()\n        self.box_scene.clear()\n        self.box_view.viewport().update()\n        self.attr_labels = []\n        self.labels = []\n        self.boxes = []\n        self.mean_labels = []\n        self.posthoc_lines = []\n        self.openContext(self.dataset)\n\n    def layout_changed(self):\n        attr = self.attribute\n        if not attr:\n            return\n        self.clear_scene()\n        if self.dataset is None or len(self.conts) == len(self.dist) == 0:\n            return\n\n        if self.is_continuous:\n            self.mean_labels = [self.mean_label(stat, attr, lab)\n                                for stat, lab in zip(self.stats, self.label_txts)]\n            self.draw_axis()\n            self.boxes = [self.box_group(stat) for stat in self.stats]\n            self.labels = [self.label_group(stat, attr, mean_lab)\n                           for stat, mean_lab in zip(self.stats, self.mean_labels)]\n            self.attr_labels = [QGraphicsSimpleTextItem(lab)\n                                for lab in self.label_txts]\n            for it in chain(self.labels, self.attr_labels):\n                self.box_scene.addItem(it)\n\n        self.display_changed()\n\n    def display_changed(self):\n        if self.dataset is None or self.attribute is None:\n            return\n\n        if self.is_continuous:\n            self._display_changed_cont()\n        else:\n            self._display_changed_disc()\n        self.draw_stat()\n        self.select_box_items()\n\n    def _display_changed_cont(self):\n        self.order = list(range(len(self.stats)))\n        criterion = self._sorting_criteria_attrs[self.compare]\n        if criterion:\n            vals = [getattr(stat, criterion) for stat in self.stats]\n            overmax = max((val for val in vals if val is not None), default=0) \\\n                      + 1\n            vals = [val if val is not None else overmax for val in vals]\n            self.order = sorted(self.order, key=vals.__getitem__)\n\n        heights = 90 if self.show_annotations else 60\n\n        for row, box_index in enumerate(self.order):\n            y = (-len(self.stats) + row) * heights + 10\n            for item in self.boxes[box_index]:\n                self.box_scene.addItem(item)\n                item.setY(y)\n            labels = self.labels[box_index]\n\n            if self.show_annotations:\n                labels.show()\n                labels.setY(y)\n            else:\n                labels.hide()\n\n            label = self.attr_labels[box_index]\n            label.setY(y - 15 - label.boundingRect().height())\n            if self.show_annotations:\n                label.hide()\n            else:\n                stat = self.stats[box_index]\n\n                if self.compare == OWBoxPlot.CompareMedians and \\\n                        stat.median is not None:\n                    pos = stat.median + 5 / self.scale_x\n                elif self.compare == OWBoxPlot.CompareMeans or stat.q25 is None:\n                    pos = stat.mean + 5 / self.scale_x\n                else:\n                    pos = stat.q25\n                label.setX(pos * self.scale_x)\n                label.show()\n\n        r = QRectF(self.scene_min_x, -30 - len(self.stats) * heights,\n                   self.scene_width, len(self.stats) * heights + 90)\n        self.box_scene.setSceneRect(r)\n\n        self._compute_tests_cont()\n        self._show_posthoc()\n\n    def _display_changed_disc(self):\n        self.clear_scene()\n        self.attr_labels = [QGraphicsSimpleTextItem(lab)\n                            for lab in self.label_txts_all]\n\n        if not self.show_stretched:\n            if self.group_var:\n                self.labels = [\n                    QGraphicsTextItem(\"{}\".format(int(sum(cont))))\n                    for cont in self.conts.array_with_unknowns\n                    if np.sum(cont) > 0]\n            else:\n                self.labels = [\n                    QGraphicsTextItem(str(int(sum(self.dist))))]\n\n        self.order = list(range(len(self.attr_labels)))\n\n        self.draw_axis_disc()\n        if self.group_var:\n            self.boxes = \\\n                [self.strudel(cont, i)\n                 for i, cont in enumerate(self.conts.array_with_unknowns)\n                 if np.sum(cont) > 0]\n\n            if self.sort_freqs:\n                # pylint: disable=invalid-unary-operand-type\n                self.order = sorted(\n                    self.order, key=(-np.sum(\n                        self.conts.array_with_unknowns, axis=1)).__getitem__)\n        else:\n            self.boxes = [self.strudel(self.dist.array_with_unknowns)]\n\n        for row, box_index in enumerate(self.order):\n            y = (-len(self.boxes) + row) * 40 + 10\n            box = self.boxes[box_index]\n            bars, labels = box[::2], box[1::2]\n\n            self.__draw_group_labels(y, box_index)\n            if not self.show_stretched:\n                self.__draw_row_counts(y, box_index)\n            if self.show_labels and self.attribute is not self.group_var:\n                self.__draw_bar_labels(y, bars, labels)\n            self.__draw_bars(y, bars)\n\n        self.box_scene.setSceneRect(-self.label_width - 5,\n                                    -30 - len(self.boxes) * 40,\n                                    self.scene_width, len(self.boxes * 40) + 90)\n        self._compute_tests_disc()\n\n    def __draw_group_labels(self, y, row):\n        \"\"\"Draw group labels\n\n        Parameters\n        ----------\n        y: int\n            vertical offset of bars\n        row: int\n            row index\n        \"\"\"\n        label = self.attr_labels[row]\n        b = label.boundingRect()\n        label.setPos(-b.width() - 10, y - b.height() / 2)\n        self.box_scene.addItem(label)\n\n    def __draw_row_counts(self, y, row):\n        \"\"\"Draw row counts\n\n        Parameters\n        ----------\n        y: int\n            vertical offset of bars\n        row: int\n            row index\n        \"\"\"\n        assert not self.is_continuous\n        label = self.labels[row]\n        b = label.boundingRect()\n        if self.group_var:\n            right = self.scale_x * sum(self.conts.array_with_unknowns[row])\n        else:\n            right = self.scale_x * sum(self.dist)\n        label.setPos(right + 10, y - b.height() / 2)\n        self.box_scene.addItem(label)\n\n    def __draw_bar_labels(self, y, bars, labels):\n        \"\"\"Draw bar labels\n\n        Parameters\n        ----------\n        y: int\n            vertical offset of bars\n        bars: List[FilterGraphicsRectItem]\n            list of bars being drawn\n        labels: List[QGraphicsTextItem]\n            list of labels for corresponding bars\n        \"\"\"\n        label = bar_part = None\n        for text_item, bar_part in zip(labels, bars):\n            label = self.Label(\n                text_item.toPlainText())\n            label.setPos(bar_part.boundingRect().x(),\n                         y - label.boundingRect().height() - 8)\n            label.setMaxWidth(bar_part.boundingRect().width())\n            self.box_scene.addItem(label)\n\n    def __draw_bars(self, y, bars):\n        \"\"\"Draw bars\n\n        Parameters\n        ----------\n        y: int\n            vertical offset of bars\n\n        bars: List[FilterGraphicsRectItem]\n            list of bars to draw\n        \"\"\"\n        for item in bars:\n            item.setPos(0, y)\n            self.box_scene.addItem(item)\n\n    # noinspection PyPep8Naming\n    def _compute_tests_cont(self):\n        # The t-test and ANOVA are implemented here since they efficiently use\n        # the widget-specific data in self.stats.\n        # The non-parametric tests can't do this, so we use statistics.tests\n\n        # pylint: disable=comparison-with-itself\n        def stat_ttest():\n            d1, d2 = self.stats\n            if d1.n < 2 or d2.n < 2:\n                return np.nan, np.nan\n            pooled_var = d1.var / d1.n + d2.var / d2.n\n            # pylint: disable=comparison-with-itself\n            if pooled_var == 0 or np.isnan(pooled_var):\n                return np.nan, np.nan\n            df = pooled_var ** 2 / \\\n                ((d1.var / d1.n) ** 2 / (d1.n - 1) +\n                 (d2.var / d2.n) ** 2 / (d2.n - 1))\n            t = abs(d1.mean - d2.mean) / math.sqrt(pooled_var)\n            p = 2 * (1 - scipy.special.stdtr(df, t))\n            return t, p\n\n        # TODO: Check this function\n        # noinspection PyPep8Naming\n        def stat_ANOVA():\n            if any(stat.n == 0 for stat in self.stats):\n                return np.nan, np.nan\n            n = sum(stat.n for stat in self.stats)\n            grand_avg = sum(stat.n * stat.mean for stat in self.stats) / n\n            var_between = sum(stat.n * (stat.mean - grand_avg) ** 2\n                              for stat in self.stats)\n            df_between = len(self.stats) - 1\n\n            var_within = sum(stat.n * stat.var for stat in self.stats)\n            df_within = n - len(self.stats)\n            if var_within == 0 or df_within == 0 or df_between == 0:\n                return np.nan, np.nan\n            F = (var_between / df_between) / (var_within / df_within)\n            p = 1 - scipy.special.fdtr(df_between, df_within, F)\n            return F, p\n\n        n = len(self.dataset)\n        if self.compare == OWBoxPlot.CompareNone or len(self.stats) < 2:\n            t = \"\"\n        elif any(s.n <= 1 for s in self.stats):\n            t = \"At least one group has just one instance, \" \\\n                \"cannot compute significance\"\n        elif len(self.stats) == 2:\n            if self.compare == OWBoxPlot.CompareMedians:\n                t = \"\"\n                # z, p = tests.wilcoxon_rank_sum(\n                #    self.stats[0].dist, self.stats[1].dist)\n                # t = \"Mann-Whitney's z: %.1f (p=%.3f)\" % (z, p)\n            else:\n                t, p = stat_ttest()\n                t = \"\" if np.isnan(t) else f\"Student's t: {t:.3f} (p={p:.3f}, N={n})\"\n        else:\n            if self.compare == OWBoxPlot.CompareMedians:\n                t = \"\"\n                # U, p = -1, -1\n                # t = \"Kruskal Wallis's U: %.1f (p=%.3f)\" % (U, p)\n            else:\n                F, p = stat_ANOVA()\n                t = \"\" if np.isnan(F) else f\"ANOVA: {F:.3f} (p={p:.3f}, N={n})\"\n        self.stat_test = t\n\n    def _compute_tests_disc(self):\n        if self.group_var is None or self.attribute is None:\n            self.stat_test = \"\"\n        else:\n            chi, p, dof = self._chi_square(self.group_var, self.attribute)\n            if np.isnan(p):\n                self.stat_test = \"\"\n            else:\n                self.stat_test = f\"χ²: {chi:.2f} (p={p:.3f}, dof={dof})\"\n\n    def mean_label(self, stat, attr, val_name):\n        label = QGraphicsItemGroup()\n        t = QGraphicsSimpleTextItem(attr.str_val(stat.mean), label)\n        t.setFont(self._label_font)\n        bbox = t.boundingRect()\n        w2, h = bbox.width() / 2, bbox.height()\n        t.setPos(-w2, -h)\n        tpm = QGraphicsSimpleTextItem(\n            \" \\u00b1 \" + \"%.*f\" % (attr.number_of_decimals + 1, stat.dev),\n            label)\n        tpm.setFont(self._label_font)\n        tpm.setPos(w2, -h)\n        if val_name:\n            vnm = QGraphicsSimpleTextItem(val_name + \": \", label)\n            vnm.setFont(self._label_font)\n            vnm.setBrush(self._attr_brush)\n            vb = vnm.boundingRect()\n            label.min_x = -w2 - vb.width()\n            vnm.setPos(label.min_x, -h)\n        else:\n            label.min_x = -w2\n        return label\n\n    def draw_axis(self):\n        \"\"\"Draw the horizontal axis and sets self.scale_x\"\"\"\n        misssing_stats = not self.stats\n        stats = self.stats or [BoxData(np.array([[0.], [1.]]), self.attribute)]\n        mean_labels = self.mean_labels or [self.mean_label(stats[0], self.attribute, \"\")]\n        bottom = min(stat.a_min for stat in stats)\n        top = max(stat.a_max for stat in stats)\n\n        first_val, step = compute_scale(bottom, top)\n        while bottom <= first_val:\n            first_val -= step\n        bottom = first_val\n        no_ticks = math.ceil((top - first_val) / step) + 1\n        top = max(top, first_val + no_ticks * step)\n\n        gbottom = min(bottom, min(stat.mean - stat.dev for stat in stats))\n        gtop = max(top, max(stat.mean + stat.dev for stat in stats))\n\n        bv = self.box_view\n        viewrect = bv.viewport().rect().adjusted(15, 15, -15, -30)\n        self.scale_x = scale_x = viewrect.width() / (gtop - gbottom)\n\n        # In principle we should repeat this until convergence since the new\n        # scaling is too conservative. (No chance am I doing this.)\n        mlb = min(stat.mean + mean_lab.min_x / scale_x\n                  for stat, mean_lab in zip(stats, mean_labels))\n        if mlb < gbottom:\n            gbottom = mlb\n            self.scale_x = scale_x = viewrect.width() / (gtop - gbottom)\n\n        self.scene_min_x = gbottom * scale_x\n        self.scene_max_x = gtop * scale_x\n        self.scene_width = self.scene_max_x - self.scene_min_x\n\n        val = first_val\n        last_text = self.scene_min_x\n        while True:\n            l = self.box_scene.addLine(val * scale_x, -1, val * scale_x, 1,\n                                       self._pen_axis_tick)\n            l.setZValue(100)\n            t = QGraphicsSimpleTextItem(\n                self.attribute.str_val(val) if not misssing_stats else \"?\")\n            t.setFont(self._axis_font)\n            t.setFlag(QGraphicsItem.ItemIgnoresTransformations)\n            r = t.boundingRect()\n            x_start = val * scale_x - r.width() / 2\n            x_finish = x_start + r.width()\n            if x_start > last_text + 10 and x_finish < self.scene_max_x:\n                t.setPos(x_start, 8)\n                self.box_scene.addItem(t)\n                last_text = x_finish\n            if val >= top:\n                break\n            val += step\n        self.box_scene.addLine(\n            bottom * scale_x - 4, 0, top * scale_x + 4, 0, self._pen_axis)\n\n    def draw_stat(self):\n        if self.stat_test:\n            label = QGraphicsSimpleTextItem(self.stat_test)\n            brect = self.box_scene.sceneRect()\n            label.setPos(brect.center().x() - label.boundingRect().width()/2,\n                         8 + self._axis_font.pixelSize()*2)\n            label.setFlag(QGraphicsItem.ItemIgnoresTransformations)\n            self.box_scene.addItem(label)\n\n    def draw_axis_disc(self):\n        \"\"\"\n        Draw the horizontal axis and sets self.scale_x for discrete attributes\n        \"\"\"\n        assert not self.is_continuous\n        if self.show_stretched:\n            if not self.attr_labels:\n                return\n            step = steps = 10\n        else:\n            if self.group_var:\n                max_box = max(float(np.sum(dist))\n                              for dist in self.conts.array_with_unknowns)\n            else:\n                max_box = float(np.sum(self.dist.array_with_unknowns))\n            if max_box == 0:\n                self.scale_x = 1\n                return\n            _, step = compute_scale(0, max_box)\n            step = int(step) if step > 1 else 1\n            steps = int(math.ceil(max_box / step))\n        max_box = step * steps\n\n        bv = self.box_view\n        viewrect = bv.viewport().rect().adjusted(15, 15, -15, -30)\n        self.scene_width = viewrect.width()\n\n        lab_width = max(lab.boundingRect().width() for lab in self.attr_labels)\n        lab_width = max(lab_width, 40)\n        lab_width = min(lab_width, self.scene_width / 3)\n        self.label_width = lab_width\n\n        right_offset = 0  # offset for the right label\n        if not self.show_stretched and self.labels:\n            if self.group_var:\n                rows = list(zip(self.conts.array_with_unknowns, self.labels))\n            else:\n                rows = [(self.dist, self.labels[0])]\n            # available space left of the 'group labels'\n            available = self.scene_width - lab_width - 10\n            scale_x = (available - right_offset) / max_box\n            max_right = max(sum(dist) * scale_x + 10 +\n                            lbl.boundingRect().width()\n                            for dist, lbl in rows)\n            right_offset = max(0, max_right - max_box * scale_x)\n\n        self.scale_x = scale_x = \\\n            (self.scene_width - lab_width - 10 - right_offset) / max_box\n\n        self.box_scene.addLine(0, 0, max_box * scale_x, 0, self._pen_axis)\n        for val in range(0, step * steps + 1, step):\n            l = self.box_scene.addLine(val * scale_x, -1, val * scale_x, 1,\n                                       self._pen_axis_tick)\n            l.setZValue(100)\n            t = self.box_scene.addSimpleText(str(val), self._axis_font)\n            t.setPos(val * scale_x - t.boundingRect().width() / 2, 8)\n        if self.show_stretched:\n            self.scale_x *= 100\n\n    def label_group(self, stat, attr, mean_lab):\n        def centered_text(val, pos):\n            t = QGraphicsSimpleTextItem(attr.str_val(val), labels)\n            t.setFont(self._label_font)\n            bbox = t.boundingRect()\n            t.setPos(pos - bbox.width() / 2, 22)\n            return t\n\n        def line(x, down=1):\n            QGraphicsLineItem(x, 12 * down, x, 20 * down, labels)\n\n        def move_label(label, frm, to):\n            label.setX(to)\n            to += t_box.width() / 2\n            path = QPainterPath()\n            path.lineTo(0, 4)\n            path.lineTo(to - frm, 4)\n            path.lineTo(to - frm, 8)\n            p = QGraphicsPathItem(path)\n            p.setPos(frm, 12)\n            labels.addToGroup(p)\n\n        labels = QGraphicsItemGroup()\n\n        labels.addToGroup(mean_lab)\n        m = stat.mean * self.scale_x\n        mean_lab.setPos(m, -22)\n        line(m, -1)\n\n        if stat.median is not None:\n            msc = stat.median * self.scale_x\n            med_t = centered_text(stat.median, msc)\n            med_box_width2 = med_t.boundingRect().width() / 2\n            line(msc)\n\n        if stat.q25 is not None:\n            x = stat.q25 * self.scale_x\n            t = centered_text(stat.q25, x)\n            t_box = t.boundingRect()\n            med_left = msc - med_box_width2\n            if x + t_box.width() / 2 >= med_left - 5:\n                move_label(t, x, med_left - t_box.width() - 5)\n            else:\n                line(x)\n\n        if stat.q75 is not None:\n            x = stat.q75 * self.scale_x\n            t = centered_text(stat.q75, x)\n            t_box = t.boundingRect()\n            med_right = msc + med_box_width2\n            if x - t_box.width() / 2 <= med_right + 5:\n                move_label(t, x, med_right + 5)\n            else:\n                line(x)\n\n        return labels\n\n    def box_group(self, stat, height=20):\n        def line(x0, y0, x1, y1, *args):\n            return QGraphicsLineItem(x0 * scale_x, y0, x1 * scale_x, y1, *args)\n\n        scale_x = self.scale_x\n        box = []\n        whisker1 = line(stat.a_min, -1.5, stat.a_min, 1.5)\n        whisker2 = line(stat.a_max, -1.5, stat.a_max, 1.5)\n        vert_line = line(stat.a_min, 0, stat.a_max, 0)\n        mean_line = line(stat.mean, -height / 3, stat.mean, height / 3)\n        for it in (whisker1, whisker2, mean_line):\n            it.setPen(self._pen_paramet)\n        vert_line.setPen(self._pen_dotted)\n        var_line = line(stat.mean - stat.dev, 0, stat.mean + stat.dev, 0)\n        var_line.setPen(self._pen_paramet)\n        box.extend([whisker1, whisker2, vert_line, mean_line, var_line])\n        if stat.q25 is not None or stat.q75 is not None:\n            # if any of them is None it means that its value is equal to median\n            box_from = stat.q25 or stat.median\n            box_to = stat.q75 or stat.median\n            mbox = FilterGraphicsRectItem(\n                stat.conditions, box_from * scale_x, -height / 2,\n                (box_to - box_from) * scale_x, height)\n            mbox.setBrush(self._box_brush)\n            mbox.setPen(QPen(Qt.NoPen))\n            mbox.setZValue(-200)\n            box.append(mbox)\n\n        if stat.median is not None:\n            median_line = line(stat.median, -height / 2,\n                               stat.median, height / 2)\n            median_line.setPen(self._pen_median)\n            median_line.setZValue(-150)\n            box.append(median_line)\n\n        return box\n\n    def strudel(self, dist, group_val_index=None):\n        attr = self.attribute\n        ss = np.sum(dist)\n        box = []\n        if ss < 1e-6:\n            cond = [FilterDiscrete(attr, None)]\n            if group_val_index is not None:\n                cond.append(FilterDiscrete(self.group_var, [group_val_index]))\n            box.append(FilterGraphicsRectItem(cond, 0, -10, 1, 10))\n        cum = 0\n        values = attr.values + [\"Missing values\"]\n        colors = np.vstack((attr.colors, [128, 128, 128]))\n        for i, v in enumerate(dist):\n            if v < 1e-6:\n                continue\n            if self.show_stretched:\n                v /= ss\n            v *= self.scale_x\n            cond = [FilterDiscrete(attr, [i])]\n            if group_val_index is not None:\n                cond.append(FilterDiscrete(self.group_var, [group_val_index]))\n            rect = FilterGraphicsRectItem(cond, cum + 1, -6, v - 2, 12)\n            rect.setBrush(QBrush(QColor(*colors[i])))\n            rect.setPen(QPen(Qt.NoPen))\n            if self.show_stretched:\n                tooltip = \"{}: {:.2f}%\".format(\n                    values[i],\n                    100 * dist[i] / sum(dist))\n            else:\n                tooltip = \"{}: {}\".format(values[i], int(dist[i]))\n            rect.setToolTip(tooltip)\n            text = QGraphicsTextItem(values[i])\n            box.append(rect)\n            box.append(text)\n            cum += v\n        return box\n\n    def commit(self):\n        self.conditions = [item.filter for item in\n                           self.box_scene.selectedItems() if item.filter]\n        selected, selection = None, []\n        if self.conditions:\n            selected = Values(self.conditions, conjunction=False)(self.dataset)\n            selection = np.in1d(\n                self.dataset.ids, selected.ids, assume_unique=True).nonzero()[0]\n        self.Outputs.selected_data.send(selected)\n        self.Outputs.annotated_data.send(\n            create_annotated_table(self.dataset, selection))\n\n    def _show_posthoc(self):\n        def line(y0, y1):\n            it = self.box_scene.addLine(x, y0, x, y1, self._post_line_pen)\n            it.setZValue(-100)\n            self.posthoc_lines.append(it)\n\n        while self.posthoc_lines:\n            self.box_scene.removeItem(self.posthoc_lines.pop())\n\n        if self.compare == OWBoxPlot.CompareNone or len(self.stats) < 2:\n            return\n\n        if self.compare == OWBoxPlot.CompareMedians:\n            crit_line = \"median\"\n        else:\n            crit_line = \"mean\"\n\n        xs = []\n\n        height = 90 if self.show_annotations else 60\n\n        y_up = -len(self.stats) * height + 10\n        for pos, box_index in enumerate(self.order):\n            stat = self.stats[box_index]\n            x = getattr(stat, crit_line)\n            if x is None:\n                continue\n            x *= self.scale_x\n            xs.append(x * self.scale_x)\n            by = y_up + pos * height\n            line(by + 12, 0)\n\n        used_to = []\n        last_to = to = 0\n        for frm, frm_x in enumerate(xs[:-1]):\n            for to in range(frm + 1, len(xs)):\n                if xs[to] - frm_x > 1.5:\n                    to -= 1\n                    break\n            if to in (last_to, frm):\n                continue\n            for rowi, used in enumerate(used_to):\n                if used < frm:\n                    used_to[rowi] = to\n                    break\n            else:\n                rowi = len(used_to)\n                used_to.append(to)\n            y = - 6 - rowi * 6\n            it = self.box_scene.addLine(frm_x - 2, y, xs[to] + 2, y,\n                                        self._post_grp_pen)\n            self.posthoc_lines.append(it)\n            last_to = to\n\n    def get_widget_name_extension(self):\n        return self.attribute.name if self.attribute else None\n\n    def send_report(self):\n        self.report_plot()\n        text = \"\"\n        if self.attribute:\n            text += \"Box plot for attribute '{}' \".format(self.attribute.name)\n        if self.group_var:\n            text += \"grouped by '{}'\".format(self.group_var.name)\n        if text:\n            self.report_caption(text)\n\n    class Label(QGraphicsSimpleTextItem):\n        \"\"\"Boxplot Label with settable maxWidth\"\"\"\n        # Minimum width to display label text\n        MIN_LABEL_WIDTH = 25\n\n        # padding bellow the text\n        PADDING = 3\n\n        __max_width = None\n\n        def maxWidth(self):\n            return self.__max_width\n\n        def setMaxWidth(self, max_width):\n            self.__max_width = max_width\n\n        def paint(self, painter, option, widget):\n            \"\"\"Overrides QGraphicsSimpleTextItem.paint\n\n            If label text is too long, it is elided\n            to fit into the allowed region\n            \"\"\"\n            if self.__max_width is None:\n                width = option.rect.width()\n            else:\n                width = self.__max_width\n\n            if width < self.MIN_LABEL_WIDTH:\n                # if space is too narrow, no label\n                return\n\n            fm = painter.fontMetrics()\n            text = fm.elidedText(self.text(), Qt.ElideRight, width)\n            painter.drawText(\n                option.rect.x(),\n                option.rect.y() + self.boundingRect().height() - self.PADDING,\n                text)\n\n\nif __name__ == \"__main__\":  # pragma: no cover\n    WidgetPreview(OWBoxPlot).run(Orange.data.Table(\"heart_disease.tab\"))\n",
            "new_file_content": "import math\nfrom itertools import chain\nimport numpy as np\n\nfrom AnyQt.QtWidgets import (\n    QGraphicsView, QGraphicsScene, QGraphicsItem, QGraphicsSimpleTextItem,\n    QGraphicsTextItem, QGraphicsItemGroup, QGraphicsLineItem,\n    QGraphicsPathItem, QGraphicsRectItem, QSizePolicy\n)\nfrom AnyQt.QtGui import QPen, QColor, QBrush, QPainterPath, QPainter, QFont\nfrom AnyQt.QtCore import Qt, QEvent, QRectF, QSize\n\nimport scipy.special\nfrom scipy.stats import f_oneway, chi2_contingency\n\nimport Orange.data\nfrom Orange.data.filter import FilterDiscrete, FilterContinuous, Values\nfrom Orange.statistics import contingency, distribution\n\nfrom Orange.widgets import widget, gui\nfrom Orange.widgets.settings import (Setting, DomainContextHandler,\n                                     ContextSetting)\nfrom Orange.widgets.utils.itemmodels import VariableListModel\nfrom Orange.widgets.utils.annotated_data import (create_annotated_table,\n                                                 ANNOTATED_DATA_SIGNAL_NAME)\nfrom Orange.widgets.utils.widgetpreview import WidgetPreview\nfrom Orange.widgets.widget import Input, Output\n\n\ndef compute_scale(min_, max_):\n    if min_ == max_:\n        return math.floor(min_), 1\n    magnitude = int(3 * math.log10(abs(max_ - min_)) + 1)\n    if magnitude % 3 == 0:\n        first_place = 1\n    elif magnitude % 3 == 1:\n        first_place = 2\n    else:\n        first_place = 5\n    magnitude = magnitude // 3 - 1\n    step = first_place * pow(10, magnitude)\n    first_val = math.ceil(min_ / step) * step\n    return first_val, step\n\n\ndef _quantiles(a, freq, q, interpolation=\"midpoint\"):\n    \"\"\"\n    Somewhat like np.quantiles, but with explicit sample frequencies.\n\n    * Only 'higher', 'lower' and 'midpoint' interpolation.\n    * `a` MUST be sorted.\n    \"\"\"\n    a = np.asarray(a)\n    freq = np.asarray(freq)\n    assert a.size > 0 and a.size == freq.size\n    cumdist = np.cumsum(freq)\n    cumdist /= cumdist[-1]\n\n    if interpolation == \"midpoint\":  # R quantile(..., type=2)\n        left = np.searchsorted(cumdist, q, side=\"left\")\n        right = np.searchsorted(cumdist, q, side=\"right\")\n        # no mid point for the right most position\n        np.clip(right, 0, a.size - 1, out=right)\n        # right and left will be different only on the `q` boundaries\n        # (excluding the right most sample)\n        return (a[left] + a[right]) / 2\n    elif interpolation == \"higher\":  # R quantile(... type=1)\n        right = np.searchsorted(cumdist, q, side=\"right\")\n        np.clip(right, 0, a.size - 1, out=right)\n        return a[right]\n    elif interpolation == \"lower\":\n        left = np.searchsorted(cumdist, q, side=\"left\")\n        return a[left]\n    else:  # pragma: no cover\n        raise ValueError(\"invalid interpolation: '{}'\".format(interpolation))\n\n\nclass BoxData:\n    def __init__(self, dist, attr, group_val_index=None, group_var=None):\n        self.dist = dist\n        self.n = n = np.sum(dist[1])\n        if n == 0:\n            return\n        self.a_min = float(dist[0, 0])\n        self.a_max = float(dist[0, -1])\n        self.mean = float(np.sum(dist[0] * dist[1]) / n)\n        self.var = float(np.sum(dist[1] * (dist[0] - self.mean) ** 2) / n)\n        self.dev = math.sqrt(self.var)\n        a, freq = np.asarray(dist)\n        q25, median, q75 = _quantiles(a, freq, [0.25, 0.5, 0.75])\n        self.median = median\n        # The code below omits the q25 or q75 in the plot when they are None\n        self.q25 = None if q25 == median else q25\n        self.q75 = None if q75 == median else q75\n        self.conditions = [FilterContinuous(attr, FilterContinuous.Between,\n                                            self.q25, self.q75)]\n        if group_val_index is not None:\n            self.conditions.append(FilterDiscrete(group_var, [group_val_index]))\n\n\nclass FilterGraphicsRectItem(QGraphicsRectItem):\n    def __init__(self, conditions, *args):\n        super().__init__(*args)\n        self.filter = Values(conditions) if conditions else None\n        self.setFlag(QGraphicsItem.ItemIsSelectable)\n\n\nclass OWBoxPlot(widget.OWWidget):\n    \"\"\"\n    Here's how the widget's functions call each other:\n\n    - `set_data` is a signal handler fills the list boxes and calls\n    `grouping_changed`.\n\n    - `grouping_changed` handles changes of grouping attribute: it enables or\n    disables the box for ordering, orders attributes and calls `attr_changed`.\n\n    - `attr_changed` handles changes of attribute. It recomputes box data by\n    calling `compute_box_data`, shows the appropriate display box\n    (discrete/continuous) and then calls`layout_changed`\n\n    - `layout_changed` constructs all the elements for the scene (as lists of\n    QGraphicsItemGroup) and calls `display_changed`. It is called when the\n    attribute or grouping is changed (by attr_changed) and on resize event.\n\n    - `display_changed` puts the elements corresponding to the current display\n    settings on the scene. It is called when the elements are reconstructed\n    (layout is changed due to selection of attributes or resize event), or\n    when the user changes display settings or colors.\n\n    For discrete attributes, the flow is a bit simpler: the elements are not\n    constructed in advance (by layout_changed). Instead, _display_changed_disc\n    draws everything.\n    \"\"\"\n    name = \"Box Plot\"\n    description = \"Visualize the distribution of feature values in a box plot.\"\n    icon = \"icons/BoxPlot.svg\"\n    priority = 100\n    keywords = [\"whisker\"]\n\n    class Inputs:\n        data = Input(\"Data\", Orange.data.Table)\n\n    class Outputs:\n        selected_data = Output(\"Selected Data\", Orange.data.Table, default=True)\n        annotated_data = Output(ANNOTATED_DATA_SIGNAL_NAME, Orange.data.Table)\n\n    #: Comparison types for continuous variables\n    CompareNone, CompareMedians, CompareMeans = 0, 1, 2\n\n    settingsHandler = DomainContextHandler()\n    conditions = ContextSetting([])\n\n    attribute = ContextSetting(None)\n    order_by_importance = Setting(False)\n    order_grouping_by_importance = Setting(False)\n    group_var = ContextSetting(None)\n    show_annotations = Setting(True)\n    compare = Setting(CompareMeans)\n    stattest = Setting(0)\n    sig_threshold = Setting(0.05)\n    stretched = Setting(True)\n    show_labels = Setting(True)\n    sort_freqs = Setting(False)\n\n    _sorting_criteria_attrs = {\n        CompareNone: \"\", CompareMedians: \"median\", CompareMeans: \"mean\"\n    }\n\n    _pen_axis_tick = QPen(Qt.white, 5)\n    _pen_axis = QPen(Qt.darkGray, 3)\n    _pen_median = QPen(QBrush(QColor(0xff, 0xff, 0x00)), 2)\n    _pen_paramet = QPen(QBrush(QColor(0x33, 0x00, 0xff)), 2)\n    _pen_dotted = QPen(QBrush(QColor(0x33, 0x00, 0xff)), 1)\n    _pen_dotted.setStyle(Qt.DotLine)\n    _post_line_pen = QPen(Qt.lightGray, 2)\n    _post_grp_pen = QPen(Qt.lightGray, 4)\n    for pen in (_pen_paramet, _pen_median, _pen_dotted,\n                _pen_axis, _pen_axis_tick, _post_line_pen, _post_grp_pen):\n        pen.setCosmetic(True)\n        pen.setCapStyle(Qt.RoundCap)\n        pen.setJoinStyle(Qt.RoundJoin)\n    _pen_axis_tick.setCapStyle(Qt.FlatCap)\n\n    _box_brush = QBrush(QColor(0x33, 0x88, 0xff, 0xc0))\n\n    _axis_font = QFont()\n    _axis_font.setPixelSize(12)\n    _label_font = QFont()\n    _label_font.setPixelSize(11)\n    _attr_brush = QBrush(QColor(0x33, 0x00, 0xff))\n\n    graph_name = \"box_scene\"\n\n    def __init__(self):\n        super().__init__()\n        self.stats = []\n        self.dataset = None\n        self.posthoc_lines = []\n\n        self.label_txts = self.mean_labels = self.boxes = self.labels = \\\n            self.label_txts_all = self.attr_labels = self.order = []\n        self.scale_x = 1\n        self.scene_min_x = self.scene_max_x = self.scene_width = 0\n        self.label_width = 0\n\n        self.attrs = VariableListModel()\n        view = gui.listView(\n            self.controlArea, self, \"attribute\", box=\"Variable\",\n            model=self.attrs, callback=self.attr_changed)\n        view.setMinimumSize(QSize(30, 30))\n        # Any other policy than Ignored will let the QListBox's scrollbar\n        # set the minimal height (see the penultimate paragraph of\n        # http://doc.qt.io/qt-4.8/qabstractscrollarea.html#addScrollBarWidget)\n        view.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Ignored)\n        gui.checkBox(\n            view.box, self, \"order_by_importance\",\n            \"Order by relevance to subgroups\",\n            tooltip=\"Order by 𝜒² or ANOVA over the subgroups\",\n            callback=self.apply_attr_sorting)\n        self.group_vars = VariableListModel(placeholder=\"None\")\n        view = gui.listView(\n            self.controlArea, self, \"group_var\", box=\"Subgroups\",\n            model=self.group_vars, callback=self.grouping_changed)\n        gui.checkBox(\n            view.box, self, \"order_grouping_by_importance\",\n            \"Order by relevance to variable\",\n            tooltip=\"Order by 𝜒² or ANOVA over the variable values\",\n            callback=self.on_group_sorting_checkbox)\n        view.setMinimumSize(QSize(30, 30))\n        # See the comment above\n        view.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Ignored)\n\n        # TODO: move Compare median/mean to grouping box\n        # The vertical size policy is needed to let only the list views expand\n        self.display_box = gui.vBox(\n            self.controlArea, \"Display\",\n            sizePolicy=(QSizePolicy.Minimum, QSizePolicy.Maximum),\n            addSpace=False)\n\n        gui.checkBox(self.display_box, self, \"show_annotations\", \"Annotate\",\n                     callback=self.display_changed)\n        self.compare_rb = gui.radioButtonsInBox(\n            self.display_box, self, 'compare',\n            btnLabels=[\"No comparison\", \"Compare medians\", \"Compare means\"],\n            callback=self.layout_changed)\n\n        # The vertical size policy is needed to let only the list views expand\n        self.stretching_box = box = gui.vBox(\n            self.controlArea, box=\"Display\",\n            sizePolicy=(QSizePolicy.Minimum, QSizePolicy.Fixed))\n        self.stretching_box.sizeHint = self.display_box.sizeHint\n        gui.checkBox(\n            box, self, 'stretched', \"Stretch bars\",\n            callback=self.display_changed)\n        gui.checkBox(\n            box, self, 'show_labels', \"Show box labels\",\n            callback=self.display_changed)\n        self.sort_cb = gui.checkBox(\n            box, self, 'sort_freqs', \"Sort by subgroup frequencies\",\n            callback=self.display_changed)\n\n        gui.vBox(self.mainArea, addSpace=True)\n        self.box_scene = QGraphicsScene()\n        self.box_scene.selectionChanged.connect(self.commit)\n        self.box_view = QGraphicsView(self.box_scene)\n        self.box_view.setRenderHints(QPainter.Antialiasing |\n                                     QPainter.TextAntialiasing |\n                                     QPainter.SmoothPixmapTransform)\n        self.box_view.viewport().installEventFilter(self)\n\n        self.mainArea.layout().addWidget(self.box_view)\n\n        gui.hBox(self.mainArea, addSpace=False)\n        self.stat_test = \"\"\n        self.mainArea.setMinimumWidth(300)\n\n        self.stats = self.dist = self.conts = []\n        self.is_continuous = False\n\n        self.update_display_box()\n\n    def sizeHint(self):\n        return QSize(900, 500)\n\n    def eventFilter(self, obj, event):\n        if obj is self.box_view.viewport() and \\\n                event.type() == QEvent.Resize:\n            self.layout_changed()\n\n        return super().eventFilter(obj, event)\n\n    @property\n    def show_stretched(self):\n        return self.stretched and self.group_var is not self.attribute\n\n    def reset_attrs(self):\n        domain = self.dataset.domain\n        self.attrs[:] = [\n            var for var in chain(\n                domain.class_vars, domain.metas, domain.attributes)\n            if var.is_primitive() and not var.attributes.get(\"hidden\", False)]\n\n    def reset_groups(self):\n        domain = self.dataset.domain\n        self.group_vars[:] = [None] + [\n            var for var in chain(\n                domain.class_vars, domain.metas, domain.attributes)\n            if var.is_discrete and not var.attributes.get(\"hidden\", False)]\n\n    # noinspection PyTypeChecker\n    @Inputs.data\n    def set_data(self, dataset):\n        if dataset is not None and (\n                not bool(dataset) or not len(dataset.domain) and not\n                any(var.is_primitive() for var in dataset.domain.metas)):\n            dataset = None\n        self.closeContext()\n        self.dataset = dataset\n        self.dist = self.stats = self.conts = []\n        self.group_var = None\n        self.attribute = None\n        if dataset:\n            self.reset_attrs()\n            self.reset_groups()\n            self.select_default_variables()\n            self.openContext(self.dataset)\n            self.grouping_changed()\n            self.attr_changed()\n        else:\n            self.reset_all_data()\n        self.commit()\n\n    def select_default_variables(self):\n        # visualize first non-class variable, group by class (if present)\n        domain = self.dataset.domain\n        if len(self.attrs) > len(domain.class_vars):\n            self.attribute = self.attrs[len(domain.class_vars)]\n        elif self.attrs:\n            self.attribute = self.attrs[0]\n\n        if domain.class_var and domain.class_var.is_discrete:\n            self.group_var = domain.class_var\n        else:\n            self.group_var = None  # Reset to trigger selection via callback\n\n    def apply_attr_sorting(self):\n        def compute_score(attr):\n            # This function and the one in apply_group_sorting are similar, but\n            # different in too many details, so they are kept as separate\n            # functions.\n            # If you discover a bug in this function, check the other one, too.\n            if attr is group_var:\n                return 3\n            if attr.is_continuous:\n                # One-way ANOVA\n                col = data.get_column_view(attr)[0].astype(float)\n                groups = (col[group_col == i] for i in range(n_groups))\n                groups = (col[~np.isnan(col)] for col in groups)\n                groups = [group for group in groups if len(group)]\n                p = f_oneway(*groups)[1] if len(groups) > 1 else 2\n            else:\n                p = self._chi_square(group_var, attr)[1]\n            if math.isnan(p):\n                return 2\n            return p\n\n        data = self.dataset\n        if data is None:\n            return\n        domain = data.domain\n        attribute = self.attribute\n        group_var = self.group_var\n        if self.order_by_importance and group_var is not None:\n            n_groups = len(group_var.values)\n            group_col = data.get_column_view(group_var)[0] if \\\n                domain.has_continuous_attributes(\n                    include_class=True, include_metas=True) else None\n            self.attrs.sort(key=compute_score)\n        else:\n            self.reset_attrs()\n        self.attribute = attribute  # reset selection\n        self._ensure_selection_visible(self.controls.attribute)\n\n    def on_group_sorting_checkbox(self):\n        if self.order_grouping_by_importance:\n            self.apply_group_sorting()\n        else:\n            self.reset_groups()\n            self.group_var = self.group_var  # reset selection\n            self._ensure_selection_visible(self.controls.group_var)\n\n    def apply_group_sorting(self):\n        def compute_stat(group):\n            # This function and the one in apply_attr_sorting are similar, but\n            # different in too many details, so they are kept as separate\n            # functions.\n            # If you discover a bug in this function, check the other one, too.\n            if group is attr:\n                return 3\n            if group is None:\n                return -1\n            if attr.is_continuous:\n                group_col = data.get_column_view(group)[0].astype(float)\n                groups = (attr_col[group_col == i]\n                          for i in range(len(group.values)))\n                groups = (col[~np.isnan(col)] for col in groups)\n                groups = [group for group in groups if len(group)]\n                p = f_oneway(*groups)[1] if len(groups) > 1 else 2\n            else:\n                p = self._chi_square(group, attr)[1]\n            if math.isnan(p):\n                return 2\n            return p\n\n        data = self.dataset\n        if data is None or not self.order_grouping_by_importance:\n            return\n        attr = self.attribute\n        group_var = self.group_var\n        if attr.is_continuous:\n            attr_col = data.get_column_view(attr)[0].astype(float)\n        self.group_vars.sort(key=compute_stat)\n        self.group_var = group_var  # reset selection\n        self._ensure_selection_visible(self.controls.group_var)\n\n    @staticmethod\n    def _ensure_selection_visible(view):\n        selection = view.selectedIndexes()\n        if len(selection) == 1:\n            view.scrollTo(selection[0])\n\n    def _chi_square(self, group_var, attr):\n        # Chi-square with the given distribution into groups\n        if not attr.values or not group_var.values:\n            return 0, 2, 0\n        observed = np.array(\n            contingency.get_contingency(self.dataset, group_var, attr))\n        observed = observed[observed.sum(axis=1) != 0, :]\n        observed = observed[:, observed.sum(axis=0) != 0]\n        if min(observed.shape) < 2:\n            return 0, 2, 0\n        return chi2_contingency(observed)[:3]\n\n    def reset_all_data(self):\n        self.clear_scene()\n        self.stat_test = \"\"\n        self.attrs[:] = []\n        self.group_vars[:] = [None]\n        self.is_continuous = False\n        self.update_display_box()\n\n    def grouping_changed(self):\n        self.controls.stretched.setDisabled(self.group_var is self.attribute)\n        self.apply_attr_sorting()\n        self.update_graph()\n\n    def select_box_items(self):\n        temp_cond = self.conditions.copy()\n        for box in self.box_scene.items():\n            if isinstance(box, FilterGraphicsRectItem):\n                box.setSelected(box.filter.conditions in\n                                [c.conditions for c in temp_cond])\n\n    def attr_changed(self):\n        self.controls.stretched.setDisabled(self.group_var is self.attribute)\n        self.apply_group_sorting()\n        self.update_graph()\n\n    def update_graph(self):\n        self.compute_box_data()\n        self.update_display_box()\n        self.layout_changed()\n\n        if self.is_continuous:\n            heights = 90 if self.show_annotations else 60\n            self.box_view.centerOn(self.scene_min_x + self.scene_width / 2,\n                                   -30 - len(self.stats) * heights / 2 + 45)\n        else:\n            self.box_view.centerOn(self.scene_width / 2,\n                                   -30 - len(self.boxes) * 40 / 2 + 45)\n\n    def compute_box_data(self):\n        attr = self.attribute\n        if not attr:\n            return\n        dataset = self.dataset\n        self.is_continuous = attr.is_continuous\n        if dataset is None or not self.is_continuous and not attr.values or \\\n                        self.group_var and not self.group_var.values:\n            self.stats = self.dist = self.conts = []\n            return\n        if self.group_var:\n            self.dist = []\n            self.conts = contingency.get_contingency(\n                dataset, attr, self.group_var)\n            if self.is_continuous:\n                stats, label_texts = [], []\n                for i, cont in enumerate(self.conts):\n                    if np.sum(cont[1]):\n                        stats.append(BoxData(cont, attr, i, self.group_var))\n                        label_texts.append(self.group_var.values[i])\n                self.stats = stats\n                self.label_txts_all = label_texts\n            else:\n                self.label_txts_all = \\\n                    [v for v, c in zip(\n                        self.group_var.values + [\"Missing values\"],\n                        self.conts.array_with_unknowns)\n                     if np.sum(c) > 0]\n        else:\n            self.dist = distribution.get_distribution(dataset, attr)\n            self.conts = []\n            if self.is_continuous:\n                self.stats = [BoxData(self.dist, attr, None)]\n            self.label_txts_all = [\"\"]\n        self.label_txts = [txts for stat, txts in zip(self.stats,\n                                                      self.label_txts_all)\n                           if stat.n > 0]\n        self.stats = [stat for stat in self.stats if stat.n > 0]\n\n    def update_display_box(self):\n        if self.is_continuous:\n            self.stretching_box.hide()\n            self.display_box.show()\n            self.compare_rb.setEnabled(self.group_var is not None)\n        else:\n            self.stretching_box.show()\n            self.display_box.hide()\n            self.sort_cb.setEnabled(self.group_var is not None)\n\n    def clear_scene(self):\n        self.closeContext()\n        self.box_scene.clearSelection()\n        self.box_scene.clear()\n        self.box_view.viewport().update()\n        self.attr_labels = []\n        self.labels = []\n        self.boxes = []\n        self.mean_labels = []\n        self.posthoc_lines = []\n        self.openContext(self.dataset)\n\n    def layout_changed(self):\n        attr = self.attribute\n        if not attr:\n            return\n        self.clear_scene()\n        if self.dataset is None or len(self.conts) == len(self.dist) == 0:\n            return\n\n        if self.is_continuous:\n            self.mean_labels = [self.mean_label(stat, attr, lab)\n                                for stat, lab in zip(self.stats, self.label_txts)]\n            self.draw_axis()\n            self.boxes = [self.box_group(stat) for stat in self.stats]\n            self.labels = [self.label_group(stat, attr, mean_lab)\n                           for stat, mean_lab in zip(self.stats, self.mean_labels)]\n            self.attr_labels = [QGraphicsSimpleTextItem(lab)\n                                for lab in self.label_txts]\n            for it in chain(self.labels, self.attr_labels):\n                self.box_scene.addItem(it)\n\n        self.display_changed()\n\n    def display_changed(self):\n        if self.dataset is None or self.attribute is None:\n            return\n\n        if self.is_continuous:\n            self._display_changed_cont()\n        else:\n            self._display_changed_disc()\n        self.draw_stat()\n        self.select_box_items()\n\n    def _display_changed_cont(self):\n        self.order = list(range(len(self.stats)))\n        criterion = self._sorting_criteria_attrs[self.compare]\n        if criterion:\n            vals = [getattr(stat, criterion) for stat in self.stats]\n            overmax = max((val for val in vals if val is not None), default=0) \\\n                      + 1\n            vals = [val if val is not None else overmax for val in vals]\n            self.order = sorted(self.order, key=vals.__getitem__)\n\n        heights = 90 if self.show_annotations else 60\n\n        for row, box_index in enumerate(self.order):\n            y = (-len(self.stats) + row) * heights + 10\n            for item in self.boxes[box_index]:\n                self.box_scene.addItem(item)\n                item.setY(y)\n            labels = self.labels[box_index]\n\n            if self.show_annotations:\n                labels.show()\n                labels.setY(y)\n            else:\n                labels.hide()\n\n            label = self.attr_labels[box_index]\n            label.setY(y - 15 - label.boundingRect().height())\n            if self.show_annotations:\n                label.hide()\n            else:\n                stat = self.stats[box_index]\n\n                if self.compare == OWBoxPlot.CompareMedians and \\\n                        stat.median is not None:\n                    pos = stat.median + 5 / self.scale_x\n                elif self.compare == OWBoxPlot.CompareMeans or stat.q25 is None:\n                    pos = stat.mean + 5 / self.scale_x\n                else:\n                    pos = stat.q25\n                label.setX(pos * self.scale_x)\n                label.show()\n\n        r = QRectF(self.scene_min_x, -30 - len(self.stats) * heights,\n                   self.scene_width, len(self.stats) * heights + 90)\n        self.box_scene.setSceneRect(r)\n\n        self._compute_tests_cont()\n        self._show_posthoc()\n\n    def _display_changed_disc(self):\n        self.clear_scene()\n        self.attr_labels = [QGraphicsSimpleTextItem(lab)\n                            for lab in self.label_txts_all]\n\n        if not self.show_stretched:\n            if self.group_var:\n                self.labels = [\n                    QGraphicsTextItem(\"{}\".format(int(sum(cont))))\n                    for cont in self.conts.array_with_unknowns\n                    if np.sum(cont) > 0]\n            else:\n                self.labels = [\n                    QGraphicsTextItem(str(int(sum(self.dist))))]\n\n        self.order = list(range(len(self.attr_labels)))\n\n        self.draw_axis_disc()\n        if self.group_var:\n            self.boxes = \\\n                [self.strudel(cont, i)\n                 for i, cont in enumerate(self.conts.array_with_unknowns)\n                 if np.sum(cont) > 0]\n\n            if self.sort_freqs:\n                # pylint: disable=invalid-unary-operand-type\n                self.order = sorted(\n                    self.order, key=(-np.sum(\n                        self.conts.array_with_unknowns, axis=1)).__getitem__)\n        else:\n            self.boxes = [self.strudel(self.dist.array_with_unknowns)]\n\n        for row, box_index in enumerate(self.order):\n            y = (-len(self.boxes) + row) * 40 + 10\n            box = self.boxes[box_index]\n            bars, labels = box[::2], box[1::2]\n\n            self.__draw_group_labels(y, box_index)\n            if not self.show_stretched:\n                self.__draw_row_counts(y, box_index)\n            if self.show_labels and self.attribute is not self.group_var:\n                self.__draw_bar_labels(y, bars, labels)\n            self.__draw_bars(y, bars)\n\n        self.box_scene.setSceneRect(-self.label_width - 5,\n                                    -30 - len(self.boxes) * 40,\n                                    self.scene_width, len(self.boxes * 40) + 90)\n        self._compute_tests_disc()\n\n    def __draw_group_labels(self, y, row):\n        \"\"\"Draw group labels\n\n        Parameters\n        ----------\n        y: int\n            vertical offset of bars\n        row: int\n            row index\n        \"\"\"\n        label = self.attr_labels[row]\n        b = label.boundingRect()\n        label.setPos(-b.width() - 10, y - b.height() / 2)\n        self.box_scene.addItem(label)\n\n    def __draw_row_counts(self, y, row):\n        \"\"\"Draw row counts\n\n        Parameters\n        ----------\n        y: int\n            vertical offset of bars\n        row: int\n            row index\n        \"\"\"\n        assert not self.is_continuous\n        label = self.labels[row]\n        b = label.boundingRect()\n        if self.group_var:\n            right = self.scale_x * sum(self.conts.array_with_unknowns[row])\n        else:\n            right = self.scale_x * sum(self.dist)\n        label.setPos(right + 10, y - b.height() / 2)\n        self.box_scene.addItem(label)\n\n    def __draw_bar_labels(self, y, bars, labels):\n        \"\"\"Draw bar labels\n\n        Parameters\n        ----------\n        y: int\n            vertical offset of bars\n        bars: List[FilterGraphicsRectItem]\n            list of bars being drawn\n        labels: List[QGraphicsTextItem]\n            list of labels for corresponding bars\n        \"\"\"\n        label = bar_part = None\n        for text_item, bar_part in zip(labels, bars):\n            label = self.Label(\n                text_item.toPlainText())\n            label.setPos(bar_part.boundingRect().x(),\n                         y - label.boundingRect().height() - 8)\n            label.setMaxWidth(bar_part.boundingRect().width())\n            self.box_scene.addItem(label)\n\n    def __draw_bars(self, y, bars):\n        \"\"\"Draw bars\n\n        Parameters\n        ----------\n        y: int\n            vertical offset of bars\n\n        bars: List[FilterGraphicsRectItem]\n            list of bars to draw\n        \"\"\"\n        for item in bars:\n            item.setPos(0, y)\n            self.box_scene.addItem(item)\n\n    # noinspection PyPep8Naming\n    def _compute_tests_cont(self):\n        # The t-test and ANOVA are implemented here since they efficiently use\n        # the widget-specific data in self.stats.\n        # The non-parametric tests can't do this, so we use statistics.tests\n\n        # pylint: disable=comparison-with-itself\n        def stat_ttest():\n            d1, d2 = self.stats\n            if d1.n < 2 or d2.n < 2:\n                return np.nan, np.nan\n            pooled_var = d1.var / d1.n + d2.var / d2.n\n            # pylint: disable=comparison-with-itself\n            if pooled_var == 0 or np.isnan(pooled_var):\n                return np.nan, np.nan\n            df = pooled_var ** 2 / \\\n                ((d1.var / d1.n) ** 2 / (d1.n - 1) +\n                 (d2.var / d2.n) ** 2 / (d2.n - 1))\n            t = abs(d1.mean - d2.mean) / math.sqrt(pooled_var)\n            p = 2 * (1 - scipy.special.stdtr(df, t))\n            return t, p\n\n        # TODO: Check this function\n        # noinspection PyPep8Naming\n        def stat_ANOVA():\n            if any(stat.n == 0 for stat in self.stats):\n                return np.nan, np.nan\n            n = sum(stat.n for stat in self.stats)\n            grand_avg = sum(stat.n * stat.mean for stat in self.stats) / n\n            var_between = sum(stat.n * (stat.mean - grand_avg) ** 2\n                              for stat in self.stats)\n            df_between = len(self.stats) - 1\n\n            var_within = sum(stat.n * stat.var for stat in self.stats)\n            df_within = n - len(self.stats)\n            if var_within == 0 or df_within == 0 or df_between == 0:\n                return np.nan, np.nan\n            F = (var_between / df_between) / (var_within / df_within)\n            p = 1 - scipy.special.fdtr(df_between, df_within, F)\n            return F, p\n\n        n = len(self.dataset)\n        if self.compare == OWBoxPlot.CompareNone or len(self.stats) < 2:\n            t = \"\"\n        elif any(s.n <= 1 for s in self.stats):\n            t = \"At least one group has just one instance, \" \\\n                \"cannot compute significance\"\n        elif len(self.stats) == 2:\n            if self.compare == OWBoxPlot.CompareMedians:\n                t = \"\"\n                # z, p = tests.wilcoxon_rank_sum(\n                #    self.stats[0].dist, self.stats[1].dist)\n                # t = \"Mann-Whitney's z: %.1f (p=%.3f)\" % (z, p)\n            else:\n                t, p = stat_ttest()\n                t = \"\" if np.isnan(t) else f\"Student's t: {t:.3f} (p={p:.3f}, N={n})\"\n        else:\n            if self.compare == OWBoxPlot.CompareMedians:\n                t = \"\"\n                # U, p = -1, -1\n                # t = \"Kruskal Wallis's U: %.1f (p=%.3f)\" % (U, p)\n            else:\n                F, p = stat_ANOVA()\n                t = \"\" if np.isnan(F) else f\"ANOVA: {F:.3f} (p={p:.3f}, N={n})\"\n        self.stat_test = t\n\n    def _compute_tests_disc(self):\n        if self.group_var is None or self.attribute is None:\n            self.stat_test = \"\"\n        else:\n            chi, p, dof = self._chi_square(self.group_var, self.attribute)\n            if np.isnan(p):\n                self.stat_test = \"\"\n            else:\n                self.stat_test = f\"χ²: {chi:.2f} (p={p:.3f}, dof={dof})\"\n\n    def mean_label(self, stat, attr, val_name):\n        label = QGraphicsItemGroup()\n        t = QGraphicsSimpleTextItem(attr.str_val(stat.mean), label)\n        t.setFont(self._label_font)\n        bbox = t.boundingRect()\n        w2, h = bbox.width() / 2, bbox.height()\n        t.setPos(-w2, -h)\n        tpm = QGraphicsSimpleTextItem(\n            \" \\u00b1 \" + \"%.*f\" % (attr.number_of_decimals + 1, stat.dev),\n            label)\n        tpm.setFont(self._label_font)\n        tpm.setPos(w2, -h)\n        if val_name:\n            vnm = QGraphicsSimpleTextItem(val_name + \": \", label)\n            vnm.setFont(self._label_font)\n            vnm.setBrush(self._attr_brush)\n            vb = vnm.boundingRect()\n            label.min_x = -w2 - vb.width()\n            vnm.setPos(label.min_x, -h)\n        else:\n            label.min_x = -w2\n        return label\n\n    def draw_axis(self):\n        \"\"\"Draw the horizontal axis and sets self.scale_x\"\"\"\n        misssing_stats = not self.stats\n        stats = self.stats or [BoxData(np.array([[0.], [1.]]), self.attribute)]\n        mean_labels = self.mean_labels or [self.mean_label(stats[0], self.attribute, \"\")]\n        bottom = min(stat.a_min for stat in stats)\n        top = max(stat.a_max for stat in stats)\n\n        first_val, step = compute_scale(bottom, top)\n        while bottom <= first_val:\n            first_val -= step\n        bottom = first_val\n        no_ticks = math.ceil((top - first_val) / step) + 1\n        top = max(top, first_val + no_ticks * step)\n\n        gbottom = min(bottom, min(stat.mean - stat.dev for stat in stats))\n        gtop = max(top, max(stat.mean + stat.dev for stat in stats))\n\n        bv = self.box_view\n        viewrect = bv.viewport().rect().adjusted(15, 15, -15, -30)\n        self.scale_x = scale_x = viewrect.width() / (gtop - gbottom)\n\n        # In principle we should repeat this until convergence since the new\n        # scaling is too conservative. (No chance am I doing this.)\n        mlb = min(stat.mean + mean_lab.min_x / scale_x\n                  for stat, mean_lab in zip(stats, mean_labels))\n        if mlb < gbottom:\n            gbottom = mlb\n            self.scale_x = scale_x = viewrect.width() / (gtop - gbottom)\n\n        self.scene_min_x = gbottom * scale_x\n        self.scene_max_x = gtop * scale_x\n        self.scene_width = self.scene_max_x - self.scene_min_x\n\n        val = first_val\n        last_text = self.scene_min_x\n        while True:\n            l = self.box_scene.addLine(val * scale_x, -1, val * scale_x, 1,\n                                       self._pen_axis_tick)\n            l.setZValue(100)\n            t = QGraphicsSimpleTextItem(\n                self.attribute.str_val(val) if not misssing_stats else \"?\")\n            t.setFont(self._axis_font)\n            t.setFlag(QGraphicsItem.ItemIgnoresTransformations)\n            r = t.boundingRect()\n            x_start = val * scale_x - r.width() / 2\n            x_finish = x_start + r.width()\n            if x_start > last_text + 10 and x_finish < self.scene_max_x:\n                t.setPos(x_start, 8)\n                self.box_scene.addItem(t)\n                last_text = x_finish\n            if val >= top:\n                break\n            val += step\n        self.box_scene.addLine(\n            bottom * scale_x - 4, 0, top * scale_x + 4, 0, self._pen_axis)\n\n    def draw_stat(self):\n        if self.stat_test:\n            label = QGraphicsSimpleTextItem(self.stat_test)\n            brect = self.box_scene.sceneRect()\n            label.setPos(brect.center().x() - label.boundingRect().width()/2,\n                         8 + self._axis_font.pixelSize()*2)\n            label.setFlag(QGraphicsItem.ItemIgnoresTransformations)\n            self.box_scene.addItem(label)\n\n    def draw_axis_disc(self):\n        \"\"\"\n        Draw the horizontal axis and sets self.scale_x for discrete attributes\n        \"\"\"\n        assert not self.is_continuous\n        if self.show_stretched:\n            if not self.attr_labels:\n                return\n            step = steps = 10\n        else:\n            if self.group_var:\n                max_box = max(float(np.sum(dist))\n                              for dist in self.conts.array_with_unknowns)\n            else:\n                max_box = float(np.sum(self.dist.array_with_unknowns))\n            if max_box == 0:\n                self.scale_x = 1\n                return\n            _, step = compute_scale(0, max_box)\n            step = int(step) if step > 1 else 1\n            steps = int(math.ceil(max_box / step))\n        max_box = step * steps\n\n        bv = self.box_view\n        viewrect = bv.viewport().rect().adjusted(15, 15, -15, -30)\n        self.scene_width = viewrect.width()\n\n        lab_width = max(lab.boundingRect().width() for lab in self.attr_labels)\n        lab_width = max(lab_width, 40)\n        lab_width = min(lab_width, self.scene_width / 3)\n        self.label_width = lab_width\n\n        right_offset = 0  # offset for the right label\n        if not self.show_stretched and self.labels:\n            if self.group_var:\n                rows = list(zip(self.conts.array_with_unknowns, self.labels))\n            else:\n                rows = [(self.dist, self.labels[0])]\n            # available space left of the 'group labels'\n            available = self.scene_width - lab_width - 10\n            scale_x = (available - right_offset) / max_box\n            max_right = max(sum(dist) * scale_x + 10 +\n                            lbl.boundingRect().width()\n                            for dist, lbl in rows)\n            right_offset = max(0, max_right - max_box * scale_x)\n\n        self.scale_x = scale_x = \\\n            (self.scene_width - lab_width - 10 - right_offset) / max_box\n\n        self.box_scene.addLine(0, 0, max_box * scale_x, 0, self._pen_axis)\n        for val in range(0, step * steps + 1, step):\n            l = self.box_scene.addLine(val * scale_x, -1, val * scale_x, 1,\n                                       self._pen_axis_tick)\n            l.setZValue(100)\n            t = self.box_scene.addSimpleText(str(val), self._axis_font)\n            t.setPos(val * scale_x - t.boundingRect().width() / 2, 8)\n        if self.show_stretched:\n            self.scale_x *= 100\n\n    def label_group(self, stat, attr, mean_lab):\n        def centered_text(val, pos):\n            t = QGraphicsSimpleTextItem(attr.str_val(val), labels)\n            t.setFont(self._label_font)\n            bbox = t.boundingRect()\n            t.setPos(pos - bbox.width() / 2, 22)\n            return t\n\n        def line(x, down=1):\n            QGraphicsLineItem(x, 12 * down, x, 20 * down, labels)\n\n        def move_label(label, frm, to):\n            label.setX(to)\n            to += t_box.width() / 2\n            path = QPainterPath()\n            path.lineTo(0, 4)\n            path.lineTo(to - frm, 4)\n            path.lineTo(to - frm, 8)\n            p = QGraphicsPathItem(path)\n            p.setPos(frm, 12)\n            labels.addToGroup(p)\n\n        labels = QGraphicsItemGroup()\n\n        labels.addToGroup(mean_lab)\n        m = stat.mean * self.scale_x\n        mean_lab.setPos(m, -22)\n        line(m, -1)\n\n        if stat.median is not None:\n            msc = stat.median * self.scale_x\n            med_t = centered_text(stat.median, msc)\n            med_box_width2 = med_t.boundingRect().width() / 2\n            line(msc)\n\n        if stat.q25 is not None:\n            x = stat.q25 * self.scale_x\n            t = centered_text(stat.q25, x)\n            t_box = t.boundingRect()\n            med_left = msc - med_box_width2\n            if x + t_box.width() / 2 >= med_left - 5:\n                move_label(t, x, med_left - t_box.width() - 5)\n            else:\n                line(x)\n\n        if stat.q75 is not None:\n            x = stat.q75 * self.scale_x\n            t = centered_text(stat.q75, x)\n            t_box = t.boundingRect()\n            med_right = msc + med_box_width2\n            if x - t_box.width() / 2 <= med_right + 5:\n                move_label(t, x, med_right + 5)\n            else:\n                line(x)\n\n        return labels\n\n    def box_group(self, stat, height=20):\n        def line(x0, y0, x1, y1, *args):\n            return QGraphicsLineItem(x0 * scale_x, y0, x1 * scale_x, y1, *args)\n\n        scale_x = self.scale_x\n        box = []\n        whisker1 = line(stat.a_min, -1.5, stat.a_min, 1.5)\n        whisker2 = line(stat.a_max, -1.5, stat.a_max, 1.5)\n        vert_line = line(stat.a_min, 0, stat.a_max, 0)\n        mean_line = line(stat.mean, -height / 3, stat.mean, height / 3)\n        for it in (whisker1, whisker2, mean_line):\n            it.setPen(self._pen_paramet)\n        vert_line.setPen(self._pen_dotted)\n        var_line = line(stat.mean - stat.dev, 0, stat.mean + stat.dev, 0)\n        var_line.setPen(self._pen_paramet)\n        box.extend([whisker1, whisker2, vert_line, mean_line, var_line])\n        if stat.q25 is not None or stat.q75 is not None:\n            # if any of them is None it means that its value is equal to median\n            box_from = stat.q25 or stat.median\n            box_to = stat.q75 or stat.median\n            mbox = FilterGraphicsRectItem(\n                stat.conditions, box_from * scale_x, -height / 2,\n                (box_to - box_from) * scale_x, height)\n            mbox.setBrush(self._box_brush)\n            mbox.setPen(QPen(Qt.NoPen))\n            mbox.setZValue(-200)\n            box.append(mbox)\n\n        if stat.median is not None:\n            median_line = line(stat.median, -height / 2,\n                               stat.median, height / 2)\n            median_line.setPen(self._pen_median)\n            median_line.setZValue(-150)\n            box.append(median_line)\n\n        return box\n\n    def strudel(self, dist, group_val_index=None):\n        attr = self.attribute\n        ss = np.sum(dist)\n        box = []\n        if ss < 1e-6:\n            cond = [FilterDiscrete(attr, None)]\n            if group_val_index is not None:\n                cond.append(FilterDiscrete(self.group_var, [group_val_index]))\n            box.append(FilterGraphicsRectItem(cond, 0, -10, 1, 10))\n        cum = 0\n        values = attr.values + [\"Missing values\"]\n        colors = np.vstack((attr.colors, [128, 128, 128]))\n        for i, v in enumerate(dist):\n            if v < 1e-6:\n                continue\n            if self.show_stretched:\n                v /= ss\n            v *= self.scale_x\n            cond = [FilterDiscrete(attr, [i])]\n            if group_val_index is not None:\n                cond.append(FilterDiscrete(self.group_var, [group_val_index]))\n            rect = FilterGraphicsRectItem(cond, cum + 1, -6, v - 2, 12)\n            rect.setBrush(QBrush(QColor(*colors[i])))\n            rect.setPen(QPen(Qt.NoPen))\n            if self.show_stretched:\n                tooltip = \"{}: {:.2f}%\".format(\n                    values[i],\n                    100 * dist[i] / sum(dist))\n            else:\n                tooltip = \"{}: {}\".format(values[i], int(dist[i]))\n            rect.setToolTip(tooltip)\n            text = QGraphicsTextItem(values[i])\n            box.append(rect)\n            box.append(text)\n            cum += v\n        return box\n\n    def commit(self):\n        self.conditions = [item.filter for item in\n                           self.box_scene.selectedItems() if item.filter]\n        selected, selection = None, []\n        if self.conditions:\n            selected = Values(self.conditions, conjunction=False)(self.dataset)\n            selection = np.in1d(\n                self.dataset.ids, selected.ids, assume_unique=True).nonzero()[0]\n        self.Outputs.selected_data.send(selected)\n        self.Outputs.annotated_data.send(\n            create_annotated_table(self.dataset, selection))\n\n    def _show_posthoc(self):\n        def line(y0, y1):\n            it = self.box_scene.addLine(x, y0, x, y1, self._post_line_pen)\n            it.setZValue(-100)\n            self.posthoc_lines.append(it)\n\n        while self.posthoc_lines:\n            self.box_scene.removeItem(self.posthoc_lines.pop())\n\n        if self.compare == OWBoxPlot.CompareNone or len(self.stats) < 2:\n            return\n\n        if self.compare == OWBoxPlot.CompareMedians:\n            crit_line = \"median\"\n        else:\n            crit_line = \"mean\"\n\n        xs = []\n\n        height = 90 if self.show_annotations else 60\n\n        y_up = -len(self.stats) * height + 10\n        for pos, box_index in enumerate(self.order):\n            stat = self.stats[box_index]\n            x = getattr(stat, crit_line)\n            if x is None:\n                continue\n            x *= self.scale_x\n            xs.append(x * self.scale_x)\n            by = y_up + pos * height\n            line(by + 12, 0)\n\n        used_to = []\n        last_to = to = 0\n        for frm, frm_x in enumerate(xs[:-1]):\n            for to in range(frm + 1, len(xs)):\n                if xs[to] - frm_x > 1.5:\n                    to -= 1\n                    break\n            if to in (last_to, frm):\n                continue\n            for rowi, used in enumerate(used_to):\n                if used < frm:\n                    used_to[rowi] = to\n                    break\n            else:\n                rowi = len(used_to)\n                used_to.append(to)\n            y = - 6 - rowi * 6\n            it = self.box_scene.addLine(frm_x - 2, y, xs[to] + 2, y,\n                                        self._post_grp_pen)\n            self.posthoc_lines.append(it)\n            last_to = to\n\n    def get_widget_name_extension(self):\n        return self.attribute.name if self.attribute else None\n\n    def send_report(self):\n        self.report_plot()\n        text = \"\"\n        if self.attribute:\n            text += \"Box plot for attribute '{}' \".format(self.attribute.name)\n        if self.group_var:\n            text += \"grouped by '{}'\".format(self.group_var.name)\n        if text:\n            self.report_caption(text)\n\n    class Label(QGraphicsSimpleTextItem):\n        \"\"\"Boxplot Label with settable maxWidth\"\"\"\n        # Minimum width to display label text\n        MIN_LABEL_WIDTH = 25\n\n        # padding bellow the text\n        PADDING = 3\n\n        __max_width = None\n\n        def maxWidth(self):\n            return self.__max_width\n\n        def setMaxWidth(self, max_width):\n            self.__max_width = max_width\n\n        def paint(self, painter, option, widget):\n            \"\"\"Overrides QGraphicsSimpleTextItem.paint\n\n            If label text is too long, it is elided\n            to fit into the allowed region\n            \"\"\"\n            if self.__max_width is None:\n                width = option.rect.width()\n            else:\n                width = self.__max_width\n\n            if width < self.MIN_LABEL_WIDTH:\n                # if space is too narrow, no label\n                return\n\n            fm = painter.fontMetrics()\n            text = fm.elidedText(self.text(), Qt.ElideRight, width)\n            painter.drawText(\n                option.rect.x(),\n                option.rect.y() + self.boundingRect().height() - self.PADDING,\n                text)\n\n\nif __name__ == \"__main__\":  # pragma: no cover\n    WidgetPreview(OWBoxPlot).run(Orange.data.Table(\"heart_disease.tab\"))\n",
            "header": {
                "file": {
                    "path": "Orange/widgets/visualize/owboxplot.py"
                },
                "misc_line": null
            },
            "index_line": {
                "old_commit_hash": "958cc8894",
                "new_commit_hash": "5811d0244",
                "mode": "100644"
            },
            "is_binary_file": false,
            "binary_line": null,
            "minus_file": {
                "path": "a/Orange/widgets/visualize/owboxplot.py"
            },
            "plus_file": {
                "path": "b/Orange/widgets/visualize/owboxplot.py"
            },
            "hunks": [
                {
                    "descriptor": {
                        "old_range": {
                            "start": 299,
                            "length": 14
                        },
                        "new_range": {
                            "start": 299,
                            "length": 14
                        },
                        "section": "class OWBoxPlot(widget.OWWidget):"
                    },
                    "line_group": {
                        "all_lines": [
                            {
                                "content": "        self.attrs[:] = [",
                                "type": "context"
                            },
                            {
                                "content": "            var for var in chain(",
                                "type": "context"
                            },
                            {
                                "content": "                domain.class_vars, domain.metas, domain.attributes)",
                                "type": "context"
                            },
                            {
                                "content": "            if var.is_primitive()]",
                                "type": "deleted"
                            },
                            {
                                "content": "            if var.is_primitive() and not var.attributes.get(\"hidden\", False)]",
                                "type": "added"
                            },
                            {
                                "content": "",
                                "type": "context"
                            },
                            {
                                "content": "    def reset_groups(self):",
                                "type": "context"
                            },
                            {
                                "content": "        domain = self.dataset.domain",
                                "type": "context"
                            },
                            {
                                "content": "        self.group_vars[:] = [None] + [",
                                "type": "context"
                            },
                            {
                                "content": "            var for var in chain(",
                                "type": "context"
                            },
                            {
                                "content": "                domain.class_vars, domain.metas, domain.attributes)",
                                "type": "context"
                            },
                            {
                                "content": "            if var.is_discrete]",
                                "type": "deleted"
                            },
                            {
                                "content": "            if var.is_discrete and not var.attributes.get(\"hidden\", False)]",
                                "type": "added"
                            },
                            {
                                "content": "",
                                "type": "context"
                            },
                            {
                                "content": "    # noinspection PyTypeChecker",
                                "type": "context"
                            },
                            {
                                "content": "    @Inputs.data",
                                "type": "context"
                            }
                        ]
                    },
                    "modified_entities": [
                        {
                            "file_name": "Orange/widgets/visualize/owboxplot.py",
                            "type": "method",
                            "name": "OWBoxPlot.reset_groups",
                            "content": "class OWBoxPlot(widget.OWWidget):\n    \"\"\"\n    Here's how the widget's functions call each other:\n\n    - `set_data` is a signal handler fills the list boxes and calls\n    `grouping_changed`.\n\n    - `grouping_changed` handles changes of grouping attribute: it enables or\n    disables the box for ordering, orders attributes and calls `attr_changed`.\n\n    - `attr_changed` handles changes of attribute. It recomputes box data by\n    calling `compute_box_data`, shows the appropriate display box\n    (discrete/continuous) and then calls`layout_changed`\n\n    - `layout_changed` constructs all the elements for the scene (as lists of\n    QGraphicsItemGroup) and calls `display_changed`. It is called when the\n    attribute or grouping is changed (by attr_changed) and on resize event.\n\n    - `display_changed` puts the elements corresponding to the current display\n    settings on the scene. It is called when the elements are reconstructed\n    (layout is changed due to selection of attributes or resize event), or\n    when the user changes display settings or colors.\n\n    For discrete attributes, the flow is a bit simpler: the elements are not\n    constructed in advance (by layout_changed). Instead, _display_changed_disc\n    draws everything.\n    \"\"\"\n    name = \"Box Plot\"\n    description = \"Visualize the distribution of feature values in a box plot.\"\n    icon = \"icons/BoxPlot.svg\"\n    priority = 100\n    keywords = [\"whisker\"]\n\n    class Inputs:\n        data = Input(\"Data\", Orange.data.Table)\n\n    class Outputs:\n        selected_data = Output(\"Selected Data\", Orange.data.Table, default=True)\n        annotated_data = Output(ANNOTATED_DATA_SIGNAL_NAME, Orange.data.Table)\n\n    #: Comparison types for continuous variables\n    CompareNone, CompareMedians, CompareMeans = 0, 1, 2\n\n    settingsHandler = DomainContextHandler()\n    conditions = ContextSetting([])\n\n    attribute = ContextSetting(None)\n    order_by_importance = Setting(False)\n    order_grouping_by_importance = Setting(False)\n    group_var = ContextSetting(None)\n    show_annotations = Setting(True)\n    compare = Setting(CompareMeans)\n    stattest = Setting(0)\n    sig_threshold = Setting(0.05)\n    stretched = Setting(True)\n    show_labels = Setting(True)\n    sort_freqs = Setting(False)\n\n    _sorting_criteria_attrs = {\n        CompareNone: \"\", CompareMedians: \"median\", CompareMeans: \"mean\"\n    }\n\n    _pen_axis_tick = QPen(Qt.white, 5)\n    _pen_axis = QPen(Qt.darkGray, 3)\n    _pen_median = QPen(QBrush(QColor(0xff, 0xff, 0x00)), 2)\n    _pen_paramet = QPen(QBrush(QColor(0x33, 0x00, 0xff)), 2)\n    _pen_dotted = QPen(QBrush(QColor(0x33, 0x00, 0xff)), 1)\n    _pen_dotted.setStyle(Qt.DotLine)\n    _post_line_pen = QPen(Qt.lightGray, 2)\n    _post_grp_pen = QPen(Qt.lightGray, 4)\n    for pen in (_pen_paramet, _pen_median, _pen_dotted,\n                _pen_axis, _pen_axis_tick, _post_line_pen, _post_grp_pen):\n        pen.setCosmetic(True)\n        pen.setCapStyle(Qt.RoundCap)\n        pen.setJoinStyle(Qt.RoundJoin)\n    _pen_axis_tick.setCapStyle(Qt.FlatCap)\n\n    _box_brush = QBrush(QColor(0x33, 0x88, 0xff, 0xc0))\n\n    _axis_font = QFont()\n    _axis_font.setPixelSize(12)\n    _label_font = QFont()\n    _label_font.setPixelSize(11)\n    _attr_brush = QBrush(QColor(0x33, 0x00, 0xff))\n\n    graph_name = \"box_scene\"\n\n    def __init__(self):\n        super().__init__()\n        self.stats = []\n        self.dataset = None\n        self.posthoc_lines = []\n\n        self.label_txts = self.mean_labels = self.boxes = self.labels = \\\n            self.label_txts_all = self.attr_labels = self.order = []\n        self.scale_x = 1\n        self.scene_min_x = self.scene_max_x = self.scene_width = 0\n        self.label_width = 0\n\n        self.attrs = VariableListModel()\n        view = gui.listView(\n            self.controlArea, self, \"attribute\", box=\"Variable\",\n            model=self.attrs, callback=self.attr_changed)\n        view.setMinimumSize(QSize(30, 30))\n        # Any other policy than Ignored will let the QListBox's scrollbar\n        # set the minimal height (see the penultimate paragraph of\n        # http://doc.qt.io/qt-4.8/qabstractscrollarea.html#addScrollBarWidget)\n        view.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Ignored)\n        gui.checkBox(\n            view.box, self, \"order_by_importance\",\n            \"Order by relevance to subgroups\",\n            tooltip=\"Order by 𝜒² or ANOVA over the subgroups\",\n            callback=self.apply_attr_sorting)\n        self.group_vars = VariableListModel(placeholder=\"None\")\n        view = gui.listView(\n            self.controlArea, self, \"group_var\", box=\"Subgroups\",\n            model=self.group_vars, callback=self.grouping_changed)\n        gui.checkBox(\n            view.box, self, \"order_grouping_by_importance\",\n            \"Order by relevance to variable\",\n            tooltip=\"Order by 𝜒² or ANOVA over the variable values\",\n            callback=self.on_group_sorting_checkbox)\n        view.setMinimumSize(QSize(30, 30))\n        # See the comment above\n        view.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Ignored)\n\n        # TODO: move Compare median/mean to grouping box\n        # The vertical size policy is needed to let only the list views expand\n        self.display_box = gui.vBox(\n            self.controlArea, \"Display\",\n            sizePolicy=(QSizePolicy.Minimum, QSizePolicy.Maximum),\n            addSpace=False)\n\n        gui.checkBox(self.display_box, self, \"show_annotations\", \"Annotate\",\n                     callback=self.display_changed)\n        self.compare_rb = gui.radioButtonsInBox(\n            self.display_box, self, 'compare',\n            btnLabels=[\"No comparison\", \"Compare medians\", \"Compare means\"],\n            callback=self.layout_changed)\n\n        # The vertical size policy is needed to let only the list views expand\n        self.stretching_box = box = gui.vBox(\n            self.controlArea, box=\"Display\",\n            sizePolicy=(QSizePolicy.Minimum, QSizePolicy.Fixed))\n        self.stretching_box.sizeHint = self.display_box.sizeHint\n        gui.checkBox(\n            box, self, 'stretched', \"Stretch bars\",\n            callback=self.display_changed)\n        gui.checkBox(\n            box, self, 'show_labels', \"Show box labels\",\n            callback=self.display_changed)\n        self.sort_cb = gui.checkBox(\n            box, self, 'sort_freqs', \"Sort by subgroup frequencies\",\n            callback=self.display_changed)\n\n        gui.vBox(self.mainArea, addSpace=True)\n        self.box_scene = QGraphicsScene()\n        self.box_scene.selectionChanged.connect(self.commit)\n        self.box_view = QGraphicsView(self.box_scene)\n        self.box_view.setRenderHints(QPainter.Antialiasing |\n                                     QPainter.TextAntialiasing |\n                                     QPainter.SmoothPixmapTransform)\n        self.box_view.viewport().installEventFilter(self)\n\n        self.mainArea.layout().addWidget(self.box_view)\n\n        gui.hBox(self.mainArea, addSpace=False)\n        self.stat_test = \"\"\n        self.mainArea.setMinimumWidth(300)\n\n        self.stats = self.dist = self.conts = []\n        self.is_continuous = False\n\n        self.update_display_box()\n\n    def sizeHint(self):\n        return QSize(900, 500)\n\n    def eventFilter(self, obj, event):\n        if obj is self.box_view.viewport() and \\\n                event.type() == QEvent.Resize:\n            self.layout_changed()\n\n        return super().eventFilter(obj, event)\n\n    @property\n    def show_stretched(self):\n        return self.stretched and self.group_var is not self.attribute\n\n    def reset_attrs(self):\n        domain = self.dataset.domain\n        self.attrs[:] = [\n            var for var in chain(\n                domain.class_vars, domain.metas, domain.attributes)\n            if var.is_primitive()]\n\n    def reset_groups(self):\n        domain = self.dataset.domain\n        self.group_vars[:] = [None] + [\n            var for var in chain(\n                domain.class_vars, domain.metas, domain.attributes)\n            if var.is_discrete]\n\n    # noinspection PyTypeChecker\n    @Inputs.data\n    def set_data(self, dataset):\n        if dataset is not None and (\n                not bool(dataset) or not len(dataset.domain) and not\n                any(var.is_primitive() for var in dataset.domain.metas)):\n            dataset = None\n        self.closeContext()\n        self.dataset = dataset\n        self.dist = self.stats = self.conts = []\n        self.group_var = None\n        self.attribute = None\n        if dataset:\n            self.reset_attrs()\n            self.reset_groups()\n            self.select_default_variables()\n            self.openContext(self.dataset)\n            self.grouping_changed()\n            self.attr_changed()\n        else:\n            self.reset_all_data()\n        self.commit()\n\n    def select_default_variables(self):\n        # visualize first non-class variable, group by class (if present)\n        domain = self.dataset.domain\n        if len(self.attrs) > len(domain.class_vars):\n            self.attribute = self.attrs[len(domain.class_vars)]\n        elif self.attrs:\n            self.attribute = self.attrs[0]\n\n        if domain.class_var and domain.class_var.is_discrete:\n            self.group_var = domain.class_var\n        else:\n            self.group_var = None  # Reset to trigger selection via callback\n\n    def apply_attr_sorting(self):\n        def compute_score(attr):\n            # This function and the one in apply_group_sorting are similar, but\n            # different in too many details, so they are kept as separate\n            # functions.\n            # If you discover a bug in this function, check the other one, too.\n            if attr is group_var:\n                return 3\n            if attr.is_continuous:\n                # One-way ANOVA\n                col = data.get_column_view(attr)[0].astype(float)\n                groups = (col[group_col == i] for i in range(n_groups))\n                groups = (col[~np.isnan(col)] for col in groups)\n                groups = [group for group in groups if len(group)]\n                p = f_oneway(*groups)[1] if len(groups) > 1 else 2\n            else:\n                p = self._chi_square(group_var, attr)[1]\n            if math.isnan(p):\n                return 2\n            return p\n\n        data = self.dataset\n        if data is None:\n            return\n        domain = data.domain\n        attribute = self.attribute\n        group_var = self.group_var\n        if self.order_by_importance and group_var is not None:\n            n_groups = len(group_var.values)\n            group_col = data.get_column_view(group_var)[0] if \\\n                domain.has_continuous_attributes(\n                    include_class=True, include_metas=True) else None\n            self.attrs.sort(key=compute_score)\n        else:\n            self.reset_attrs()\n        self.attribute = attribute  # reset selection\n        self._ensure_selection_visible(self.controls.attribute)\n\n    def on_group_sorting_checkbox(self):\n        if self.order_grouping_by_importance:\n            self.apply_group_sorting()\n        else:\n            self.reset_groups()\n            self.group_var = self.group_var  # reset selection\n            self._ensure_selection_visible(self.controls.group_var)\n\n    def apply_group_sorting(self):\n        def compute_stat(group):\n            # This function and the one in apply_attr_sorting are similar, but\n            # different in too many details, so they are kept as separate\n            # functions.\n            # If you discover a bug in this function, check the other one, too.\n            if group is attr:\n                return 3\n            if group is None:\n                return -1\n            if attr.is_continuous:\n                group_col = data.get_column_view(group)[0].astype(float)\n                groups = (attr_col[group_col == i]\n                          for i in range(len(group.values)))\n                groups = (col[~np.isnan(col)] for col in groups)\n                groups = [group for group in groups if len(group)]\n                p = f_oneway(*groups)[1] if len(groups) > 1 else 2\n            else:\n                p = self._chi_square(group, attr)[1]\n            if math.isnan(p):\n                return 2\n            return p\n\n        data = self.dataset\n        if data is None or not self.order_grouping_by_importance:\n            return\n        attr = self.attribute\n        group_var = self.group_var\n        if attr.is_continuous:\n            attr_col = data.get_column_view(attr)[0].astype(float)\n        self.group_vars.sort(key=compute_stat)\n        self.group_var = group_var  # reset selection\n        self._ensure_selection_visible(self.controls.group_var)\n\n    @staticmethod\n    def _ensure_selection_visible(view):\n        selection = view.selectedIndexes()\n        if len(selection) == 1:\n            view.scrollTo(selection[0])\n\n    def _chi_square(self, group_var, attr):\n        # Chi-square with the given distribution into groups\n        if not attr.values or not group_var.values:\n            return 0, 2, 0\n        observed = np.array(\n            contingency.get_contingency(self.dataset, group_var, attr))\n        observed = observed[observed.sum(axis=1) != 0, :]\n        observed = observed[:, observed.sum(axis=0) != 0]\n        if min(observed.shape) < 2:\n            return 0, 2, 0\n        return chi2_contingency(observed)[:3]\n\n    def reset_all_data(self):\n        self.clear_scene()\n        self.stat_test = \"\"\n        self.attrs[:] = []\n        self.group_vars[:] = [None]\n        self.is_continuous = False\n        self.update_display_box()\n\n    def grouping_changed(self):\n        self.controls.stretched.setDisabled(self.group_var is self.attribute)\n        self.apply_attr_sorting()\n        self.update_graph()\n\n    def select_box_items(self):\n        temp_cond = self.conditions.copy()\n        for box in self.box_scene.items():\n            if isinstance(box, FilterGraphicsRectItem):\n                box.setSelected(box.filter.conditions in\n                                [c.conditions for c in temp_cond])\n\n    def attr_changed(self):\n        self.controls.stretched.setDisabled(self.group_var is self.attribute)\n        self.apply_group_sorting()\n        self.update_graph()\n\n    def update_graph(self):\n        self.compute_box_data()\n        self.update_display_box()\n        self.layout_changed()\n\n        if self.is_continuous:\n            heights = 90 if self.show_annotations else 60\n            self.box_view.centerOn(self.scene_min_x + self.scene_width / 2,\n                                   -30 - len(self.stats) * heights / 2 + 45)\n        else:\n            self.box_view.centerOn(self.scene_width / 2,\n                                   -30 - len(self.boxes) * 40 / 2 + 45)\n\n    def compute_box_data(self):\n        attr = self.attribute\n        if not attr:\n            return\n        dataset = self.dataset\n        self.is_continuous = attr.is_continuous\n        if dataset is None or not self.is_continuous and not attr.values or \\\n                        self.group_var and not self.group_var.values:\n            self.stats = self.dist = self.conts = []\n            return\n        if self.group_var:\n            self.dist = []\n            self.conts = contingency.get_contingency(\n                dataset, attr, self.group_var)\n            if self.is_continuous:\n                stats, label_texts = [], []\n                for i, cont in enumerate(self.conts):\n                    if np.sum(cont[1]):\n                        stats.append(BoxData(cont, attr, i, self.group_var))\n                        label_texts.append(self.group_var.values[i])\n                self.stats = stats\n                self.label_txts_all = label_texts\n            else:\n                self.label_txts_all = \\\n                    [v for v, c in zip(\n                        self.group_var.values + [\"Missing values\"],\n                        self.conts.array_with_unknowns)\n                     if np.sum(c) > 0]\n        else:\n            self.dist = distribution.get_distribution(dataset, attr)\n            self.conts = []\n            if self.is_continuous:\n                self.stats = [BoxData(self.dist, attr, None)]\n            self.label_txts_all = [\"\"]\n        self.label_txts = [txts for stat, txts in zip(self.stats,\n                                                      self.label_txts_all)\n                           if stat.n > 0]\n        self.stats = [stat for stat in self.stats if stat.n > 0]\n\n    def update_display_box(self):\n        if self.is_continuous:\n            self.stretching_box.hide()\n            self.display_box.show()\n            self.compare_rb.setEnabled(self.group_var is not None)\n        else:\n            self.stretching_box.show()\n            self.display_box.hide()\n            self.sort_cb.setEnabled(self.group_var is not None)\n\n    def clear_scene(self):\n        self.closeContext()\n        self.box_scene.clearSelection()\n        self.box_scene.clear()\n        self.box_view.viewport().update()\n        self.attr_labels = []\n        self.labels = []\n        self.boxes = []\n        self.mean_labels = []\n        self.posthoc_lines = []\n        self.openContext(self.dataset)\n\n    def layout_changed(self):\n        attr = self.attribute\n        if not attr:\n            return\n        self.clear_scene()\n        if self.dataset is None or len(self.conts) == len(self.dist) == 0:\n            return\n\n        if self.is_continuous:\n            self.mean_labels = [self.mean_label(stat, attr, lab)\n                                for stat, lab in zip(self.stats, self.label_txts)]\n            self.draw_axis()\n            self.boxes = [self.box_group(stat) for stat in self.stats]\n            self.labels = [self.label_group(stat, attr, mean_lab)\n                           for stat, mean_lab in zip(self.stats, self.mean_labels)]\n            self.attr_labels = [QGraphicsSimpleTextItem(lab)\n                                for lab in self.label_txts]\n            for it in chain(self.labels, self.attr_labels):\n                self.box_scene.addItem(it)\n\n        self.display_changed()\n\n    def display_changed(self):\n        if self.dataset is None or self.attribute is None:\n            return\n\n        if self.is_continuous:\n            self._display_changed_cont()\n        else:\n            self._display_changed_disc()\n        self.draw_stat()\n        self.select_box_items()\n\n    def _display_changed_cont(self):\n        self.order = list(range(len(self.stats)))\n        criterion = self._sorting_criteria_attrs[self.compare]\n        if criterion:\n            vals = [getattr(stat, criterion) for stat in self.stats]\n            overmax = max((val for val in vals if val is not None), default=0) \\\n                      + 1\n            vals = [val if val is not None else overmax for val in vals]\n            self.order = sorted(self.order, key=vals.__getitem__)\n\n        heights = 90 if self.show_annotations else 60\n\n        for row, box_index in enumerate(self.order):\n            y = (-len(self.stats) + row) * heights + 10\n            for item in self.boxes[box_index]:\n                self.box_scene.addItem(item)\n                item.setY(y)\n            labels = self.labels[box_index]\n\n            if self.show_annotations:\n                labels.show()\n                labels.setY(y)\n            else:\n                labels.hide()\n\n            label = self.attr_labels[box_index]\n            label.setY(y - 15 - label.boundingRect().height())\n            if self.show_annotations:\n                label.hide()\n            else:\n                stat = self.stats[box_index]\n\n                if self.compare == OWBoxPlot.CompareMedians and \\\n                        stat.median is not None:\n                    pos = stat.median + 5 / self.scale_x\n                elif self.compare == OWBoxPlot.CompareMeans or stat.q25 is None:\n                    pos = stat.mean + 5 / self.scale_x\n                else:\n                    pos = stat.q25\n                label.setX(pos * self.scale_x)\n                label.show()\n\n        r = QRectF(self.scene_min_x, -30 - len(self.stats) * heights,\n                   self.scene_width, len(self.stats) * heights + 90)\n        self.box_scene.setSceneRect(r)\n\n        self._compute_tests_cont()\n        self._show_posthoc()\n\n    def _display_changed_disc(self):\n        self.clear_scene()\n        self.attr_labels = [QGraphicsSimpleTextItem(lab)\n                            for lab in self.label_txts_all]\n\n        if not self.show_stretched:\n            if self.group_var:\n                self.labels = [\n                    QGraphicsTextItem(\"{}\".format(int(sum(cont))))\n                    for cont in self.conts.array_with_unknowns\n                    if np.sum(cont) > 0]\n            else:\n                self.labels = [\n                    QGraphicsTextItem(str(int(sum(self.dist))))]\n\n        self.order = list(range(len(self.attr_labels)))\n\n        self.draw_axis_disc()\n        if self.group_var:\n            self.boxes = \\\n                [self.strudel(cont, i)\n                 for i, cont in enumerate(self.conts.array_with_unknowns)\n                 if np.sum(cont) > 0]\n\n            if self.sort_freqs:\n                # pylint: disable=invalid-unary-operand-type\n                self.order = sorted(\n                    self.order, key=(-np.sum(\n                        self.conts.array_with_unknowns, axis=1)).__getitem__)\n        else:\n            self.boxes = [self.strudel(self.dist.array_with_unknowns)]\n\n        for row, box_index in enumerate(self.order):\n            y = (-len(self.boxes) + row) * 40 + 10\n            box = self.boxes[box_index]\n            bars, labels = box[::2], box[1::2]\n\n            self.__draw_group_labels(y, box_index)\n            if not self.show_stretched:\n                self.__draw_row_counts(y, box_index)\n            if self.show_labels and self.attribute is not self.group_var:\n                self.__draw_bar_labels(y, bars, labels)\n            self.__draw_bars(y, bars)\n\n        self.box_scene.setSceneRect(-self.label_width - 5,\n                                    -30 - len(self.boxes) * 40,\n                                    self.scene_width, len(self.boxes * 40) + 90)\n        self._compute_tests_disc()\n\n    def __draw_group_labels(self, y, row):\n        \"\"\"Draw group labels\n\n        Parameters\n        ----------\n        y: int\n            vertical offset of bars\n        row: int\n            row index\n        \"\"\"\n        label = self.attr_labels[row]\n        b = label.boundingRect()\n        label.setPos(-b.width() - 10, y - b.height() / 2)\n        self.box_scene.addItem(label)\n\n    def __draw_row_counts(self, y, row):\n        \"\"\"Draw row counts\n\n        Parameters\n        ----------\n        y: int\n            vertical offset of bars\n        row: int\n            row index\n        \"\"\"\n        assert not self.is_continuous\n        label = self.labels[row]\n        b = label.boundingRect()\n        if self.group_var:\n            right = self.scale_x * sum(self.conts.array_with_unknowns[row])\n        else:\n            right = self.scale_x * sum(self.dist)\n        label.setPos(right + 10, y - b.height() / 2)\n        self.box_scene.addItem(label)\n\n    def __draw_bar_labels(self, y, bars, labels):\n        \"\"\"Draw bar labels\n\n        Parameters\n        ----------\n        y: int\n            vertical offset of bars\n        bars: List[FilterGraphicsRectItem]\n            list of bars being drawn\n        labels: List[QGraphicsTextItem]\n            list of labels for corresponding bars\n        \"\"\"\n        label = bar_part = None\n        for text_item, bar_part in zip(labels, bars):\n            label = self.Label(\n                text_item.toPlainText())\n            label.setPos(bar_part.boundingRect().x(),\n                         y - label.boundingRect().height() - 8)\n            label.setMaxWidth(bar_part.boundingRect().width())\n            self.box_scene.addItem(label)\n\n    def __draw_bars(self, y, bars):\n        \"\"\"Draw bars\n\n        Parameters\n        ----------\n        y: int\n            vertical offset of bars\n\n        bars: List[FilterGraphicsRectItem]\n            list of bars to draw\n        \"\"\"\n        for item in bars:\n            item.setPos(0, y)\n            self.box_scene.addItem(item)\n\n    # noinspection PyPep8Naming\n    def _compute_tests_cont(self):\n        # The t-test and ANOVA are implemented here since they efficiently use\n        # the widget-specific data in self.stats.\n        # The non-parametric tests can't do this, so we use statistics.tests\n\n        # pylint: disable=comparison-with-itself\n        def stat_ttest():\n            d1, d2 = self.stats\n            if d1.n < 2 or d2.n < 2:\n                return np.nan, np.nan\n            pooled_var = d1.var / d1.n + d2.var / d2.n\n            # pylint: disable=comparison-with-itself\n            if pooled_var == 0 or np.isnan(pooled_var):\n                return np.nan, np.nan\n            df = pooled_var ** 2 / \\\n                ((d1.var / d1.n) ** 2 / (d1.n - 1) +\n                 (d2.var / d2.n) ** 2 / (d2.n - 1))\n            t = abs(d1.mean - d2.mean) / math.sqrt(pooled_var)\n            p = 2 * (1 - scipy.special.stdtr(df, t))\n            return t, p\n\n        # TODO: Check this function\n        # noinspection PyPep8Naming\n        def stat_ANOVA():\n            if any(stat.n == 0 for stat in self.stats):\n                return np.nan, np.nan\n            n = sum(stat.n for stat in self.stats)\n            grand_avg = sum(stat.n * stat.mean for stat in self.stats) / n\n            var_between = sum(stat.n * (stat.mean - grand_avg) ** 2\n                              for stat in self.stats)\n            df_between = len(self.stats) - 1\n\n            var_within = sum(stat.n * stat.var for stat in self.stats)\n            df_within = n - len(self.stats)\n            if var_within == 0 or df_within == 0 or df_between == 0:\n                return np.nan, np.nan\n            F = (var_between / df_between) / (var_within / df_within)\n            p = 1 - scipy.special.fdtr(df_between, df_within, F)\n            return F, p\n\n        n = len(self.dataset)\n        if self.compare == OWBoxPlot.CompareNone or len(self.stats) < 2:\n            t = \"\"\n        elif any(s.n <= 1 for s in self.stats):\n            t = \"At least one group has just one instance, \" \\\n                \"cannot compute significance\"\n        elif len(self.stats) == 2:\n            if self.compare == OWBoxPlot.CompareMedians:\n                t = \"\"\n                # z, p = tests.wilcoxon_rank_sum(\n                #    self.stats[0].dist, self.stats[1].dist)\n                # t = \"Mann-Whitney's z: %.1f (p=%.3f)\" % (z, p)\n            else:\n                t, p = stat_ttest()\n                t = \"\" if np.isnan(t) else f\"Student's t: {t:.3f} (p={p:.3f}, N={n})\"\n        else:\n            if self.compare == OWBoxPlot.CompareMedians:\n                t = \"\"\n                # U, p = -1, -1\n                # t = \"Kruskal Wallis's U: %.1f (p=%.3f)\" % (U, p)\n            else:\n                F, p = stat_ANOVA()\n                t = \"\" if np.isnan(F) else f\"ANOVA: {F:.3f} (p={p:.3f}, N={n})\"\n        self.stat_test = t\n\n    def _compute_tests_disc(self):\n        if self.group_var is None or self.attribute is None:\n            self.stat_test = \"\"\n        else:\n            chi, p, dof = self._chi_square(self.group_var, self.attribute)\n            if np.isnan(p):\n                self.stat_test = \"\"\n            else:\n                self.stat_test = f\"χ²: {chi:.2f} (p={p:.3f}, dof={dof})\"\n\n    def mean_label(self, stat, attr, val_name):\n        label = QGraphicsItemGroup()\n        t = QGraphicsSimpleTextItem(attr.str_val(stat.mean), label)\n        t.setFont(self._label_font)\n        bbox = t.boundingRect()\n        w2, h = bbox.width() / 2, bbox.height()\n        t.setPos(-w2, -h)\n        tpm = QGraphicsSimpleTextItem(\n            \" \\u00b1 \" + \"%.*f\" % (attr.number_of_decimals + 1, stat.dev),\n            label)\n        tpm.setFont(self._label_font)\n        tpm.setPos(w2, -h)\n        if val_name:\n            vnm = QGraphicsSimpleTextItem(val_name + \": \", label)\n            vnm.setFont(self._label_font)\n            vnm.setBrush(self._attr_brush)\n            vb = vnm.boundingRect()\n            label.min_x = -w2 - vb.width()\n            vnm.setPos(label.min_x, -h)\n        else:\n            label.min_x = -w2\n        return label\n\n    def draw_axis(self):\n        \"\"\"Draw the horizontal axis and sets self.scale_x\"\"\"\n        misssing_stats = not self.stats\n        stats = self.stats or [BoxData(np.array([[0.], [1.]]), self.attribute)]\n        mean_labels = self.mean_labels or [self.mean_label(stats[0], self.attribute, \"\")]\n        bottom = min(stat.a_min for stat in stats)\n        top = max(stat.a_max for stat in stats)\n\n        first_val, step = compute_scale(bottom, top)\n        while bottom <= first_val:\n            first_val -= step\n        bottom = first_val\n        no_ticks = math.ceil((top - first_val) / step) + 1\n        top = max(top, first_val + no_ticks * step)\n\n        gbottom = min(bottom, min(stat.mean - stat.dev for stat in stats))\n        gtop = max(top, max(stat.mean + stat.dev for stat in stats))\n\n        bv = self.box_view\n        viewrect = bv.viewport().rect().adjusted(15, 15, -15, -30)\n        self.scale_x = scale_x = viewrect.width() / (gtop - gbottom)\n\n        # In principle we should repeat this until convergence since the new\n        # scaling is too conservative. (No chance am I doing this.)\n        mlb = min(stat.mean + mean_lab.min_x / scale_x\n                  for stat, mean_lab in zip(stats, mean_labels))\n        if mlb < gbottom:\n            gbottom = mlb\n            self.scale_x = scale_x = viewrect.width() / (gtop - gbottom)\n\n        self.scene_min_x = gbottom * scale_x\n        self.scene_max_x = gtop * scale_x\n        self.scene_width = self.scene_max_x - self.scene_min_x\n\n        val = first_val\n        last_text = self.scene_min_x\n        while True:\n            l = self.box_scene.addLine(val * scale_x, -1, val * scale_x, 1,\n                                       self._pen_axis_tick)\n            l.setZValue(100)\n            t = QGraphicsSimpleTextItem(\n                self.attribute.str_val(val) if not misssing_stats else \"?\")\n            t.setFont(self._axis_font)\n            t.setFlag(QGraphicsItem.ItemIgnoresTransformations)\n            r = t.boundingRect()\n            x_start = val * scale_x - r.width() / 2\n            x_finish = x_start + r.width()\n            if x_start > last_text + 10 and x_finish < self.scene_max_x:\n                t.setPos(x_start, 8)\n                self.box_scene.addItem(t)\n                last_text = x_finish\n            if val >= top:\n                break\n            val += step\n        self.box_scene.addLine(\n            bottom * scale_x - 4, 0, top * scale_x + 4, 0, self._pen_axis)\n\n    def draw_stat(self):\n        if self.stat_test:\n            label = QGraphicsSimpleTextItem(self.stat_test)\n            brect = self.box_scene.sceneRect()\n            label.setPos(brect.center().x() - label.boundingRect().width()/2,\n                         8 + self._axis_font.pixelSize()*2)\n            label.setFlag(QGraphicsItem.ItemIgnoresTransformations)\n            self.box_scene.addItem(label)\n\n    def draw_axis_disc(self):\n        \"\"\"\n        Draw the horizontal axis and sets self.scale_x for discrete attributes\n        \"\"\"\n        assert not self.is_continuous\n        if self.show_stretched:\n            if not self.attr_labels:\n                return\n            step = steps = 10\n        else:\n            if self.group_var:\n                max_box = max(float(np.sum(dist))\n                              for dist in self.conts.array_with_unknowns)\n            else:\n                max_box = float(np.sum(self.dist.array_with_unknowns))\n            if max_box == 0:\n                self.scale_x = 1\n                return\n            _, step = compute_scale(0, max_box)\n            step = int(step) if step > 1 else 1\n            steps = int(math.ceil(max_box / step))\n        max_box = step * steps\n\n        bv = self.box_view\n        viewrect = bv.viewport().rect().adjusted(15, 15, -15, -30)\n        self.scene_width = viewrect.width()\n\n        lab_width = max(lab.boundingRect().width() for lab in self.attr_labels)\n        lab_width = max(lab_width, 40)\n        lab_width = min(lab_width, self.scene_width / 3)\n        self.label_width = lab_width\n\n        right_offset = 0  # offset for the right label\n        if not self.show_stretched and self.labels:\n            if self.group_var:\n                rows = list(zip(self.conts.array_with_unknowns, self.labels))\n            else:\n                rows = [(self.dist, self.labels[0])]\n            # available space left of the 'group labels'\n            available = self.scene_width - lab_width - 10\n            scale_x = (available - right_offset) / max_box\n            max_right = max(sum(dist) * scale_x + 10 +\n                            lbl.boundingRect().width()\n                            for dist, lbl in rows)\n            right_offset = max(0, max_right - max_box * scale_x)\n\n        self.scale_x = scale_x = \\\n            (self.scene_width - lab_width - 10 - right_offset) / max_box\n\n        self.box_scene.addLine(0, 0, max_box * scale_x, 0, self._pen_axis)\n        for val in range(0, step * steps + 1, step):\n            l = self.box_scene.addLine(val * scale_x, -1, val * scale_x, 1,\n                                       self._pen_axis_tick)\n            l.setZValue(100)\n            t = self.box_scene.addSimpleText(str(val), self._axis_font)\n            t.setPos(val * scale_x - t.boundingRect().width() / 2, 8)\n        if self.show_stretched:\n            self.scale_x *= 100\n\n    def label_group(self, stat, attr, mean_lab):\n        def centered_text(val, pos):\n            t = QGraphicsSimpleTextItem(attr.str_val(val), labels)\n            t.setFont(self._label_font)\n            bbox = t.boundingRect()\n            t.setPos(pos - bbox.width() / 2, 22)\n            return t\n\n        def line(x, down=1):\n            QGraphicsLineItem(x, 12 * down, x, 20 * down, labels)\n\n        def move_label(label, frm, to):\n            label.setX(to)\n            to += t_box.width() / 2\n            path = QPainterPath()\n            path.lineTo(0, 4)\n            path.lineTo(to - frm, 4)\n            path.lineTo(to - frm, 8)\n            p = QGraphicsPathItem(path)\n            p.setPos(frm, 12)\n            labels.addToGroup(p)\n\n        labels = QGraphicsItemGroup()\n\n        labels.addToGroup(mean_lab)\n        m = stat.mean * self.scale_x\n        mean_lab.setPos(m, -22)\n        line(m, -1)\n\n        if stat.median is not None:\n            msc = stat.median * self.scale_x\n            med_t = centered_text(stat.median, msc)\n            med_box_width2 = med_t.boundingRect().width() / 2\n            line(msc)\n\n        if stat.q25 is not None:\n            x = stat.q25 * self.scale_x\n            t = centered_text(stat.q25, x)\n            t_box = t.boundingRect()\n            med_left = msc - med_box_width2\n            if x + t_box.width() / 2 >= med_left - 5:\n                move_label(t, x, med_left - t_box.width() - 5)\n            else:\n                line(x)\n\n        if stat.q75 is not None:\n            x = stat.q75 * self.scale_x\n            t = centered_text(stat.q75, x)\n            t_box = t.boundingRect()\n            med_right = msc + med_box_width2\n            if x - t_box.width() / 2 <= med_right + 5:\n                move_label(t, x, med_right + 5)\n            else:\n                line(x)\n\n        return labels\n\n    def box_group(self, stat, height=20):\n        def line(x0, y0, x1, y1, *args):\n            return QGraphicsLineItem(x0 * scale_x, y0, x1 * scale_x, y1, *args)\n\n        scale_x = self.scale_x\n        box = []\n        whisker1 = line(stat.a_min, -1.5, stat.a_min, 1.5)\n        whisker2 = line(stat.a_max, -1.5, stat.a_max, 1.5)\n        vert_line = line(stat.a_min, 0, stat.a_max, 0)\n        mean_line = line(stat.mean, -height / 3, stat.mean, height / 3)\n        for it in (whisker1, whisker2, mean_line):\n            it.setPen(self._pen_paramet)\n        vert_line.setPen(self._pen_dotted)\n        var_line = line(stat.mean - stat.dev, 0, stat.mean + stat.dev, 0)\n        var_line.setPen(self._pen_paramet)\n        box.extend([whisker1, whisker2, vert_line, mean_line, var_line])\n        if stat.q25 is not None or stat.q75 is not None:\n            # if any of them is None it means that its value is equal to median\n            box_from = stat.q25 or stat.median\n            box_to = stat.q75 or stat.median\n            mbox = FilterGraphicsRectItem(\n                stat.conditions, box_from * scale_x, -height / 2,\n                (box_to - box_from) * scale_x, height)\n            mbox.setBrush(self._box_brush)\n            mbox.setPen(QPen(Qt.NoPen))\n            mbox.setZValue(-200)\n            box.append(mbox)\n\n        if stat.median is not None:\n            median_line = line(stat.median, -height / 2,\n                               stat.median, height / 2)\n            median_line.setPen(self._pen_median)\n            median_line.setZValue(-150)\n            box.append(median_line)\n\n        return box\n\n    def strudel(self, dist, group_val_index=None):\n        attr = self.attribute\n        ss = np.sum(dist)\n        box = []\n        if ss < 1e-6:\n            cond = [FilterDiscrete(attr, None)]\n            if group_val_index is not None:\n                cond.append(FilterDiscrete(self.group_var, [group_val_index]))\n            box.append(FilterGraphicsRectItem(cond, 0, -10, 1, 10))\n        cum = 0\n        values = attr.values + [\"Missing values\"]\n        colors = np.vstack((attr.colors, [128, 128, 128]))\n        for i, v in enumerate(dist):\n            if v < 1e-6:\n                continue\n            if self.show_stretched:\n                v /= ss\n            v *= self.scale_x\n            cond = [FilterDiscrete(attr, [i])]\n            if group_val_index is not None:\n                cond.append(FilterDiscrete(self.group_var, [group_val_index]))\n            rect = FilterGraphicsRectItem(cond, cum + 1, -6, v - 2, 12)\n            rect.setBrush(QBrush(QColor(*colors[i])))\n            rect.setPen(QPen(Qt.NoPen))\n            if self.show_stretched:\n                tooltip = \"{}: {:.2f}%\".format(\n                    values[i],\n                    100 * dist[i] / sum(dist))\n            else:\n                tooltip = \"{}: {}\".format(values[i], int(dist[i]))\n            rect.setToolTip(tooltip)\n            text = QGraphicsTextItem(values[i])\n            box.append(rect)\n            box.append(text)\n            cum += v\n        return box\n\n    def commit(self):\n        self.conditions = [item.filter for item in\n                           self.box_scene.selectedItems() if item.filter]\n        selected, selection = None, []\n        if self.conditions:\n            selected = Values(self.conditions, conjunction=False)(self.dataset)\n            selection = np.in1d(\n                self.dataset.ids, selected.ids, assume_unique=True).nonzero()[0]\n        self.Outputs.selected_data.send(selected)\n        self.Outputs.annotated_data.send(\n            create_annotated_table(self.dataset, selection))\n\n    def _show_posthoc(self):\n        def line(y0, y1):\n            it = self.box_scene.addLine(x, y0, x, y1, self._post_line_pen)\n            it.setZValue(-100)\n            self.posthoc_lines.append(it)\n\n        while self.posthoc_lines:\n            self.box_scene.removeItem(self.posthoc_lines.pop())\n\n        if self.compare == OWBoxPlot.CompareNone or len(self.stats) < 2:\n            return\n\n        if self.compare == OWBoxPlot.CompareMedians:\n            crit_line = \"median\"\n        else:\n            crit_line = \"mean\"\n\n        xs = []\n\n        height = 90 if self.show_annotations else 60\n\n        y_up = -len(self.stats) * height + 10\n        for pos, box_index in enumerate(self.order):\n            stat = self.stats[box_index]\n            x = getattr(stat, crit_line)\n            if x is None:\n                continue\n            x *= self.scale_x\n            xs.append(x * self.scale_x)\n            by = y_up + pos * height\n            line(by + 12, 0)\n\n        used_to = []\n        last_to = to = 0\n        for frm, frm_x in enumerate(xs[:-1]):\n            for to in range(frm + 1, len(xs)):\n                if xs[to] - frm_x > 1.5:\n                    to -= 1\n                    break\n            if to in (last_to, frm):\n                continue\n            for rowi, used in enumerate(used_to):\n                if used < frm:\n                    used_to[rowi] = to\n                    break\n            else:\n                rowi = len(used_to)\n                used_to.append(to)\n            y = - 6 - rowi * 6\n            it = self.box_scene.addLine(frm_x - 2, y, xs[to] + 2, y,\n                                        self._post_grp_pen)\n            self.posthoc_lines.append(it)\n            last_to = to\n\n    def get_widget_name_extension(self):\n        return self.attribute.name if self.attribute else None\n\n    def send_report(self):\n        self.report_plot()\n        text = \"\"\n        if self.attribute:\n            text += \"Box plot for attribute '{}' \".format(self.attribute.name)\n        if self.group_var:\n            text += \"grouped by '{}'\".format(self.group_var.name)\n        if text:\n            self.report_caption(text)\n\n    class Label(QGraphicsSimpleTextItem):\n        \"\"\"Boxplot Label with settable maxWidth\"\"\"\n        # Minimum width to display label text\n        MIN_LABEL_WIDTH = 25\n\n        # padding bellow the text\n        PADDING = 3\n\n        __max_width = None\n\n        def maxWidth(self):\n            return self.__max_width\n\n        def setMaxWidth(self, max_width):\n            self.__max_width = max_width\n\n        def paint(self, painter, option, widget):\n            \"\"\"Overrides QGraphicsSimpleTextItem.paint\n\n            If label text is too long, it is elided\n            to fit into the allowed region\n            \"\"\"\n            if self.__max_width is None:\n                width = option.rect.width()\n            else:\n                width = self.__max_width\n\n            if width < self.MIN_LABEL_WIDTH:\n                # if space is too narrow, no label\n                return\n\n            fm = painter.fontMetrics()\n            text = fm.elidedText(self.text(), Qt.ElideRight, width)\n            painter.drawText(\n                option.rect.x(),\n                option.rect.y() + self.boundingRect().height() - self.PADDING,\n                text)",
                            "ast_type_str": "ClassDef",
                            "start_lineno": 304,
                            "end_lineno": 309,
                            "parent": {
                                "file_name": "Orange/widgets/visualize/owboxplot.py",
                                "type": "class",
                                "name": "OWBoxPlot",
                                "content": "class OWBoxPlot(widget.OWWidget):\n    \"\"\"\n    Here's how the widget's functions call each other:\n\n    - `set_data` is a signal handler fills the list boxes and calls\n    `grouping_changed`.\n\n    - `grouping_changed` handles changes of grouping attribute: it enables or\n    disables the box for ordering, orders attributes and calls `attr_changed`.\n\n    - `attr_changed` handles changes of attribute. It recomputes box data by\n    calling `compute_box_data`, shows the appropriate display box\n    (discrete/continuous) and then calls`layout_changed`\n\n    - `layout_changed` constructs all the elements for the scene (as lists of\n    QGraphicsItemGroup) and calls `display_changed`. It is called when the\n    attribute or grouping is changed (by attr_changed) and on resize event.\n\n    - `display_changed` puts the elements corresponding to the current display\n    settings on the scene. It is called when the elements are reconstructed\n    (layout is changed due to selection of attributes or resize event), or\n    when the user changes display settings or colors.\n\n    For discrete attributes, the flow is a bit simpler: the elements are not\n    constructed in advance (by layout_changed). Instead, _display_changed_disc\n    draws everything.\n    \"\"\"\n    name = \"Box Plot\"\n    description = \"Visualize the distribution of feature values in a box plot.\"\n    icon = \"icons/BoxPlot.svg\"\n    priority = 100\n    keywords = [\"whisker\"]\n\n    class Inputs:\n        data = Input(\"Data\", Orange.data.Table)\n\n    class Outputs:\n        selected_data = Output(\"Selected Data\", Orange.data.Table, default=True)\n        annotated_data = Output(ANNOTATED_DATA_SIGNAL_NAME, Orange.data.Table)\n\n    #: Comparison types for continuous variables\n    CompareNone, CompareMedians, CompareMeans = 0, 1, 2\n\n    settingsHandler = DomainContextHandler()\n    conditions = ContextSetting([])\n\n    attribute = ContextSetting(None)\n    order_by_importance = Setting(False)\n    order_grouping_by_importance = Setting(False)\n    group_var = ContextSetting(None)\n    show_annotations = Setting(True)\n    compare = Setting(CompareMeans)\n    stattest = Setting(0)\n    sig_threshold = Setting(0.05)\n    stretched = Setting(True)\n    show_labels = Setting(True)\n    sort_freqs = Setting(False)\n\n    _sorting_criteria_attrs = {\n        CompareNone: \"\", CompareMedians: \"median\", CompareMeans: \"mean\"\n    }\n\n    _pen_axis_tick = QPen(Qt.white, 5)\n    _pen_axis = QPen(Qt.darkGray, 3)\n    _pen_median = QPen(QBrush(QColor(0xff, 0xff, 0x00)), 2)\n    _pen_paramet = QPen(QBrush(QColor(0x33, 0x00, 0xff)), 2)\n    _pen_dotted = QPen(QBrush(QColor(0x33, 0x00, 0xff)), 1)\n    _pen_dotted.setStyle(Qt.DotLine)\n    _post_line_pen = QPen(Qt.lightGray, 2)\n    _post_grp_pen = QPen(Qt.lightGray, 4)\n    for pen in (_pen_paramet, _pen_median, _pen_dotted,\n                _pen_axis, _pen_axis_tick, _post_line_pen, _post_grp_pen):\n        pen.setCosmetic(True)\n        pen.setCapStyle(Qt.RoundCap)\n        pen.setJoinStyle(Qt.RoundJoin)\n    _pen_axis_tick.setCapStyle(Qt.FlatCap)\n\n    _box_brush = QBrush(QColor(0x33, 0x88, 0xff, 0xc0))\n\n    _axis_font = QFont()\n    _axis_font.setPixelSize(12)\n    _label_font = QFont()\n    _label_font.setPixelSize(11)\n    _attr_brush = QBrush(QColor(0x33, 0x00, 0xff))\n\n    graph_name = \"box_scene\"\n\n    def __init__(self):\n        super().__init__()\n        self.stats = []\n        self.dataset = None\n        self.posthoc_lines = []\n\n        self.label_txts = self.mean_labels = self.boxes = self.labels = \\\n            self.label_txts_all = self.attr_labels = self.order = []\n        self.scale_x = 1\n        self.scene_min_x = self.scene_max_x = self.scene_width = 0\n        self.label_width = 0\n\n        self.attrs = VariableListModel()\n        view = gui.listView(\n            self.controlArea, self, \"attribute\", box=\"Variable\",\n            model=self.attrs, callback=self.attr_changed)\n        view.setMinimumSize(QSize(30, 30))\n        # Any other policy than Ignored will let the QListBox's scrollbar\n        # set the minimal height (see the penultimate paragraph of\n        # http://doc.qt.io/qt-4.8/qabstractscrollarea.html#addScrollBarWidget)\n        view.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Ignored)\n        gui.checkBox(\n            view.box, self, \"order_by_importance\",\n            \"Order by relevance to subgroups\",\n            tooltip=\"Order by 𝜒² or ANOVA over the subgroups\",\n            callback=self.apply_attr_sorting)\n        self.group_vars = VariableListModel(placeholder=\"None\")\n        view = gui.listView(\n            self.controlArea, self, \"group_var\", box=\"Subgroups\",\n            model=self.group_vars, callback=self.grouping_changed)\n        gui.checkBox(\n            view.box, self, \"order_grouping_by_importance\",\n            \"Order by relevance to variable\",\n            tooltip=\"Order by 𝜒² or ANOVA over the variable values\",\n            callback=self.on_group_sorting_checkbox)\n        view.setMinimumSize(QSize(30, 30))\n        # See the comment above\n        view.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Ignored)\n\n        # TODO: move Compare median/mean to grouping box\n        # The vertical size policy is needed to let only the list views expand\n        self.display_box = gui.vBox(\n            self.controlArea, \"Display\",\n            sizePolicy=(QSizePolicy.Minimum, QSizePolicy.Maximum),\n            addSpace=False)\n\n        gui.checkBox(self.display_box, self, \"show_annotations\", \"Annotate\",\n                     callback=self.display_changed)\n        self.compare_rb = gui.radioButtonsInBox(\n            self.display_box, self, 'compare',\n            btnLabels=[\"No comparison\", \"Compare medians\", \"Compare means\"],\n            callback=self.layout_changed)\n\n        # The vertical size policy is needed to let only the list views expand\n        self.stretching_box = box = gui.vBox(\n            self.controlArea, box=\"Display\",\n            sizePolicy=(QSizePolicy.Minimum, QSizePolicy.Fixed))\n        self.stretching_box.sizeHint = self.display_box.sizeHint\n        gui.checkBox(\n            box, self, 'stretched', \"Stretch bars\",\n            callback=self.display_changed)\n        gui.checkBox(\n            box, self, 'show_labels', \"Show box labels\",\n            callback=self.display_changed)\n        self.sort_cb = gui.checkBox(\n            box, self, 'sort_freqs', \"Sort by subgroup frequencies\",\n            callback=self.display_changed)\n\n        gui.vBox(self.mainArea, addSpace=True)\n        self.box_scene = QGraphicsScene()\n        self.box_scene.selectionChanged.connect(self.commit)\n        self.box_view = QGraphicsView(self.box_scene)\n        self.box_view.setRenderHints(QPainter.Antialiasing |\n                                     QPainter.TextAntialiasing |\n                                     QPainter.SmoothPixmapTransform)\n        self.box_view.viewport().installEventFilter(self)\n\n        self.mainArea.layout().addWidget(self.box_view)\n\n        gui.hBox(self.mainArea, addSpace=False)\n        self.stat_test = \"\"\n        self.mainArea.setMinimumWidth(300)\n\n        self.stats = self.dist = self.conts = []\n        self.is_continuous = False\n\n        self.update_display_box()\n\n    def sizeHint(self):\n        return QSize(900, 500)\n\n    def eventFilter(self, obj, event):\n        if obj is self.box_view.viewport() and \\\n                event.type() == QEvent.Resize:\n            self.layout_changed()\n\n        return super().eventFilter(obj, event)\n\n    @property\n    def show_stretched(self):\n        return self.stretched and self.group_var is not self.attribute\n\n    def reset_attrs(self):\n        domain = self.dataset.domain\n        self.attrs[:] = [\n            var for var in chain(\n                domain.class_vars, domain.metas, domain.attributes)\n            if var.is_primitive()]\n\n    def reset_groups(self):\n        domain = self.dataset.domain\n        self.group_vars[:] = [None] + [\n            var for var in chain(\n                domain.class_vars, domain.metas, domain.attributes)\n            if var.is_discrete]\n\n    # noinspection PyTypeChecker\n    @Inputs.data\n    def set_data(self, dataset):\n        if dataset is not None and (\n                not bool(dataset) or not len(dataset.domain) and not\n                any(var.is_primitive() for var in dataset.domain.metas)):\n            dataset = None\n        self.closeContext()\n        self.dataset = dataset\n        self.dist = self.stats = self.conts = []\n        self.group_var = None\n        self.attribute = None\n        if dataset:\n            self.reset_attrs()\n            self.reset_groups()\n            self.select_default_variables()\n            self.openContext(self.dataset)\n            self.grouping_changed()\n            self.attr_changed()\n        else:\n            self.reset_all_data()\n        self.commit()\n\n    def select_default_variables(self):\n        # visualize first non-class variable, group by class (if present)\n        domain = self.dataset.domain\n        if len(self.attrs) > len(domain.class_vars):\n            self.attribute = self.attrs[len(domain.class_vars)]\n        elif self.attrs:\n            self.attribute = self.attrs[0]\n\n        if domain.class_var and domain.class_var.is_discrete:\n            self.group_var = domain.class_var\n        else:\n            self.group_var = None  # Reset to trigger selection via callback\n\n    def apply_attr_sorting(self):\n        def compute_score(attr):\n            # This function and the one in apply_group_sorting are similar, but\n            # different in too many details, so they are kept as separate\n            # functions.\n            # If you discover a bug in this function, check the other one, too.\n            if attr is group_var:\n                return 3\n            if attr.is_continuous:\n                # One-way ANOVA\n                col = data.get_column_view(attr)[0].astype(float)\n                groups = (col[group_col == i] for i in range(n_groups))\n                groups = (col[~np.isnan(col)] for col in groups)\n                groups = [group for group in groups if len(group)]\n                p = f_oneway(*groups)[1] if len(groups) > 1 else 2\n            else:\n                p = self._chi_square(group_var, attr)[1]\n            if math.isnan(p):\n                return 2\n            return p\n\n        data = self.dataset\n        if data is None:\n            return\n        domain = data.domain\n        attribute = self.attribute\n        group_var = self.group_var\n        if self.order_by_importance and group_var is not None:\n            n_groups = len(group_var.values)\n            group_col = data.get_column_view(group_var)[0] if \\\n                domain.has_continuous_attributes(\n                    include_class=True, include_metas=True) else None\n            self.attrs.sort(key=compute_score)\n        else:\n            self.reset_attrs()\n        self.attribute = attribute  # reset selection\n        self._ensure_selection_visible(self.controls.attribute)\n\n    def on_group_sorting_checkbox(self):\n        if self.order_grouping_by_importance:\n            self.apply_group_sorting()\n        else:\n            self.reset_groups()\n            self.group_var = self.group_var  # reset selection\n            self._ensure_selection_visible(self.controls.group_var)\n\n    def apply_group_sorting(self):\n        def compute_stat(group):\n            # This function and the one in apply_attr_sorting are similar, but\n            # different in too many details, so they are kept as separate\n            # functions.\n            # If you discover a bug in this function, check the other one, too.\n            if group is attr:\n                return 3\n            if group is None:\n                return -1\n            if attr.is_continuous:\n                group_col = data.get_column_view(group)[0].astype(float)\n                groups = (attr_col[group_col == i]\n                          for i in range(len(group.values)))\n                groups = (col[~np.isnan(col)] for col in groups)\n                groups = [group for group in groups if len(group)]\n                p = f_oneway(*groups)[1] if len(groups) > 1 else 2\n            else:\n                p = self._chi_square(group, attr)[1]\n            if math.isnan(p):\n                return 2\n            return p\n\n        data = self.dataset\n        if data is None or not self.order_grouping_by_importance:\n            return\n        attr = self.attribute\n        group_var = self.group_var\n        if attr.is_continuous:\n            attr_col = data.get_column_view(attr)[0].astype(float)\n        self.group_vars.sort(key=compute_stat)\n        self.group_var = group_var  # reset selection\n        self._ensure_selection_visible(self.controls.group_var)\n\n    @staticmethod\n    def _ensure_selection_visible(view):\n        selection = view.selectedIndexes()\n        if len(selection) == 1:\n            view.scrollTo(selection[0])\n\n    def _chi_square(self, group_var, attr):\n        # Chi-square with the given distribution into groups\n        if not attr.values or not group_var.values:\n            return 0, 2, 0\n        observed = np.array(\n            contingency.get_contingency(self.dataset, group_var, attr))\n        observed = observed[observed.sum(axis=1) != 0, :]\n        observed = observed[:, observed.sum(axis=0) != 0]\n        if min(observed.shape) < 2:\n            return 0, 2, 0\n        return chi2_contingency(observed)[:3]\n\n    def reset_all_data(self):\n        self.clear_scene()\n        self.stat_test = \"\"\n        self.attrs[:] = []\n        self.group_vars[:] = [None]\n        self.is_continuous = False\n        self.update_display_box()\n\n    def grouping_changed(self):\n        self.controls.stretched.setDisabled(self.group_var is self.attribute)\n        self.apply_attr_sorting()\n        self.update_graph()\n\n    def select_box_items(self):\n        temp_cond = self.conditions.copy()\n        for box in self.box_scene.items():\n            if isinstance(box, FilterGraphicsRectItem):\n                box.setSelected(box.filter.conditions in\n                                [c.conditions for c in temp_cond])\n\n    def attr_changed(self):\n        self.controls.stretched.setDisabled(self.group_var is self.attribute)\n        self.apply_group_sorting()\n        self.update_graph()\n\n    def update_graph(self):\n        self.compute_box_data()\n        self.update_display_box()\n        self.layout_changed()\n\n        if self.is_continuous:\n            heights = 90 if self.show_annotations else 60\n            self.box_view.centerOn(self.scene_min_x + self.scene_width / 2,\n                                   -30 - len(self.stats) * heights / 2 + 45)\n        else:\n            self.box_view.centerOn(self.scene_width / 2,\n                                   -30 - len(self.boxes) * 40 / 2 + 45)\n\n    def compute_box_data(self):\n        attr = self.attribute\n        if not attr:\n            return\n        dataset = self.dataset\n        self.is_continuous = attr.is_continuous\n        if dataset is None or not self.is_continuous and not attr.values or \\\n                        self.group_var and not self.group_var.values:\n            self.stats = self.dist = self.conts = []\n            return\n        if self.group_var:\n            self.dist = []\n            self.conts = contingency.get_contingency(\n                dataset, attr, self.group_var)\n            if self.is_continuous:\n                stats, label_texts = [], []\n                for i, cont in enumerate(self.conts):\n                    if np.sum(cont[1]):\n                        stats.append(BoxData(cont, attr, i, self.group_var))\n                        label_texts.append(self.group_var.values[i])\n                self.stats = stats\n                self.label_txts_all = label_texts\n            else:\n                self.label_txts_all = \\\n                    [v for v, c in zip(\n                        self.group_var.values + [\"Missing values\"],\n                        self.conts.array_with_unknowns)\n                     if np.sum(c) > 0]\n        else:\n            self.dist = distribution.get_distribution(dataset, attr)\n            self.conts = []\n            if self.is_continuous:\n                self.stats = [BoxData(self.dist, attr, None)]\n            self.label_txts_all = [\"\"]\n        self.label_txts = [txts for stat, txts in zip(self.stats,\n                                                      self.label_txts_all)\n                           if stat.n > 0]\n        self.stats = [stat for stat in self.stats if stat.n > 0]\n\n    def update_display_box(self):\n        if self.is_continuous:\n            self.stretching_box.hide()\n            self.display_box.show()\n            self.compare_rb.setEnabled(self.group_var is not None)\n        else:\n            self.stretching_box.show()\n            self.display_box.hide()\n            self.sort_cb.setEnabled(self.group_var is not None)\n\n    def clear_scene(self):\n        self.closeContext()\n        self.box_scene.clearSelection()\n        self.box_scene.clear()\n        self.box_view.viewport().update()\n        self.attr_labels = []\n        self.labels = []\n        self.boxes = []\n        self.mean_labels = []\n        self.posthoc_lines = []\n        self.openContext(self.dataset)\n\n    def layout_changed(self):\n        attr = self.attribute\n        if not attr:\n            return\n        self.clear_scene()\n        if self.dataset is None or len(self.conts) == len(self.dist) == 0:\n            return\n\n        if self.is_continuous:\n            self.mean_labels = [self.mean_label(stat, attr, lab)\n                                for stat, lab in zip(self.stats, self.label_txts)]\n            self.draw_axis()\n            self.boxes = [self.box_group(stat) for stat in self.stats]\n            self.labels = [self.label_group(stat, attr, mean_lab)\n                           for stat, mean_lab in zip(self.stats, self.mean_labels)]\n            self.attr_labels = [QGraphicsSimpleTextItem(lab)\n                                for lab in self.label_txts]\n            for it in chain(self.labels, self.attr_labels):\n                self.box_scene.addItem(it)\n\n        self.display_changed()\n\n    def display_changed(self):\n        if self.dataset is None or self.attribute is None:\n            return\n\n        if self.is_continuous:\n            self._display_changed_cont()\n        else:\n            self._display_changed_disc()\n        self.draw_stat()\n        self.select_box_items()\n\n    def _display_changed_cont(self):\n        self.order = list(range(len(self.stats)))\n        criterion = self._sorting_criteria_attrs[self.compare]\n        if criterion:\n            vals = [getattr(stat, criterion) for stat in self.stats]\n            overmax = max((val for val in vals if val is not None), default=0) \\\n                      + 1\n            vals = [val if val is not None else overmax for val in vals]\n            self.order = sorted(self.order, key=vals.__getitem__)\n\n        heights = 90 if self.show_annotations else 60\n\n        for row, box_index in enumerate(self.order):\n            y = (-len(self.stats) + row) * heights + 10\n            for item in self.boxes[box_index]:\n                self.box_scene.addItem(item)\n                item.setY(y)\n            labels = self.labels[box_index]\n\n            if self.show_annotations:\n                labels.show()\n                labels.setY(y)\n            else:\n                labels.hide()\n\n            label = self.attr_labels[box_index]\n            label.setY(y - 15 - label.boundingRect().height())\n            if self.show_annotations:\n                label.hide()\n            else:\n                stat = self.stats[box_index]\n\n                if self.compare == OWBoxPlot.CompareMedians and \\\n                        stat.median is not None:\n                    pos = stat.median + 5 / self.scale_x\n                elif self.compare == OWBoxPlot.CompareMeans or stat.q25 is None:\n                    pos = stat.mean + 5 / self.scale_x\n                else:\n                    pos = stat.q25\n                label.setX(pos * self.scale_x)\n                label.show()\n\n        r = QRectF(self.scene_min_x, -30 - len(self.stats) * heights,\n                   self.scene_width, len(self.stats) * heights + 90)\n        self.box_scene.setSceneRect(r)\n\n        self._compute_tests_cont()\n        self._show_posthoc()\n\n    def _display_changed_disc(self):\n        self.clear_scene()\n        self.attr_labels = [QGraphicsSimpleTextItem(lab)\n                            for lab in self.label_txts_all]\n\n        if not self.show_stretched:\n            if self.group_var:\n                self.labels = [\n                    QGraphicsTextItem(\"{}\".format(int(sum(cont))))\n                    for cont in self.conts.array_with_unknowns\n                    if np.sum(cont) > 0]\n            else:\n                self.labels = [\n                    QGraphicsTextItem(str(int(sum(self.dist))))]\n\n        self.order = list(range(len(self.attr_labels)))\n\n        self.draw_axis_disc()\n        if self.group_var:\n            self.boxes = \\\n                [self.strudel(cont, i)\n                 for i, cont in enumerate(self.conts.array_with_unknowns)\n                 if np.sum(cont) > 0]\n\n            if self.sort_freqs:\n                # pylint: disable=invalid-unary-operand-type\n                self.order = sorted(\n                    self.order, key=(-np.sum(\n                        self.conts.array_with_unknowns, axis=1)).__getitem__)\n        else:\n            self.boxes = [self.strudel(self.dist.array_with_unknowns)]\n\n        for row, box_index in enumerate(self.order):\n            y = (-len(self.boxes) + row) * 40 + 10\n            box = self.boxes[box_index]\n            bars, labels = box[::2], box[1::2]\n\n            self.__draw_group_labels(y, box_index)\n            if not self.show_stretched:\n                self.__draw_row_counts(y, box_index)\n            if self.show_labels and self.attribute is not self.group_var:\n                self.__draw_bar_labels(y, bars, labels)\n            self.__draw_bars(y, bars)\n\n        self.box_scene.setSceneRect(-self.label_width - 5,\n                                    -30 - len(self.boxes) * 40,\n                                    self.scene_width, len(self.boxes * 40) + 90)\n        self._compute_tests_disc()\n\n    def __draw_group_labels(self, y, row):\n        \"\"\"Draw group labels\n\n        Parameters\n        ----------\n        y: int\n            vertical offset of bars\n        row: int\n            row index\n        \"\"\"\n        label = self.attr_labels[row]\n        b = label.boundingRect()\n        label.setPos(-b.width() - 10, y - b.height() / 2)\n        self.box_scene.addItem(label)\n\n    def __draw_row_counts(self, y, row):\n        \"\"\"Draw row counts\n\n        Parameters\n        ----------\n        y: int\n            vertical offset of bars\n        row: int\n            row index\n        \"\"\"\n        assert not self.is_continuous\n        label = self.labels[row]\n        b = label.boundingRect()\n        if self.group_var:\n            right = self.scale_x * sum(self.conts.array_with_unknowns[row])\n        else:\n            right = self.scale_x * sum(self.dist)\n        label.setPos(right + 10, y - b.height() / 2)\n        self.box_scene.addItem(label)\n\n    def __draw_bar_labels(self, y, bars, labels):\n        \"\"\"Draw bar labels\n\n        Parameters\n        ----------\n        y: int\n            vertical offset of bars\n        bars: List[FilterGraphicsRectItem]\n            list of bars being drawn\n        labels: List[QGraphicsTextItem]\n            list of labels for corresponding bars\n        \"\"\"\n        label = bar_part = None\n        for text_item, bar_part in zip(labels, bars):\n            label = self.Label(\n                text_item.toPlainText())\n            label.setPos(bar_part.boundingRect().x(),\n                         y - label.boundingRect().height() - 8)\n            label.setMaxWidth(bar_part.boundingRect().width())\n            self.box_scene.addItem(label)\n\n    def __draw_bars(self, y, bars):\n        \"\"\"Draw bars\n\n        Parameters\n        ----------\n        y: int\n            vertical offset of bars\n\n        bars: List[FilterGraphicsRectItem]\n            list of bars to draw\n        \"\"\"\n        for item in bars:\n            item.setPos(0, y)\n            self.box_scene.addItem(item)\n\n    # noinspection PyPep8Naming\n    def _compute_tests_cont(self):\n        # The t-test and ANOVA are implemented here since they efficiently use\n        # the widget-specific data in self.stats.\n        # The non-parametric tests can't do this, so we use statistics.tests\n\n        # pylint: disable=comparison-with-itself\n        def stat_ttest():\n            d1, d2 = self.stats\n            if d1.n < 2 or d2.n < 2:\n                return np.nan, np.nan\n            pooled_var = d1.var / d1.n + d2.var / d2.n\n            # pylint: disable=comparison-with-itself\n            if pooled_var == 0 or np.isnan(pooled_var):\n                return np.nan, np.nan\n            df = pooled_var ** 2 / \\\n                ((d1.var / d1.n) ** 2 / (d1.n - 1) +\n                 (d2.var / d2.n) ** 2 / (d2.n - 1))\n            t = abs(d1.mean - d2.mean) / math.sqrt(pooled_var)\n            p = 2 * (1 - scipy.special.stdtr(df, t))\n            return t, p\n\n        # TODO: Check this function\n        # noinspection PyPep8Naming\n        def stat_ANOVA():\n            if any(stat.n == 0 for stat in self.stats):\n                return np.nan, np.nan\n            n = sum(stat.n for stat in self.stats)\n            grand_avg = sum(stat.n * stat.mean for stat in self.stats) / n\n            var_between = sum(stat.n * (stat.mean - grand_avg) ** 2\n                              for stat in self.stats)\n            df_between = len(self.stats) - 1\n\n            var_within = sum(stat.n * stat.var for stat in self.stats)\n            df_within = n - len(self.stats)\n            if var_within == 0 or df_within == 0 or df_between == 0:\n                return np.nan, np.nan\n            F = (var_between / df_between) / (var_within / df_within)\n            p = 1 - scipy.special.fdtr(df_between, df_within, F)\n            return F, p\n\n        n = len(self.dataset)\n        if self.compare == OWBoxPlot.CompareNone or len(self.stats) < 2:\n            t = \"\"\n        elif any(s.n <= 1 for s in self.stats):\n            t = \"At least one group has just one instance, \" \\\n                \"cannot compute significance\"\n        elif len(self.stats) == 2:\n            if self.compare == OWBoxPlot.CompareMedians:\n                t = \"\"\n                # z, p = tests.wilcoxon_rank_sum(\n                #    self.stats[0].dist, self.stats[1].dist)\n                # t = \"Mann-Whitney's z: %.1f (p=%.3f)\" % (z, p)\n            else:\n                t, p = stat_ttest()\n                t = \"\" if np.isnan(t) else f\"Student's t: {t:.3f} (p={p:.3f}, N={n})\"\n        else:\n            if self.compare == OWBoxPlot.CompareMedians:\n                t = \"\"\n                # U, p = -1, -1\n                # t = \"Kruskal Wallis's U: %.1f (p=%.3f)\" % (U, p)\n            else:\n                F, p = stat_ANOVA()\n                t = \"\" if np.isnan(F) else f\"ANOVA: {F:.3f} (p={p:.3f}, N={n})\"\n        self.stat_test = t\n\n    def _compute_tests_disc(self):\n        if self.group_var is None or self.attribute is None:\n            self.stat_test = \"\"\n        else:\n            chi, p, dof = self._chi_square(self.group_var, self.attribute)\n            if np.isnan(p):\n                self.stat_test = \"\"\n            else:\n                self.stat_test = f\"χ²: {chi:.2f} (p={p:.3f}, dof={dof})\"\n\n    def mean_label(self, stat, attr, val_name):\n        label = QGraphicsItemGroup()\n        t = QGraphicsSimpleTextItem(attr.str_val(stat.mean), label)\n        t.setFont(self._label_font)\n        bbox = t.boundingRect()\n        w2, h = bbox.width() / 2, bbox.height()\n        t.setPos(-w2, -h)\n        tpm = QGraphicsSimpleTextItem(\n            \" \\u00b1 \" + \"%.*f\" % (attr.number_of_decimals + 1, stat.dev),\n            label)\n        tpm.setFont(self._label_font)\n        tpm.setPos(w2, -h)\n        if val_name:\n            vnm = QGraphicsSimpleTextItem(val_name + \": \", label)\n            vnm.setFont(self._label_font)\n            vnm.setBrush(self._attr_brush)\n            vb = vnm.boundingRect()\n            label.min_x = -w2 - vb.width()\n            vnm.setPos(label.min_x, -h)\n        else:\n            label.min_x = -w2\n        return label\n\n    def draw_axis(self):\n        \"\"\"Draw the horizontal axis and sets self.scale_x\"\"\"\n        misssing_stats = not self.stats\n        stats = self.stats or [BoxData(np.array([[0.], [1.]]), self.attribute)]\n        mean_labels = self.mean_labels or [self.mean_label(stats[0], self.attribute, \"\")]\n        bottom = min(stat.a_min for stat in stats)\n        top = max(stat.a_max for stat in stats)\n\n        first_val, step = compute_scale(bottom, top)\n        while bottom <= first_val:\n            first_val -= step\n        bottom = first_val\n        no_ticks = math.ceil((top - first_val) / step) + 1\n        top = max(top, first_val + no_ticks * step)\n\n        gbottom = min(bottom, min(stat.mean - stat.dev for stat in stats))\n        gtop = max(top, max(stat.mean + stat.dev for stat in stats))\n\n        bv = self.box_view\n        viewrect = bv.viewport().rect().adjusted(15, 15, -15, -30)\n        self.scale_x = scale_x = viewrect.width() / (gtop - gbottom)\n\n        # In principle we should repeat this until convergence since the new\n        # scaling is too conservative. (No chance am I doing this.)\n        mlb = min(stat.mean + mean_lab.min_x / scale_x\n                  for stat, mean_lab in zip(stats, mean_labels))\n        if mlb < gbottom:\n            gbottom = mlb\n            self.scale_x = scale_x = viewrect.width() / (gtop - gbottom)\n\n        self.scene_min_x = gbottom * scale_x\n        self.scene_max_x = gtop * scale_x\n        self.scene_width = self.scene_max_x - self.scene_min_x\n\n        val = first_val\n        last_text = self.scene_min_x\n        while True:\n            l = self.box_scene.addLine(val * scale_x, -1, val * scale_x, 1,\n                                       self._pen_axis_tick)\n            l.setZValue(100)\n            t = QGraphicsSimpleTextItem(\n                self.attribute.str_val(val) if not misssing_stats else \"?\")\n            t.setFont(self._axis_font)\n            t.setFlag(QGraphicsItem.ItemIgnoresTransformations)\n            r = t.boundingRect()\n            x_start = val * scale_x - r.width() / 2\n            x_finish = x_start + r.width()\n            if x_start > last_text + 10 and x_finish < self.scene_max_x:\n                t.setPos(x_start, 8)\n                self.box_scene.addItem(t)\n                last_text = x_finish\n            if val >= top:\n                break\n            val += step\n        self.box_scene.addLine(\n            bottom * scale_x - 4, 0, top * scale_x + 4, 0, self._pen_axis)\n\n    def draw_stat(self):\n        if self.stat_test:\n            label = QGraphicsSimpleTextItem(self.stat_test)\n            brect = self.box_scene.sceneRect()\n            label.setPos(brect.center().x() - label.boundingRect().width()/2,\n                         8 + self._axis_font.pixelSize()*2)\n            label.setFlag(QGraphicsItem.ItemIgnoresTransformations)\n            self.box_scene.addItem(label)\n\n    def draw_axis_disc(self):\n        \"\"\"\n        Draw the horizontal axis and sets self.scale_x for discrete attributes\n        \"\"\"\n        assert not self.is_continuous\n        if self.show_stretched:\n            if not self.attr_labels:\n                return\n            step = steps = 10\n        else:\n            if self.group_var:\n                max_box = max(float(np.sum(dist))\n                              for dist in self.conts.array_with_unknowns)\n            else:\n                max_box = float(np.sum(self.dist.array_with_unknowns))\n            if max_box == 0:\n                self.scale_x = 1\n                return\n            _, step = compute_scale(0, max_box)\n            step = int(step) if step > 1 else 1\n            steps = int(math.ceil(max_box / step))\n        max_box = step * steps\n\n        bv = self.box_view\n        viewrect = bv.viewport().rect().adjusted(15, 15, -15, -30)\n        self.scene_width = viewrect.width()\n\n        lab_width = max(lab.boundingRect().width() for lab in self.attr_labels)\n        lab_width = max(lab_width, 40)\n        lab_width = min(lab_width, self.scene_width / 3)\n        self.label_width = lab_width\n\n        right_offset = 0  # offset for the right label\n        if not self.show_stretched and self.labels:\n            if self.group_var:\n                rows = list(zip(self.conts.array_with_unknowns, self.labels))\n            else:\n                rows = [(self.dist, self.labels[0])]\n            # available space left of the 'group labels'\n            available = self.scene_width - lab_width - 10\n            scale_x = (available - right_offset) / max_box\n            max_right = max(sum(dist) * scale_x + 10 +\n                            lbl.boundingRect().width()\n                            for dist, lbl in rows)\n            right_offset = max(0, max_right - max_box * scale_x)\n\n        self.scale_x = scale_x = \\\n            (self.scene_width - lab_width - 10 - right_offset) / max_box\n\n        self.box_scene.addLine(0, 0, max_box * scale_x, 0, self._pen_axis)\n        for val in range(0, step * steps + 1, step):\n            l = self.box_scene.addLine(val * scale_x, -1, val * scale_x, 1,\n                                       self._pen_axis_tick)\n            l.setZValue(100)\n            t = self.box_scene.addSimpleText(str(val), self._axis_font)\n            t.setPos(val * scale_x - t.boundingRect().width() / 2, 8)\n        if self.show_stretched:\n            self.scale_x *= 100\n\n    def label_group(self, stat, attr, mean_lab):\n        def centered_text(val, pos):\n            t = QGraphicsSimpleTextItem(attr.str_val(val), labels)\n            t.setFont(self._label_font)\n            bbox = t.boundingRect()\n            t.setPos(pos - bbox.width() / 2, 22)\n            return t\n\n        def line(x, down=1):\n            QGraphicsLineItem(x, 12 * down, x, 20 * down, labels)\n\n        def move_label(label, frm, to):\n            label.setX(to)\n            to += t_box.width() / 2\n            path = QPainterPath()\n            path.lineTo(0, 4)\n            path.lineTo(to - frm, 4)\n            path.lineTo(to - frm, 8)\n            p = QGraphicsPathItem(path)\n            p.setPos(frm, 12)\n            labels.addToGroup(p)\n\n        labels = QGraphicsItemGroup()\n\n        labels.addToGroup(mean_lab)\n        m = stat.mean * self.scale_x\n        mean_lab.setPos(m, -22)\n        line(m, -1)\n\n        if stat.median is not None:\n            msc = stat.median * self.scale_x\n            med_t = centered_text(stat.median, msc)\n            med_box_width2 = med_t.boundingRect().width() / 2\n            line(msc)\n\n        if stat.q25 is not None:\n            x = stat.q25 * self.scale_x\n            t = centered_text(stat.q25, x)\n            t_box = t.boundingRect()\n            med_left = msc - med_box_width2\n            if x + t_box.width() / 2 >= med_left - 5:\n                move_label(t, x, med_left - t_box.width() - 5)\n            else:\n                line(x)\n\n        if stat.q75 is not None:\n            x = stat.q75 * self.scale_x\n            t = centered_text(stat.q75, x)\n            t_box = t.boundingRect()\n            med_right = msc + med_box_width2\n            if x - t_box.width() / 2 <= med_right + 5:\n                move_label(t, x, med_right + 5)\n            else:\n                line(x)\n\n        return labels\n\n    def box_group(self, stat, height=20):\n        def line(x0, y0, x1, y1, *args):\n            return QGraphicsLineItem(x0 * scale_x, y0, x1 * scale_x, y1, *args)\n\n        scale_x = self.scale_x\n        box = []\n        whisker1 = line(stat.a_min, -1.5, stat.a_min, 1.5)\n        whisker2 = line(stat.a_max, -1.5, stat.a_max, 1.5)\n        vert_line = line(stat.a_min, 0, stat.a_max, 0)\n        mean_line = line(stat.mean, -height / 3, stat.mean, height / 3)\n        for it in (whisker1, whisker2, mean_line):\n            it.setPen(self._pen_paramet)\n        vert_line.setPen(self._pen_dotted)\n        var_line = line(stat.mean - stat.dev, 0, stat.mean + stat.dev, 0)\n        var_line.setPen(self._pen_paramet)\n        box.extend([whisker1, whisker2, vert_line, mean_line, var_line])\n        if stat.q25 is not None or stat.q75 is not None:\n            # if any of them is None it means that its value is equal to median\n            box_from = stat.q25 or stat.median\n            box_to = stat.q75 or stat.median\n            mbox = FilterGraphicsRectItem(\n                stat.conditions, box_from * scale_x, -height / 2,\n                (box_to - box_from) * scale_x, height)\n            mbox.setBrush(self._box_brush)\n            mbox.setPen(QPen(Qt.NoPen))\n            mbox.setZValue(-200)\n            box.append(mbox)\n\n        if stat.median is not None:\n            median_line = line(stat.median, -height / 2,\n                               stat.median, height / 2)\n            median_line.setPen(self._pen_median)\n            median_line.setZValue(-150)\n            box.append(median_line)\n\n        return box\n\n    def strudel(self, dist, group_val_index=None):\n        attr = self.attribute\n        ss = np.sum(dist)\n        box = []\n        if ss < 1e-6:\n            cond = [FilterDiscrete(attr, None)]\n            if group_val_index is not None:\n                cond.append(FilterDiscrete(self.group_var, [group_val_index]))\n            box.append(FilterGraphicsRectItem(cond, 0, -10, 1, 10))\n        cum = 0\n        values = attr.values + [\"Missing values\"]\n        colors = np.vstack((attr.colors, [128, 128, 128]))\n        for i, v in enumerate(dist):\n            if v < 1e-6:\n                continue\n            if self.show_stretched:\n                v /= ss\n            v *= self.scale_x\n            cond = [FilterDiscrete(attr, [i])]\n            if group_val_index is not None:\n                cond.append(FilterDiscrete(self.group_var, [group_val_index]))\n            rect = FilterGraphicsRectItem(cond, cum + 1, -6, v - 2, 12)\n            rect.setBrush(QBrush(QColor(*colors[i])))\n            rect.setPen(QPen(Qt.NoPen))\n            if self.show_stretched:\n                tooltip = \"{}: {:.2f}%\".format(\n                    values[i],\n                    100 * dist[i] / sum(dist))\n            else:\n                tooltip = \"{}: {}\".format(values[i], int(dist[i]))\n            rect.setToolTip(tooltip)\n            text = QGraphicsTextItem(values[i])\n            box.append(rect)\n            box.append(text)\n            cum += v\n        return box\n\n    def commit(self):\n        self.conditions = [item.filter for item in\n                           self.box_scene.selectedItems() if item.filter]\n        selected, selection = None, []\n        if self.conditions:\n            selected = Values(self.conditions, conjunction=False)(self.dataset)\n            selection = np.in1d(\n                self.dataset.ids, selected.ids, assume_unique=True).nonzero()[0]\n        self.Outputs.selected_data.send(selected)\n        self.Outputs.annotated_data.send(\n            create_annotated_table(self.dataset, selection))\n\n    def _show_posthoc(self):\n        def line(y0, y1):\n            it = self.box_scene.addLine(x, y0, x, y1, self._post_line_pen)\n            it.setZValue(-100)\n            self.posthoc_lines.append(it)\n\n        while self.posthoc_lines:\n            self.box_scene.removeItem(self.posthoc_lines.pop())\n\n        if self.compare == OWBoxPlot.CompareNone or len(self.stats) < 2:\n            return\n\n        if self.compare == OWBoxPlot.CompareMedians:\n            crit_line = \"median\"\n        else:\n            crit_line = \"mean\"\n\n        xs = []\n\n        height = 90 if self.show_annotations else 60\n\n        y_up = -len(self.stats) * height + 10\n        for pos, box_index in enumerate(self.order):\n            stat = self.stats[box_index]\n            x = getattr(stat, crit_line)\n            if x is None:\n                continue\n            x *= self.scale_x\n            xs.append(x * self.scale_x)\n            by = y_up + pos * height\n            line(by + 12, 0)\n\n        used_to = []\n        last_to = to = 0\n        for frm, frm_x in enumerate(xs[:-1]):\n            for to in range(frm + 1, len(xs)):\n                if xs[to] - frm_x > 1.5:\n                    to -= 1\n                    break\n            if to in (last_to, frm):\n                continue\n            for rowi, used in enumerate(used_to):\n                if used < frm:\n                    used_to[rowi] = to\n                    break\n            else:\n                rowi = len(used_to)\n                used_to.append(to)\n            y = - 6 - rowi * 6\n            it = self.box_scene.addLine(frm_x - 2, y, xs[to] + 2, y,\n                                        self._post_grp_pen)\n            self.posthoc_lines.append(it)\n            last_to = to\n\n    def get_widget_name_extension(self):\n        return self.attribute.name if self.attribute else None\n\n    def send_report(self):\n        self.report_plot()\n        text = \"\"\n        if self.attribute:\n            text += \"Box plot for attribute '{}' \".format(self.attribute.name)\n        if self.group_var:\n            text += \"grouped by '{}'\".format(self.group_var.name)\n        if text:\n            self.report_caption(text)\n\n    class Label(QGraphicsSimpleTextItem):\n        \"\"\"Boxplot Label with settable maxWidth\"\"\"\n        # Minimum width to display label text\n        MIN_LABEL_WIDTH = 25\n\n        # padding bellow the text\n        PADDING = 3\n\n        __max_width = None\n\n        def maxWidth(self):\n            return self.__max_width\n\n        def setMaxWidth(self, max_width):\n            self.__max_width = max_width\n\n        def paint(self, painter, option, widget):\n            \"\"\"Overrides QGraphicsSimpleTextItem.paint\n\n            If label text is too long, it is elided\n            to fit into the allowed region\n            \"\"\"\n            if self.__max_width is None:\n                width = option.rect.width()\n            else:\n                width = self.__max_width\n\n            if width < self.MIN_LABEL_WIDTH:\n                # if space is too narrow, no label\n                return\n\n            fm = painter.fontMetrics()\n            text = fm.elidedText(self.text(), Qt.ElideRight, width)\n            painter.drawText(\n                option.rect.x(),\n                option.rect.y() + self.boundingRect().height() - self.PADDING,\n                text)",
                                "ast_type_str": "ClassDef",
                                "start_lineno": 108,
                                "end_lineno": 1216,
                                "parent": null
                            }
                        },
                        {
                            "file_name": "Orange/widgets/visualize/owboxplot.py",
                            "type": "method",
                            "name": "OWBoxPlot.reset_attrs",
                            "content": "class OWBoxPlot(widget.OWWidget):\n    \"\"\"\n    Here's how the widget's functions call each other:\n\n    - `set_data` is a signal handler fills the list boxes and calls\n    `grouping_changed`.\n\n    - `grouping_changed` handles changes of grouping attribute: it enables or\n    disables the box for ordering, orders attributes and calls `attr_changed`.\n\n    - `attr_changed` handles changes of attribute. It recomputes box data by\n    calling `compute_box_data`, shows the appropriate display box\n    (discrete/continuous) and then calls`layout_changed`\n\n    - `layout_changed` constructs all the elements for the scene (as lists of\n    QGraphicsItemGroup) and calls `display_changed`. It is called when the\n    attribute or grouping is changed (by attr_changed) and on resize event.\n\n    - `display_changed` puts the elements corresponding to the current display\n    settings on the scene. It is called when the elements are reconstructed\n    (layout is changed due to selection of attributes or resize event), or\n    when the user changes display settings or colors.\n\n    For discrete attributes, the flow is a bit simpler: the elements are not\n    constructed in advance (by layout_changed). Instead, _display_changed_disc\n    draws everything.\n    \"\"\"\n    name = \"Box Plot\"\n    description = \"Visualize the distribution of feature values in a box plot.\"\n    icon = \"icons/BoxPlot.svg\"\n    priority = 100\n    keywords = [\"whisker\"]\n\n    class Inputs:\n        data = Input(\"Data\", Orange.data.Table)\n\n    class Outputs:\n        selected_data = Output(\"Selected Data\", Orange.data.Table, default=True)\n        annotated_data = Output(ANNOTATED_DATA_SIGNAL_NAME, Orange.data.Table)\n\n    #: Comparison types for continuous variables\n    CompareNone, CompareMedians, CompareMeans = 0, 1, 2\n\n    settingsHandler = DomainContextHandler()\n    conditions = ContextSetting([])\n\n    attribute = ContextSetting(None)\n    order_by_importance = Setting(False)\n    order_grouping_by_importance = Setting(False)\n    group_var = ContextSetting(None)\n    show_annotations = Setting(True)\n    compare = Setting(CompareMeans)\n    stattest = Setting(0)\n    sig_threshold = Setting(0.05)\n    stretched = Setting(True)\n    show_labels = Setting(True)\n    sort_freqs = Setting(False)\n\n    _sorting_criteria_attrs = {\n        CompareNone: \"\", CompareMedians: \"median\", CompareMeans: \"mean\"\n    }\n\n    _pen_axis_tick = QPen(Qt.white, 5)\n    _pen_axis = QPen(Qt.darkGray, 3)\n    _pen_median = QPen(QBrush(QColor(0xff, 0xff, 0x00)), 2)\n    _pen_paramet = QPen(QBrush(QColor(0x33, 0x00, 0xff)), 2)\n    _pen_dotted = QPen(QBrush(QColor(0x33, 0x00, 0xff)), 1)\n    _pen_dotted.setStyle(Qt.DotLine)\n    _post_line_pen = QPen(Qt.lightGray, 2)\n    _post_grp_pen = QPen(Qt.lightGray, 4)\n    for pen in (_pen_paramet, _pen_median, _pen_dotted,\n                _pen_axis, _pen_axis_tick, _post_line_pen, _post_grp_pen):\n        pen.setCosmetic(True)\n        pen.setCapStyle(Qt.RoundCap)\n        pen.setJoinStyle(Qt.RoundJoin)\n    _pen_axis_tick.setCapStyle(Qt.FlatCap)\n\n    _box_brush = QBrush(QColor(0x33, 0x88, 0xff, 0xc0))\n\n    _axis_font = QFont()\n    _axis_font.setPixelSize(12)\n    _label_font = QFont()\n    _label_font.setPixelSize(11)\n    _attr_brush = QBrush(QColor(0x33, 0x00, 0xff))\n\n    graph_name = \"box_scene\"\n\n    def __init__(self):\n        super().__init__()\n        self.stats = []\n        self.dataset = None\n        self.posthoc_lines = []\n\n        self.label_txts = self.mean_labels = self.boxes = self.labels = \\\n            self.label_txts_all = self.attr_labels = self.order = []\n        self.scale_x = 1\n        self.scene_min_x = self.scene_max_x = self.scene_width = 0\n        self.label_width = 0\n\n        self.attrs = VariableListModel()\n        view = gui.listView(\n            self.controlArea, self, \"attribute\", box=\"Variable\",\n            model=self.attrs, callback=self.attr_changed)\n        view.setMinimumSize(QSize(30, 30))\n        # Any other policy than Ignored will let the QListBox's scrollbar\n        # set the minimal height (see the penultimate paragraph of\n        # http://doc.qt.io/qt-4.8/qabstractscrollarea.html#addScrollBarWidget)\n        view.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Ignored)\n        gui.checkBox(\n            view.box, self, \"order_by_importance\",\n            \"Order by relevance to subgroups\",\n            tooltip=\"Order by 𝜒² or ANOVA over the subgroups\",\n            callback=self.apply_attr_sorting)\n        self.group_vars = VariableListModel(placeholder=\"None\")\n        view = gui.listView(\n            self.controlArea, self, \"group_var\", box=\"Subgroups\",\n            model=self.group_vars, callback=self.grouping_changed)\n        gui.checkBox(\n            view.box, self, \"order_grouping_by_importance\",\n            \"Order by relevance to variable\",\n            tooltip=\"Order by 𝜒² or ANOVA over the variable values\",\n            callback=self.on_group_sorting_checkbox)\n        view.setMinimumSize(QSize(30, 30))\n        # See the comment above\n        view.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Ignored)\n\n        # TODO: move Compare median/mean to grouping box\n        # The vertical size policy is needed to let only the list views expand\n        self.display_box = gui.vBox(\n            self.controlArea, \"Display\",\n            sizePolicy=(QSizePolicy.Minimum, QSizePolicy.Maximum),\n            addSpace=False)\n\n        gui.checkBox(self.display_box, self, \"show_annotations\", \"Annotate\",\n                     callback=self.display_changed)\n        self.compare_rb = gui.radioButtonsInBox(\n            self.display_box, self, 'compare',\n            btnLabels=[\"No comparison\", \"Compare medians\", \"Compare means\"],\n            callback=self.layout_changed)\n\n        # The vertical size policy is needed to let only the list views expand\n        self.stretching_box = box = gui.vBox(\n            self.controlArea, box=\"Display\",\n            sizePolicy=(QSizePolicy.Minimum, QSizePolicy.Fixed))\n        self.stretching_box.sizeHint = self.display_box.sizeHint\n        gui.checkBox(\n            box, self, 'stretched', \"Stretch bars\",\n            callback=self.display_changed)\n        gui.checkBox(\n            box, self, 'show_labels', \"Show box labels\",\n            callback=self.display_changed)\n        self.sort_cb = gui.checkBox(\n            box, self, 'sort_freqs', \"Sort by subgroup frequencies\",\n            callback=self.display_changed)\n\n        gui.vBox(self.mainArea, addSpace=True)\n        self.box_scene = QGraphicsScene()\n        self.box_scene.selectionChanged.connect(self.commit)\n        self.box_view = QGraphicsView(self.box_scene)\n        self.box_view.setRenderHints(QPainter.Antialiasing |\n                                     QPainter.TextAntialiasing |\n                                     QPainter.SmoothPixmapTransform)\n        self.box_view.viewport().installEventFilter(self)\n\n        self.mainArea.layout().addWidget(self.box_view)\n\n        gui.hBox(self.mainArea, addSpace=False)\n        self.stat_test = \"\"\n        self.mainArea.setMinimumWidth(300)\n\n        self.stats = self.dist = self.conts = []\n        self.is_continuous = False\n\n        self.update_display_box()\n\n    def sizeHint(self):\n        return QSize(900, 500)\n\n    def eventFilter(self, obj, event):\n        if obj is self.box_view.viewport() and \\\n                event.type() == QEvent.Resize:\n            self.layout_changed()\n\n        return super().eventFilter(obj, event)\n\n    @property\n    def show_stretched(self):\n        return self.stretched and self.group_var is not self.attribute\n\n    def reset_attrs(self):\n        domain = self.dataset.domain\n        self.attrs[:] = [\n            var for var in chain(\n                domain.class_vars, domain.metas, domain.attributes)\n            if var.is_primitive()]\n\n    def reset_groups(self):\n        domain = self.dataset.domain\n        self.group_vars[:] = [None] + [\n            var for var in chain(\n                domain.class_vars, domain.metas, domain.attributes)\n            if var.is_discrete]\n\n    # noinspection PyTypeChecker\n    @Inputs.data\n    def set_data(self, dataset):\n        if dataset is not None and (\n                not bool(dataset) or not len(dataset.domain) and not\n                any(var.is_primitive() for var in dataset.domain.metas)):\n            dataset = None\n        self.closeContext()\n        self.dataset = dataset\n        self.dist = self.stats = self.conts = []\n        self.group_var = None\n        self.attribute = None\n        if dataset:\n            self.reset_attrs()\n            self.reset_groups()\n            self.select_default_variables()\n            self.openContext(self.dataset)\n            self.grouping_changed()\n            self.attr_changed()\n        else:\n            self.reset_all_data()\n        self.commit()\n\n    def select_default_variables(self):\n        # visualize first non-class variable, group by class (if present)\n        domain = self.dataset.domain\n        if len(self.attrs) > len(domain.class_vars):\n            self.attribute = self.attrs[len(domain.class_vars)]\n        elif self.attrs:\n            self.attribute = self.attrs[0]\n\n        if domain.class_var and domain.class_var.is_discrete:\n            self.group_var = domain.class_var\n        else:\n            self.group_var = None  # Reset to trigger selection via callback\n\n    def apply_attr_sorting(self):\n        def compute_score(attr):\n            # This function and the one in apply_group_sorting are similar, but\n            # different in too many details, so they are kept as separate\n            # functions.\n            # If you discover a bug in this function, check the other one, too.\n            if attr is group_var:\n                return 3\n            if attr.is_continuous:\n                # One-way ANOVA\n                col = data.get_column_view(attr)[0].astype(float)\n                groups = (col[group_col == i] for i in range(n_groups))\n                groups = (col[~np.isnan(col)] for col in groups)\n                groups = [group for group in groups if len(group)]\n                p = f_oneway(*groups)[1] if len(groups) > 1 else 2\n            else:\n                p = self._chi_square(group_var, attr)[1]\n            if math.isnan(p):\n                return 2\n            return p\n\n        data = self.dataset\n        if data is None:\n            return\n        domain = data.domain\n        attribute = self.attribute\n        group_var = self.group_var\n        if self.order_by_importance and group_var is not None:\n            n_groups = len(group_var.values)\n            group_col = data.get_column_view(group_var)[0] if \\\n                domain.has_continuous_attributes(\n                    include_class=True, include_metas=True) else None\n            self.attrs.sort(key=compute_score)\n        else:\n            self.reset_attrs()\n        self.attribute = attribute  # reset selection\n        self._ensure_selection_visible(self.controls.attribute)\n\n    def on_group_sorting_checkbox(self):\n        if self.order_grouping_by_importance:\n            self.apply_group_sorting()\n        else:\n            self.reset_groups()\n            self.group_var = self.group_var  # reset selection\n            self._ensure_selection_visible(self.controls.group_var)\n\n    def apply_group_sorting(self):\n        def compute_stat(group):\n            # This function and the one in apply_attr_sorting are similar, but\n            # different in too many details, so they are kept as separate\n            # functions.\n            # If you discover a bug in this function, check the other one, too.\n            if group is attr:\n                return 3\n            if group is None:\n                return -1\n            if attr.is_continuous:\n                group_col = data.get_column_view(group)[0].astype(float)\n                groups = (attr_col[group_col == i]\n                          for i in range(len(group.values)))\n                groups = (col[~np.isnan(col)] for col in groups)\n                groups = [group for group in groups if len(group)]\n                p = f_oneway(*groups)[1] if len(groups) > 1 else 2\n            else:\n                p = self._chi_square(group, attr)[1]\n            if math.isnan(p):\n                return 2\n            return p\n\n        data = self.dataset\n        if data is None or not self.order_grouping_by_importance:\n            return\n        attr = self.attribute\n        group_var = self.group_var\n        if attr.is_continuous:\n            attr_col = data.get_column_view(attr)[0].astype(float)\n        self.group_vars.sort(key=compute_stat)\n        self.group_var = group_var  # reset selection\n        self._ensure_selection_visible(self.controls.group_var)\n\n    @staticmethod\n    def _ensure_selection_visible(view):\n        selection = view.selectedIndexes()\n        if len(selection) == 1:\n            view.scrollTo(selection[0])\n\n    def _chi_square(self, group_var, attr):\n        # Chi-square with the given distribution into groups\n        if not attr.values or not group_var.values:\n            return 0, 2, 0\n        observed = np.array(\n            contingency.get_contingency(self.dataset, group_var, attr))\n        observed = observed[observed.sum(axis=1) != 0, :]\n        observed = observed[:, observed.sum(axis=0) != 0]\n        if min(observed.shape) < 2:\n            return 0, 2, 0\n        return chi2_contingency(observed)[:3]\n\n    def reset_all_data(self):\n        self.clear_scene()\n        self.stat_test = \"\"\n        self.attrs[:] = []\n        self.group_vars[:] = [None]\n        self.is_continuous = False\n        self.update_display_box()\n\n    def grouping_changed(self):\n        self.controls.stretched.setDisabled(self.group_var is self.attribute)\n        self.apply_attr_sorting()\n        self.update_graph()\n\n    def select_box_items(self):\n        temp_cond = self.conditions.copy()\n        for box in self.box_scene.items():\n            if isinstance(box, FilterGraphicsRectItem):\n                box.setSelected(box.filter.conditions in\n                                [c.conditions for c in temp_cond])\n\n    def attr_changed(self):\n        self.controls.stretched.setDisabled(self.group_var is self.attribute)\n        self.apply_group_sorting()\n        self.update_graph()\n\n    def update_graph(self):\n        self.compute_box_data()\n        self.update_display_box()\n        self.layout_changed()\n\n        if self.is_continuous:\n            heights = 90 if self.show_annotations else 60\n            self.box_view.centerOn(self.scene_min_x + self.scene_width / 2,\n                                   -30 - len(self.stats) * heights / 2 + 45)\n        else:\n            self.box_view.centerOn(self.scene_width / 2,\n                                   -30 - len(self.boxes) * 40 / 2 + 45)\n\n    def compute_box_data(self):\n        attr = self.attribute\n        if not attr:\n            return\n        dataset = self.dataset\n        self.is_continuous = attr.is_continuous\n        if dataset is None or not self.is_continuous and not attr.values or \\\n                        self.group_var and not self.group_var.values:\n            self.stats = self.dist = self.conts = []\n            return\n        if self.group_var:\n            self.dist = []\n            self.conts = contingency.get_contingency(\n                dataset, attr, self.group_var)\n            if self.is_continuous:\n                stats, label_texts = [], []\n                for i, cont in enumerate(self.conts):\n                    if np.sum(cont[1]):\n                        stats.append(BoxData(cont, attr, i, self.group_var))\n                        label_texts.append(self.group_var.values[i])\n                self.stats = stats\n                self.label_txts_all = label_texts\n            else:\n                self.label_txts_all = \\\n                    [v for v, c in zip(\n                        self.group_var.values + [\"Missing values\"],\n                        self.conts.array_with_unknowns)\n                     if np.sum(c) > 0]\n        else:\n            self.dist = distribution.get_distribution(dataset, attr)\n            self.conts = []\n            if self.is_continuous:\n                self.stats = [BoxData(self.dist, attr, None)]\n            self.label_txts_all = [\"\"]\n        self.label_txts = [txts for stat, txts in zip(self.stats,\n                                                      self.label_txts_all)\n                           if stat.n > 0]\n        self.stats = [stat for stat in self.stats if stat.n > 0]\n\n    def update_display_box(self):\n        if self.is_continuous:\n            self.stretching_box.hide()\n            self.display_box.show()\n            self.compare_rb.setEnabled(self.group_var is not None)\n        else:\n            self.stretching_box.show()\n            self.display_box.hide()\n            self.sort_cb.setEnabled(self.group_var is not None)\n\n    def clear_scene(self):\n        self.closeContext()\n        self.box_scene.clearSelection()\n        self.box_scene.clear()\n        self.box_view.viewport().update()\n        self.attr_labels = []\n        self.labels = []\n        self.boxes = []\n        self.mean_labels = []\n        self.posthoc_lines = []\n        self.openContext(self.dataset)\n\n    def layout_changed(self):\n        attr = self.attribute\n        if not attr:\n            return\n        self.clear_scene()\n        if self.dataset is None or len(self.conts) == len(self.dist) == 0:\n            return\n\n        if self.is_continuous:\n            self.mean_labels = [self.mean_label(stat, attr, lab)\n                                for stat, lab in zip(self.stats, self.label_txts)]\n            self.draw_axis()\n            self.boxes = [self.box_group(stat) for stat in self.stats]\n            self.labels = [self.label_group(stat, attr, mean_lab)\n                           for stat, mean_lab in zip(self.stats, self.mean_labels)]\n            self.attr_labels = [QGraphicsSimpleTextItem(lab)\n                                for lab in self.label_txts]\n            for it in chain(self.labels, self.attr_labels):\n                self.box_scene.addItem(it)\n\n        self.display_changed()\n\n    def display_changed(self):\n        if self.dataset is None or self.attribute is None:\n            return\n\n        if self.is_continuous:\n            self._display_changed_cont()\n        else:\n            self._display_changed_disc()\n        self.draw_stat()\n        self.select_box_items()\n\n    def _display_changed_cont(self):\n        self.order = list(range(len(self.stats)))\n        criterion = self._sorting_criteria_attrs[self.compare]\n        if criterion:\n            vals = [getattr(stat, criterion) for stat in self.stats]\n            overmax = max((val for val in vals if val is not None), default=0) \\\n                      + 1\n            vals = [val if val is not None else overmax for val in vals]\n            self.order = sorted(self.order, key=vals.__getitem__)\n\n        heights = 90 if self.show_annotations else 60\n\n        for row, box_index in enumerate(self.order):\n            y = (-len(self.stats) + row) * heights + 10\n            for item in self.boxes[box_index]:\n                self.box_scene.addItem(item)\n                item.setY(y)\n            labels = self.labels[box_index]\n\n            if self.show_annotations:\n                labels.show()\n                labels.setY(y)\n            else:\n                labels.hide()\n\n            label = self.attr_labels[box_index]\n            label.setY(y - 15 - label.boundingRect().height())\n            if self.show_annotations:\n                label.hide()\n            else:\n                stat = self.stats[box_index]\n\n                if self.compare == OWBoxPlot.CompareMedians and \\\n                        stat.median is not None:\n                    pos = stat.median + 5 / self.scale_x\n                elif self.compare == OWBoxPlot.CompareMeans or stat.q25 is None:\n                    pos = stat.mean + 5 / self.scale_x\n                else:\n                    pos = stat.q25\n                label.setX(pos * self.scale_x)\n                label.show()\n\n        r = QRectF(self.scene_min_x, -30 - len(self.stats) * heights,\n                   self.scene_width, len(self.stats) * heights + 90)\n        self.box_scene.setSceneRect(r)\n\n        self._compute_tests_cont()\n        self._show_posthoc()\n\n    def _display_changed_disc(self):\n        self.clear_scene()\n        self.attr_labels = [QGraphicsSimpleTextItem(lab)\n                            for lab in self.label_txts_all]\n\n        if not self.show_stretched:\n            if self.group_var:\n                self.labels = [\n                    QGraphicsTextItem(\"{}\".format(int(sum(cont))))\n                    for cont in self.conts.array_with_unknowns\n                    if np.sum(cont) > 0]\n            else:\n                self.labels = [\n                    QGraphicsTextItem(str(int(sum(self.dist))))]\n\n        self.order = list(range(len(self.attr_labels)))\n\n        self.draw_axis_disc()\n        if self.group_var:\n            self.boxes = \\\n                [self.strudel(cont, i)\n                 for i, cont in enumerate(self.conts.array_with_unknowns)\n                 if np.sum(cont) > 0]\n\n            if self.sort_freqs:\n                # pylint: disable=invalid-unary-operand-type\n                self.order = sorted(\n                    self.order, key=(-np.sum(\n                        self.conts.array_with_unknowns, axis=1)).__getitem__)\n        else:\n            self.boxes = [self.strudel(self.dist.array_with_unknowns)]\n\n        for row, box_index in enumerate(self.order):\n            y = (-len(self.boxes) + row) * 40 + 10\n            box = self.boxes[box_index]\n            bars, labels = box[::2], box[1::2]\n\n            self.__draw_group_labels(y, box_index)\n            if not self.show_stretched:\n                self.__draw_row_counts(y, box_index)\n            if self.show_labels and self.attribute is not self.group_var:\n                self.__draw_bar_labels(y, bars, labels)\n            self.__draw_bars(y, bars)\n\n        self.box_scene.setSceneRect(-self.label_width - 5,\n                                    -30 - len(self.boxes) * 40,\n                                    self.scene_width, len(self.boxes * 40) + 90)\n        self._compute_tests_disc()\n\n    def __draw_group_labels(self, y, row):\n        \"\"\"Draw group labels\n\n        Parameters\n        ----------\n        y: int\n            vertical offset of bars\n        row: int\n            row index\n        \"\"\"\n        label = self.attr_labels[row]\n        b = label.boundingRect()\n        label.setPos(-b.width() - 10, y - b.height() / 2)\n        self.box_scene.addItem(label)\n\n    def __draw_row_counts(self, y, row):\n        \"\"\"Draw row counts\n\n        Parameters\n        ----------\n        y: int\n            vertical offset of bars\n        row: int\n            row index\n        \"\"\"\n        assert not self.is_continuous\n        label = self.labels[row]\n        b = label.boundingRect()\n        if self.group_var:\n            right = self.scale_x * sum(self.conts.array_with_unknowns[row])\n        else:\n            right = self.scale_x * sum(self.dist)\n        label.setPos(right + 10, y - b.height() / 2)\n        self.box_scene.addItem(label)\n\n    def __draw_bar_labels(self, y, bars, labels):\n        \"\"\"Draw bar labels\n\n        Parameters\n        ----------\n        y: int\n            vertical offset of bars\n        bars: List[FilterGraphicsRectItem]\n            list of bars being drawn\n        labels: List[QGraphicsTextItem]\n            list of labels for corresponding bars\n        \"\"\"\n        label = bar_part = None\n        for text_item, bar_part in zip(labels, bars):\n            label = self.Label(\n                text_item.toPlainText())\n            label.setPos(bar_part.boundingRect().x(),\n                         y - label.boundingRect().height() - 8)\n            label.setMaxWidth(bar_part.boundingRect().width())\n            self.box_scene.addItem(label)\n\n    def __draw_bars(self, y, bars):\n        \"\"\"Draw bars\n\n        Parameters\n        ----------\n        y: int\n            vertical offset of bars\n\n        bars: List[FilterGraphicsRectItem]\n            list of bars to draw\n        \"\"\"\n        for item in bars:\n            item.setPos(0, y)\n            self.box_scene.addItem(item)\n\n    # noinspection PyPep8Naming\n    def _compute_tests_cont(self):\n        # The t-test and ANOVA are implemented here since they efficiently use\n        # the widget-specific data in self.stats.\n        # The non-parametric tests can't do this, so we use statistics.tests\n\n        # pylint: disable=comparison-with-itself\n        def stat_ttest():\n            d1, d2 = self.stats\n            if d1.n < 2 or d2.n < 2:\n                return np.nan, np.nan\n            pooled_var = d1.var / d1.n + d2.var / d2.n\n            # pylint: disable=comparison-with-itself\n            if pooled_var == 0 or np.isnan(pooled_var):\n                return np.nan, np.nan\n            df = pooled_var ** 2 / \\\n                ((d1.var / d1.n) ** 2 / (d1.n - 1) +\n                 (d2.var / d2.n) ** 2 / (d2.n - 1))\n            t = abs(d1.mean - d2.mean) / math.sqrt(pooled_var)\n            p = 2 * (1 - scipy.special.stdtr(df, t))\n            return t, p\n\n        # TODO: Check this function\n        # noinspection PyPep8Naming\n        def stat_ANOVA():\n            if any(stat.n == 0 for stat in self.stats):\n                return np.nan, np.nan\n            n = sum(stat.n for stat in self.stats)\n            grand_avg = sum(stat.n * stat.mean for stat in self.stats) / n\n            var_between = sum(stat.n * (stat.mean - grand_avg) ** 2\n                              for stat in self.stats)\n            df_between = len(self.stats) - 1\n\n            var_within = sum(stat.n * stat.var for stat in self.stats)\n            df_within = n - len(self.stats)\n            if var_within == 0 or df_within == 0 or df_between == 0:\n                return np.nan, np.nan\n            F = (var_between / df_between) / (var_within / df_within)\n            p = 1 - scipy.special.fdtr(df_between, df_within, F)\n            return F, p\n\n        n = len(self.dataset)\n        if self.compare == OWBoxPlot.CompareNone or len(self.stats) < 2:\n            t = \"\"\n        elif any(s.n <= 1 for s in self.stats):\n            t = \"At least one group has just one instance, \" \\\n                \"cannot compute significance\"\n        elif len(self.stats) == 2:\n            if self.compare == OWBoxPlot.CompareMedians:\n                t = \"\"\n                # z, p = tests.wilcoxon_rank_sum(\n                #    self.stats[0].dist, self.stats[1].dist)\n                # t = \"Mann-Whitney's z: %.1f (p=%.3f)\" % (z, p)\n            else:\n                t, p = stat_ttest()\n                t = \"\" if np.isnan(t) else f\"Student's t: {t:.3f} (p={p:.3f}, N={n})\"\n        else:\n            if self.compare == OWBoxPlot.CompareMedians:\n                t = \"\"\n                # U, p = -1, -1\n                # t = \"Kruskal Wallis's U: %.1f (p=%.3f)\" % (U, p)\n            else:\n                F, p = stat_ANOVA()\n                t = \"\" if np.isnan(F) else f\"ANOVA: {F:.3f} (p={p:.3f}, N={n})\"\n        self.stat_test = t\n\n    def _compute_tests_disc(self):\n        if self.group_var is None or self.attribute is None:\n            self.stat_test = \"\"\n        else:\n            chi, p, dof = self._chi_square(self.group_var, self.attribute)\n            if np.isnan(p):\n                self.stat_test = \"\"\n            else:\n                self.stat_test = f\"χ²: {chi:.2f} (p={p:.3f}, dof={dof})\"\n\n    def mean_label(self, stat, attr, val_name):\n        label = QGraphicsItemGroup()\n        t = QGraphicsSimpleTextItem(attr.str_val(stat.mean), label)\n        t.setFont(self._label_font)\n        bbox = t.boundingRect()\n        w2, h = bbox.width() / 2, bbox.height()\n        t.setPos(-w2, -h)\n        tpm = QGraphicsSimpleTextItem(\n            \" \\u00b1 \" + \"%.*f\" % (attr.number_of_decimals + 1, stat.dev),\n            label)\n        tpm.setFont(self._label_font)\n        tpm.setPos(w2, -h)\n        if val_name:\n            vnm = QGraphicsSimpleTextItem(val_name + \": \", label)\n            vnm.setFont(self._label_font)\n            vnm.setBrush(self._attr_brush)\n            vb = vnm.boundingRect()\n            label.min_x = -w2 - vb.width()\n            vnm.setPos(label.min_x, -h)\n        else:\n            label.min_x = -w2\n        return label\n\n    def draw_axis(self):\n        \"\"\"Draw the horizontal axis and sets self.scale_x\"\"\"\n        misssing_stats = not self.stats\n        stats = self.stats or [BoxData(np.array([[0.], [1.]]), self.attribute)]\n        mean_labels = self.mean_labels or [self.mean_label(stats[0], self.attribute, \"\")]\n        bottom = min(stat.a_min for stat in stats)\n        top = max(stat.a_max for stat in stats)\n\n        first_val, step = compute_scale(bottom, top)\n        while bottom <= first_val:\n            first_val -= step\n        bottom = first_val\n        no_ticks = math.ceil((top - first_val) / step) + 1\n        top = max(top, first_val + no_ticks * step)\n\n        gbottom = min(bottom, min(stat.mean - stat.dev for stat in stats))\n        gtop = max(top, max(stat.mean + stat.dev for stat in stats))\n\n        bv = self.box_view\n        viewrect = bv.viewport().rect().adjusted(15, 15, -15, -30)\n        self.scale_x = scale_x = viewrect.width() / (gtop - gbottom)\n\n        # In principle we should repeat this until convergence since the new\n        # scaling is too conservative. (No chance am I doing this.)\n        mlb = min(stat.mean + mean_lab.min_x / scale_x\n                  for stat, mean_lab in zip(stats, mean_labels))\n        if mlb < gbottom:\n            gbottom = mlb\n            self.scale_x = scale_x = viewrect.width() / (gtop - gbottom)\n\n        self.scene_min_x = gbottom * scale_x\n        self.scene_max_x = gtop * scale_x\n        self.scene_width = self.scene_max_x - self.scene_min_x\n\n        val = first_val\n        last_text = self.scene_min_x\n        while True:\n            l = self.box_scene.addLine(val * scale_x, -1, val * scale_x, 1,\n                                       self._pen_axis_tick)\n            l.setZValue(100)\n            t = QGraphicsSimpleTextItem(\n                self.attribute.str_val(val) if not misssing_stats else \"?\")\n            t.setFont(self._axis_font)\n            t.setFlag(QGraphicsItem.ItemIgnoresTransformations)\n            r = t.boundingRect()\n            x_start = val * scale_x - r.width() / 2\n            x_finish = x_start + r.width()\n            if x_start > last_text + 10 and x_finish < self.scene_max_x:\n                t.setPos(x_start, 8)\n                self.box_scene.addItem(t)\n                last_text = x_finish\n            if val >= top:\n                break\n            val += step\n        self.box_scene.addLine(\n            bottom * scale_x - 4, 0, top * scale_x + 4, 0, self._pen_axis)\n\n    def draw_stat(self):\n        if self.stat_test:\n            label = QGraphicsSimpleTextItem(self.stat_test)\n            brect = self.box_scene.sceneRect()\n            label.setPos(brect.center().x() - label.boundingRect().width()/2,\n                         8 + self._axis_font.pixelSize()*2)\n            label.setFlag(QGraphicsItem.ItemIgnoresTransformations)\n            self.box_scene.addItem(label)\n\n    def draw_axis_disc(self):\n        \"\"\"\n        Draw the horizontal axis and sets self.scale_x for discrete attributes\n        \"\"\"\n        assert not self.is_continuous\n        if self.show_stretched:\n            if not self.attr_labels:\n                return\n            step = steps = 10\n        else:\n            if self.group_var:\n                max_box = max(float(np.sum(dist))\n                              for dist in self.conts.array_with_unknowns)\n            else:\n                max_box = float(np.sum(self.dist.array_with_unknowns))\n            if max_box == 0:\n                self.scale_x = 1\n                return\n            _, step = compute_scale(0, max_box)\n            step = int(step) if step > 1 else 1\n            steps = int(math.ceil(max_box / step))\n        max_box = step * steps\n\n        bv = self.box_view\n        viewrect = bv.viewport().rect().adjusted(15, 15, -15, -30)\n        self.scene_width = viewrect.width()\n\n        lab_width = max(lab.boundingRect().width() for lab in self.attr_labels)\n        lab_width = max(lab_width, 40)\n        lab_width = min(lab_width, self.scene_width / 3)\n        self.label_width = lab_width\n\n        right_offset = 0  # offset for the right label\n        if not self.show_stretched and self.labels:\n            if self.group_var:\n                rows = list(zip(self.conts.array_with_unknowns, self.labels))\n            else:\n                rows = [(self.dist, self.labels[0])]\n            # available space left of the 'group labels'\n            available = self.scene_width - lab_width - 10\n            scale_x = (available - right_offset) / max_box\n            max_right = max(sum(dist) * scale_x + 10 +\n                            lbl.boundingRect().width()\n                            for dist, lbl in rows)\n            right_offset = max(0, max_right - max_box * scale_x)\n\n        self.scale_x = scale_x = \\\n            (self.scene_width - lab_width - 10 - right_offset) / max_box\n\n        self.box_scene.addLine(0, 0, max_box * scale_x, 0, self._pen_axis)\n        for val in range(0, step * steps + 1, step):\n            l = self.box_scene.addLine(val * scale_x, -1, val * scale_x, 1,\n                                       self._pen_axis_tick)\n            l.setZValue(100)\n            t = self.box_scene.addSimpleText(str(val), self._axis_font)\n            t.setPos(val * scale_x - t.boundingRect().width() / 2, 8)\n        if self.show_stretched:\n            self.scale_x *= 100\n\n    def label_group(self, stat, attr, mean_lab):\n        def centered_text(val, pos):\n            t = QGraphicsSimpleTextItem(attr.str_val(val), labels)\n            t.setFont(self._label_font)\n            bbox = t.boundingRect()\n            t.setPos(pos - bbox.width() / 2, 22)\n            return t\n\n        def line(x, down=1):\n            QGraphicsLineItem(x, 12 * down, x, 20 * down, labels)\n\n        def move_label(label, frm, to):\n            label.setX(to)\n            to += t_box.width() / 2\n            path = QPainterPath()\n            path.lineTo(0, 4)\n            path.lineTo(to - frm, 4)\n            path.lineTo(to - frm, 8)\n            p = QGraphicsPathItem(path)\n            p.setPos(frm, 12)\n            labels.addToGroup(p)\n\n        labels = QGraphicsItemGroup()\n\n        labels.addToGroup(mean_lab)\n        m = stat.mean * self.scale_x\n        mean_lab.setPos(m, -22)\n        line(m, -1)\n\n        if stat.median is not None:\n            msc = stat.median * self.scale_x\n            med_t = centered_text(stat.median, msc)\n            med_box_width2 = med_t.boundingRect().width() / 2\n            line(msc)\n\n        if stat.q25 is not None:\n            x = stat.q25 * self.scale_x\n            t = centered_text(stat.q25, x)\n            t_box = t.boundingRect()\n            med_left = msc - med_box_width2\n            if x + t_box.width() / 2 >= med_left - 5:\n                move_label(t, x, med_left - t_box.width() - 5)\n            else:\n                line(x)\n\n        if stat.q75 is not None:\n            x = stat.q75 * self.scale_x\n            t = centered_text(stat.q75, x)\n            t_box = t.boundingRect()\n            med_right = msc + med_box_width2\n            if x - t_box.width() / 2 <= med_right + 5:\n                move_label(t, x, med_right + 5)\n            else:\n                line(x)\n\n        return labels\n\n    def box_group(self, stat, height=20):\n        def line(x0, y0, x1, y1, *args):\n            return QGraphicsLineItem(x0 * scale_x, y0, x1 * scale_x, y1, *args)\n\n        scale_x = self.scale_x\n        box = []\n        whisker1 = line(stat.a_min, -1.5, stat.a_min, 1.5)\n        whisker2 = line(stat.a_max, -1.5, stat.a_max, 1.5)\n        vert_line = line(stat.a_min, 0, stat.a_max, 0)\n        mean_line = line(stat.mean, -height / 3, stat.mean, height / 3)\n        for it in (whisker1, whisker2, mean_line):\n            it.setPen(self._pen_paramet)\n        vert_line.setPen(self._pen_dotted)\n        var_line = line(stat.mean - stat.dev, 0, stat.mean + stat.dev, 0)\n        var_line.setPen(self._pen_paramet)\n        box.extend([whisker1, whisker2, vert_line, mean_line, var_line])\n        if stat.q25 is not None or stat.q75 is not None:\n            # if any of them is None it means that its value is equal to median\n            box_from = stat.q25 or stat.median\n            box_to = stat.q75 or stat.median\n            mbox = FilterGraphicsRectItem(\n                stat.conditions, box_from * scale_x, -height / 2,\n                (box_to - box_from) * scale_x, height)\n            mbox.setBrush(self._box_brush)\n            mbox.setPen(QPen(Qt.NoPen))\n            mbox.setZValue(-200)\n            box.append(mbox)\n\n        if stat.median is not None:\n            median_line = line(stat.median, -height / 2,\n                               stat.median, height / 2)\n            median_line.setPen(self._pen_median)\n            median_line.setZValue(-150)\n            box.append(median_line)\n\n        return box\n\n    def strudel(self, dist, group_val_index=None):\n        attr = self.attribute\n        ss = np.sum(dist)\n        box = []\n        if ss < 1e-6:\n            cond = [FilterDiscrete(attr, None)]\n            if group_val_index is not None:\n                cond.append(FilterDiscrete(self.group_var, [group_val_index]))\n            box.append(FilterGraphicsRectItem(cond, 0, -10, 1, 10))\n        cum = 0\n        values = attr.values + [\"Missing values\"]\n        colors = np.vstack((attr.colors, [128, 128, 128]))\n        for i, v in enumerate(dist):\n            if v < 1e-6:\n                continue\n            if self.show_stretched:\n                v /= ss\n            v *= self.scale_x\n            cond = [FilterDiscrete(attr, [i])]\n            if group_val_index is not None:\n                cond.append(FilterDiscrete(self.group_var, [group_val_index]))\n            rect = FilterGraphicsRectItem(cond, cum + 1, -6, v - 2, 12)\n            rect.setBrush(QBrush(QColor(*colors[i])))\n            rect.setPen(QPen(Qt.NoPen))\n            if self.show_stretched:\n                tooltip = \"{}: {:.2f}%\".format(\n                    values[i],\n                    100 * dist[i] / sum(dist))\n            else:\n                tooltip = \"{}: {}\".format(values[i], int(dist[i]))\n            rect.setToolTip(tooltip)\n            text = QGraphicsTextItem(values[i])\n            box.append(rect)\n            box.append(text)\n            cum += v\n        return box\n\n    def commit(self):\n        self.conditions = [item.filter for item in\n                           self.box_scene.selectedItems() if item.filter]\n        selected, selection = None, []\n        if self.conditions:\n            selected = Values(self.conditions, conjunction=False)(self.dataset)\n            selection = np.in1d(\n                self.dataset.ids, selected.ids, assume_unique=True).nonzero()[0]\n        self.Outputs.selected_data.send(selected)\n        self.Outputs.annotated_data.send(\n            create_annotated_table(self.dataset, selection))\n\n    def _show_posthoc(self):\n        def line(y0, y1):\n            it = self.box_scene.addLine(x, y0, x, y1, self._post_line_pen)\n            it.setZValue(-100)\n            self.posthoc_lines.append(it)\n\n        while self.posthoc_lines:\n            self.box_scene.removeItem(self.posthoc_lines.pop())\n\n        if self.compare == OWBoxPlot.CompareNone or len(self.stats) < 2:\n            return\n\n        if self.compare == OWBoxPlot.CompareMedians:\n            crit_line = \"median\"\n        else:\n            crit_line = \"mean\"\n\n        xs = []\n\n        height = 90 if self.show_annotations else 60\n\n        y_up = -len(self.stats) * height + 10\n        for pos, box_index in enumerate(self.order):\n            stat = self.stats[box_index]\n            x = getattr(stat, crit_line)\n            if x is None:\n                continue\n            x *= self.scale_x\n            xs.append(x * self.scale_x)\n            by = y_up + pos * height\n            line(by + 12, 0)\n\n        used_to = []\n        last_to = to = 0\n        for frm, frm_x in enumerate(xs[:-1]):\n            for to in range(frm + 1, len(xs)):\n                if xs[to] - frm_x > 1.5:\n                    to -= 1\n                    break\n            if to in (last_to, frm):\n                continue\n            for rowi, used in enumerate(used_to):\n                if used < frm:\n                    used_to[rowi] = to\n                    break\n            else:\n                rowi = len(used_to)\n                used_to.append(to)\n            y = - 6 - rowi * 6\n            it = self.box_scene.addLine(frm_x - 2, y, xs[to] + 2, y,\n                                        self._post_grp_pen)\n            self.posthoc_lines.append(it)\n            last_to = to\n\n    def get_widget_name_extension(self):\n        return self.attribute.name if self.attribute else None\n\n    def send_report(self):\n        self.report_plot()\n        text = \"\"\n        if self.attribute:\n            text += \"Box plot for attribute '{}' \".format(self.attribute.name)\n        if self.group_var:\n            text += \"grouped by '{}'\".format(self.group_var.name)\n        if text:\n            self.report_caption(text)\n\n    class Label(QGraphicsSimpleTextItem):\n        \"\"\"Boxplot Label with settable maxWidth\"\"\"\n        # Minimum width to display label text\n        MIN_LABEL_WIDTH = 25\n\n        # padding bellow the text\n        PADDING = 3\n\n        __max_width = None\n\n        def maxWidth(self):\n            return self.__max_width\n\n        def setMaxWidth(self, max_width):\n            self.__max_width = max_width\n\n        def paint(self, painter, option, widget):\n            \"\"\"Overrides QGraphicsSimpleTextItem.paint\n\n            If label text is too long, it is elided\n            to fit into the allowed region\n            \"\"\"\n            if self.__max_width is None:\n                width = option.rect.width()\n            else:\n                width = self.__max_width\n\n            if width < self.MIN_LABEL_WIDTH:\n                # if space is too narrow, no label\n                return\n\n            fm = painter.fontMetrics()\n            text = fm.elidedText(self.text(), Qt.ElideRight, width)\n            painter.drawText(\n                option.rect.x(),\n                option.rect.y() + self.boundingRect().height() - self.PADDING,\n                text)",
                            "ast_type_str": "ClassDef",
                            "start_lineno": 297,
                            "end_lineno": 302,
                            "parent": {
                                "file_name": "Orange/widgets/visualize/owboxplot.py",
                                "type": "class",
                                "name": "OWBoxPlot",
                                "content": "class OWBoxPlot(widget.OWWidget):\n    \"\"\"\n    Here's how the widget's functions call each other:\n\n    - `set_data` is a signal handler fills the list boxes and calls\n    `grouping_changed`.\n\n    - `grouping_changed` handles changes of grouping attribute: it enables or\n    disables the box for ordering, orders attributes and calls `attr_changed`.\n\n    - `attr_changed` handles changes of attribute. It recomputes box data by\n    calling `compute_box_data`, shows the appropriate display box\n    (discrete/continuous) and then calls`layout_changed`\n\n    - `layout_changed` constructs all the elements for the scene (as lists of\n    QGraphicsItemGroup) and calls `display_changed`. It is called when the\n    attribute or grouping is changed (by attr_changed) and on resize event.\n\n    - `display_changed` puts the elements corresponding to the current display\n    settings on the scene. It is called when the elements are reconstructed\n    (layout is changed due to selection of attributes or resize event), or\n    when the user changes display settings or colors.\n\n    For discrete attributes, the flow is a bit simpler: the elements are not\n    constructed in advance (by layout_changed). Instead, _display_changed_disc\n    draws everything.\n    \"\"\"\n    name = \"Box Plot\"\n    description = \"Visualize the distribution of feature values in a box plot.\"\n    icon = \"icons/BoxPlot.svg\"\n    priority = 100\n    keywords = [\"whisker\"]\n\n    class Inputs:\n        data = Input(\"Data\", Orange.data.Table)\n\n    class Outputs:\n        selected_data = Output(\"Selected Data\", Orange.data.Table, default=True)\n        annotated_data = Output(ANNOTATED_DATA_SIGNAL_NAME, Orange.data.Table)\n\n    #: Comparison types for continuous variables\n    CompareNone, CompareMedians, CompareMeans = 0, 1, 2\n\n    settingsHandler = DomainContextHandler()\n    conditions = ContextSetting([])\n\n    attribute = ContextSetting(None)\n    order_by_importance = Setting(False)\n    order_grouping_by_importance = Setting(False)\n    group_var = ContextSetting(None)\n    show_annotations = Setting(True)\n    compare = Setting(CompareMeans)\n    stattest = Setting(0)\n    sig_threshold = Setting(0.05)\n    stretched = Setting(True)\n    show_labels = Setting(True)\n    sort_freqs = Setting(False)\n\n    _sorting_criteria_attrs = {\n        CompareNone: \"\", CompareMedians: \"median\", CompareMeans: \"mean\"\n    }\n\n    _pen_axis_tick = QPen(Qt.white, 5)\n    _pen_axis = QPen(Qt.darkGray, 3)\n    _pen_median = QPen(QBrush(QColor(0xff, 0xff, 0x00)), 2)\n    _pen_paramet = QPen(QBrush(QColor(0x33, 0x00, 0xff)), 2)\n    _pen_dotted = QPen(QBrush(QColor(0x33, 0x00, 0xff)), 1)\n    _pen_dotted.setStyle(Qt.DotLine)\n    _post_line_pen = QPen(Qt.lightGray, 2)\n    _post_grp_pen = QPen(Qt.lightGray, 4)\n    for pen in (_pen_paramet, _pen_median, _pen_dotted,\n                _pen_axis, _pen_axis_tick, _post_line_pen, _post_grp_pen):\n        pen.setCosmetic(True)\n        pen.setCapStyle(Qt.RoundCap)\n        pen.setJoinStyle(Qt.RoundJoin)\n    _pen_axis_tick.setCapStyle(Qt.FlatCap)\n\n    _box_brush = QBrush(QColor(0x33, 0x88, 0xff, 0xc0))\n\n    _axis_font = QFont()\n    _axis_font.setPixelSize(12)\n    _label_font = QFont()\n    _label_font.setPixelSize(11)\n    _attr_brush = QBrush(QColor(0x33, 0x00, 0xff))\n\n    graph_name = \"box_scene\"\n\n    def __init__(self):\n        super().__init__()\n        self.stats = []\n        self.dataset = None\n        self.posthoc_lines = []\n\n        self.label_txts = self.mean_labels = self.boxes = self.labels = \\\n            self.label_txts_all = self.attr_labels = self.order = []\n        self.scale_x = 1\n        self.scene_min_x = self.scene_max_x = self.scene_width = 0\n        self.label_width = 0\n\n        self.attrs = VariableListModel()\n        view = gui.listView(\n            self.controlArea, self, \"attribute\", box=\"Variable\",\n            model=self.attrs, callback=self.attr_changed)\n        view.setMinimumSize(QSize(30, 30))\n        # Any other policy than Ignored will let the QListBox's scrollbar\n        # set the minimal height (see the penultimate paragraph of\n        # http://doc.qt.io/qt-4.8/qabstractscrollarea.html#addScrollBarWidget)\n        view.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Ignored)\n        gui.checkBox(\n            view.box, self, \"order_by_importance\",\n            \"Order by relevance to subgroups\",\n            tooltip=\"Order by 𝜒² or ANOVA over the subgroups\",\n            callback=self.apply_attr_sorting)\n        self.group_vars = VariableListModel(placeholder=\"None\")\n        view = gui.listView(\n            self.controlArea, self, \"group_var\", box=\"Subgroups\",\n            model=self.group_vars, callback=self.grouping_changed)\n        gui.checkBox(\n            view.box, self, \"order_grouping_by_importance\",\n            \"Order by relevance to variable\",\n            tooltip=\"Order by 𝜒² or ANOVA over the variable values\",\n            callback=self.on_group_sorting_checkbox)\n        view.setMinimumSize(QSize(30, 30))\n        # See the comment above\n        view.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Ignored)\n\n        # TODO: move Compare median/mean to grouping box\n        # The vertical size policy is needed to let only the list views expand\n        self.display_box = gui.vBox(\n            self.controlArea, \"Display\",\n            sizePolicy=(QSizePolicy.Minimum, QSizePolicy.Maximum),\n            addSpace=False)\n\n        gui.checkBox(self.display_box, self, \"show_annotations\", \"Annotate\",\n                     callback=self.display_changed)\n        self.compare_rb = gui.radioButtonsInBox(\n            self.display_box, self, 'compare',\n            btnLabels=[\"No comparison\", \"Compare medians\", \"Compare means\"],\n            callback=self.layout_changed)\n\n        # The vertical size policy is needed to let only the list views expand\n        self.stretching_box = box = gui.vBox(\n            self.controlArea, box=\"Display\",\n            sizePolicy=(QSizePolicy.Minimum, QSizePolicy.Fixed))\n        self.stretching_box.sizeHint = self.display_box.sizeHint\n        gui.checkBox(\n            box, self, 'stretched', \"Stretch bars\",\n            callback=self.display_changed)\n        gui.checkBox(\n            box, self, 'show_labels', \"Show box labels\",\n            callback=self.display_changed)\n        self.sort_cb = gui.checkBox(\n            box, self, 'sort_freqs', \"Sort by subgroup frequencies\",\n            callback=self.display_changed)\n\n        gui.vBox(self.mainArea, addSpace=True)\n        self.box_scene = QGraphicsScene()\n        self.box_scene.selectionChanged.connect(self.commit)\n        self.box_view = QGraphicsView(self.box_scene)\n        self.box_view.setRenderHints(QPainter.Antialiasing |\n                                     QPainter.TextAntialiasing |\n                                     QPainter.SmoothPixmapTransform)\n        self.box_view.viewport().installEventFilter(self)\n\n        self.mainArea.layout().addWidget(self.box_view)\n\n        gui.hBox(self.mainArea, addSpace=False)\n        self.stat_test = \"\"\n        self.mainArea.setMinimumWidth(300)\n\n        self.stats = self.dist = self.conts = []\n        self.is_continuous = False\n\n        self.update_display_box()\n\n    def sizeHint(self):\n        return QSize(900, 500)\n\n    def eventFilter(self, obj, event):\n        if obj is self.box_view.viewport() and \\\n                event.type() == QEvent.Resize:\n            self.layout_changed()\n\n        return super().eventFilter(obj, event)\n\n    @property\n    def show_stretched(self):\n        return self.stretched and self.group_var is not self.attribute\n\n    def reset_attrs(self):\n        domain = self.dataset.domain\n        self.attrs[:] = [\n            var for var in chain(\n                domain.class_vars, domain.metas, domain.attributes)\n            if var.is_primitive()]\n\n    def reset_groups(self):\n        domain = self.dataset.domain\n        self.group_vars[:] = [None] + [\n            var for var in chain(\n                domain.class_vars, domain.metas, domain.attributes)\n            if var.is_discrete]\n\n    # noinspection PyTypeChecker\n    @Inputs.data\n    def set_data(self, dataset):\n        if dataset is not None and (\n                not bool(dataset) or not len(dataset.domain) and not\n                any(var.is_primitive() for var in dataset.domain.metas)):\n            dataset = None\n        self.closeContext()\n        self.dataset = dataset\n        self.dist = self.stats = self.conts = []\n        self.group_var = None\n        self.attribute = None\n        if dataset:\n            self.reset_attrs()\n            self.reset_groups()\n            self.select_default_variables()\n            self.openContext(self.dataset)\n            self.grouping_changed()\n            self.attr_changed()\n        else:\n            self.reset_all_data()\n        self.commit()\n\n    def select_default_variables(self):\n        # visualize first non-class variable, group by class (if present)\n        domain = self.dataset.domain\n        if len(self.attrs) > len(domain.class_vars):\n            self.attribute = self.attrs[len(domain.class_vars)]\n        elif self.attrs:\n            self.attribute = self.attrs[0]\n\n        if domain.class_var and domain.class_var.is_discrete:\n            self.group_var = domain.class_var\n        else:\n            self.group_var = None  # Reset to trigger selection via callback\n\n    def apply_attr_sorting(self):\n        def compute_score(attr):\n            # This function and the one in apply_group_sorting are similar, but\n            # different in too many details, so they are kept as separate\n            # functions.\n            # If you discover a bug in this function, check the other one, too.\n            if attr is group_var:\n                return 3\n            if attr.is_continuous:\n                # One-way ANOVA\n                col = data.get_column_view(attr)[0].astype(float)\n                groups = (col[group_col == i] for i in range(n_groups))\n                groups = (col[~np.isnan(col)] for col in groups)\n                groups = [group for group in groups if len(group)]\n                p = f_oneway(*groups)[1] if len(groups) > 1 else 2\n            else:\n                p = self._chi_square(group_var, attr)[1]\n            if math.isnan(p):\n                return 2\n            return p\n\n        data = self.dataset\n        if data is None:\n            return\n        domain = data.domain\n        attribute = self.attribute\n        group_var = self.group_var\n        if self.order_by_importance and group_var is not None:\n            n_groups = len(group_var.values)\n            group_col = data.get_column_view(group_var)[0] if \\\n                domain.has_continuous_attributes(\n                    include_class=True, include_metas=True) else None\n            self.attrs.sort(key=compute_score)\n        else:\n            self.reset_attrs()\n        self.attribute = attribute  # reset selection\n        self._ensure_selection_visible(self.controls.attribute)\n\n    def on_group_sorting_checkbox(self):\n        if self.order_grouping_by_importance:\n            self.apply_group_sorting()\n        else:\n            self.reset_groups()\n            self.group_var = self.group_var  # reset selection\n            self._ensure_selection_visible(self.controls.group_var)\n\n    def apply_group_sorting(self):\n        def compute_stat(group):\n            # This function and the one in apply_attr_sorting are similar, but\n            # different in too many details, so they are kept as separate\n            # functions.\n            # If you discover a bug in this function, check the other one, too.\n            if group is attr:\n                return 3\n            if group is None:\n                return -1\n            if attr.is_continuous:\n                group_col = data.get_column_view(group)[0].astype(float)\n                groups = (attr_col[group_col == i]\n                          for i in range(len(group.values)))\n                groups = (col[~np.isnan(col)] for col in groups)\n                groups = [group for group in groups if len(group)]\n                p = f_oneway(*groups)[1] if len(groups) > 1 else 2\n            else:\n                p = self._chi_square(group, attr)[1]\n            if math.isnan(p):\n                return 2\n            return p\n\n        data = self.dataset\n        if data is None or not self.order_grouping_by_importance:\n            return\n        attr = self.attribute\n        group_var = self.group_var\n        if attr.is_continuous:\n            attr_col = data.get_column_view(attr)[0].astype(float)\n        self.group_vars.sort(key=compute_stat)\n        self.group_var = group_var  # reset selection\n        self._ensure_selection_visible(self.controls.group_var)\n\n    @staticmethod\n    def _ensure_selection_visible(view):\n        selection = view.selectedIndexes()\n        if len(selection) == 1:\n            view.scrollTo(selection[0])\n\n    def _chi_square(self, group_var, attr):\n        # Chi-square with the given distribution into groups\n        if not attr.values or not group_var.values:\n            return 0, 2, 0\n        observed = np.array(\n            contingency.get_contingency(self.dataset, group_var, attr))\n        observed = observed[observed.sum(axis=1) != 0, :]\n        observed = observed[:, observed.sum(axis=0) != 0]\n        if min(observed.shape) < 2:\n            return 0, 2, 0\n        return chi2_contingency(observed)[:3]\n\n    def reset_all_data(self):\n        self.clear_scene()\n        self.stat_test = \"\"\n        self.attrs[:] = []\n        self.group_vars[:] = [None]\n        self.is_continuous = False\n        self.update_display_box()\n\n    def grouping_changed(self):\n        self.controls.stretched.setDisabled(self.group_var is self.attribute)\n        self.apply_attr_sorting()\n        self.update_graph()\n\n    def select_box_items(self):\n        temp_cond = self.conditions.copy()\n        for box in self.box_scene.items():\n            if isinstance(box, FilterGraphicsRectItem):\n                box.setSelected(box.filter.conditions in\n                                [c.conditions for c in temp_cond])\n\n    def attr_changed(self):\n        self.controls.stretched.setDisabled(self.group_var is self.attribute)\n        self.apply_group_sorting()\n        self.update_graph()\n\n    def update_graph(self):\n        self.compute_box_data()\n        self.update_display_box()\n        self.layout_changed()\n\n        if self.is_continuous:\n            heights = 90 if self.show_annotations else 60\n            self.box_view.centerOn(self.scene_min_x + self.scene_width / 2,\n                                   -30 - len(self.stats) * heights / 2 + 45)\n        else:\n            self.box_view.centerOn(self.scene_width / 2,\n                                   -30 - len(self.boxes) * 40 / 2 + 45)\n\n    def compute_box_data(self):\n        attr = self.attribute\n        if not attr:\n            return\n        dataset = self.dataset\n        self.is_continuous = attr.is_continuous\n        if dataset is None or not self.is_continuous and not attr.values or \\\n                        self.group_var and not self.group_var.values:\n            self.stats = self.dist = self.conts = []\n            return\n        if self.group_var:\n            self.dist = []\n            self.conts = contingency.get_contingency(\n                dataset, attr, self.group_var)\n            if self.is_continuous:\n                stats, label_texts = [], []\n                for i, cont in enumerate(self.conts):\n                    if np.sum(cont[1]):\n                        stats.append(BoxData(cont, attr, i, self.group_var))\n                        label_texts.append(self.group_var.values[i])\n                self.stats = stats\n                self.label_txts_all = label_texts\n            else:\n                self.label_txts_all = \\\n                    [v for v, c in zip(\n                        self.group_var.values + [\"Missing values\"],\n                        self.conts.array_with_unknowns)\n                     if np.sum(c) > 0]\n        else:\n            self.dist = distribution.get_distribution(dataset, attr)\n            self.conts = []\n            if self.is_continuous:\n                self.stats = [BoxData(self.dist, attr, None)]\n            self.label_txts_all = [\"\"]\n        self.label_txts = [txts for stat, txts in zip(self.stats,\n                                                      self.label_txts_all)\n                           if stat.n > 0]\n        self.stats = [stat for stat in self.stats if stat.n > 0]\n\n    def update_display_box(self):\n        if self.is_continuous:\n            self.stretching_box.hide()\n            self.display_box.show()\n            self.compare_rb.setEnabled(self.group_var is not None)\n        else:\n            self.stretching_box.show()\n            self.display_box.hide()\n            self.sort_cb.setEnabled(self.group_var is not None)\n\n    def clear_scene(self):\n        self.closeContext()\n        self.box_scene.clearSelection()\n        self.box_scene.clear()\n        self.box_view.viewport().update()\n        self.attr_labels = []\n        self.labels = []\n        self.boxes = []\n        self.mean_labels = []\n        self.posthoc_lines = []\n        self.openContext(self.dataset)\n\n    def layout_changed(self):\n        attr = self.attribute\n        if not attr:\n            return\n        self.clear_scene()\n        if self.dataset is None or len(self.conts) == len(self.dist) == 0:\n            return\n\n        if self.is_continuous:\n            self.mean_labels = [self.mean_label(stat, attr, lab)\n                                for stat, lab in zip(self.stats, self.label_txts)]\n            self.draw_axis()\n            self.boxes = [self.box_group(stat) for stat in self.stats]\n            self.labels = [self.label_group(stat, attr, mean_lab)\n                           for stat, mean_lab in zip(self.stats, self.mean_labels)]\n            self.attr_labels = [QGraphicsSimpleTextItem(lab)\n                                for lab in self.label_txts]\n            for it in chain(self.labels, self.attr_labels):\n                self.box_scene.addItem(it)\n\n        self.display_changed()\n\n    def display_changed(self):\n        if self.dataset is None or self.attribute is None:\n            return\n\n        if self.is_continuous:\n            self._display_changed_cont()\n        else:\n            self._display_changed_disc()\n        self.draw_stat()\n        self.select_box_items()\n\n    def _display_changed_cont(self):\n        self.order = list(range(len(self.stats)))\n        criterion = self._sorting_criteria_attrs[self.compare]\n        if criterion:\n            vals = [getattr(stat, criterion) for stat in self.stats]\n            overmax = max((val for val in vals if val is not None), default=0) \\\n                      + 1\n            vals = [val if val is not None else overmax for val in vals]\n            self.order = sorted(self.order, key=vals.__getitem__)\n\n        heights = 90 if self.show_annotations else 60\n\n        for row, box_index in enumerate(self.order):\n            y = (-len(self.stats) + row) * heights + 10\n            for item in self.boxes[box_index]:\n                self.box_scene.addItem(item)\n                item.setY(y)\n            labels = self.labels[box_index]\n\n            if self.show_annotations:\n                labels.show()\n                labels.setY(y)\n            else:\n                labels.hide()\n\n            label = self.attr_labels[box_index]\n            label.setY(y - 15 - label.boundingRect().height())\n            if self.show_annotations:\n                label.hide()\n            else:\n                stat = self.stats[box_index]\n\n                if self.compare == OWBoxPlot.CompareMedians and \\\n                        stat.median is not None:\n                    pos = stat.median + 5 / self.scale_x\n                elif self.compare == OWBoxPlot.CompareMeans or stat.q25 is None:\n                    pos = stat.mean + 5 / self.scale_x\n                else:\n                    pos = stat.q25\n                label.setX(pos * self.scale_x)\n                label.show()\n\n        r = QRectF(self.scene_min_x, -30 - len(self.stats) * heights,\n                   self.scene_width, len(self.stats) * heights + 90)\n        self.box_scene.setSceneRect(r)\n\n        self._compute_tests_cont()\n        self._show_posthoc()\n\n    def _display_changed_disc(self):\n        self.clear_scene()\n        self.attr_labels = [QGraphicsSimpleTextItem(lab)\n                            for lab in self.label_txts_all]\n\n        if not self.show_stretched:\n            if self.group_var:\n                self.labels = [\n                    QGraphicsTextItem(\"{}\".format(int(sum(cont))))\n                    for cont in self.conts.array_with_unknowns\n                    if np.sum(cont) > 0]\n            else:\n                self.labels = [\n                    QGraphicsTextItem(str(int(sum(self.dist))))]\n\n        self.order = list(range(len(self.attr_labels)))\n\n        self.draw_axis_disc()\n        if self.group_var:\n            self.boxes = \\\n                [self.strudel(cont, i)\n                 for i, cont in enumerate(self.conts.array_with_unknowns)\n                 if np.sum(cont) > 0]\n\n            if self.sort_freqs:\n                # pylint: disable=invalid-unary-operand-type\n                self.order = sorted(\n                    self.order, key=(-np.sum(\n                        self.conts.array_with_unknowns, axis=1)).__getitem__)\n        else:\n            self.boxes = [self.strudel(self.dist.array_with_unknowns)]\n\n        for row, box_index in enumerate(self.order):\n            y = (-len(self.boxes) + row) * 40 + 10\n            box = self.boxes[box_index]\n            bars, labels = box[::2], box[1::2]\n\n            self.__draw_group_labels(y, box_index)\n            if not self.show_stretched:\n                self.__draw_row_counts(y, box_index)\n            if self.show_labels and self.attribute is not self.group_var:\n                self.__draw_bar_labels(y, bars, labels)\n            self.__draw_bars(y, bars)\n\n        self.box_scene.setSceneRect(-self.label_width - 5,\n                                    -30 - len(self.boxes) * 40,\n                                    self.scene_width, len(self.boxes * 40) + 90)\n        self._compute_tests_disc()\n\n    def __draw_group_labels(self, y, row):\n        \"\"\"Draw group labels\n\n        Parameters\n        ----------\n        y: int\n            vertical offset of bars\n        row: int\n            row index\n        \"\"\"\n        label = self.attr_labels[row]\n        b = label.boundingRect()\n        label.setPos(-b.width() - 10, y - b.height() / 2)\n        self.box_scene.addItem(label)\n\n    def __draw_row_counts(self, y, row):\n        \"\"\"Draw row counts\n\n        Parameters\n        ----------\n        y: int\n            vertical offset of bars\n        row: int\n            row index\n        \"\"\"\n        assert not self.is_continuous\n        label = self.labels[row]\n        b = label.boundingRect()\n        if self.group_var:\n            right = self.scale_x * sum(self.conts.array_with_unknowns[row])\n        else:\n            right = self.scale_x * sum(self.dist)\n        label.setPos(right + 10, y - b.height() / 2)\n        self.box_scene.addItem(label)\n\n    def __draw_bar_labels(self, y, bars, labels):\n        \"\"\"Draw bar labels\n\n        Parameters\n        ----------\n        y: int\n            vertical offset of bars\n        bars: List[FilterGraphicsRectItem]\n            list of bars being drawn\n        labels: List[QGraphicsTextItem]\n            list of labels for corresponding bars\n        \"\"\"\n        label = bar_part = None\n        for text_item, bar_part in zip(labels, bars):\n            label = self.Label(\n                text_item.toPlainText())\n            label.setPos(bar_part.boundingRect().x(),\n                         y - label.boundingRect().height() - 8)\n            label.setMaxWidth(bar_part.boundingRect().width())\n            self.box_scene.addItem(label)\n\n    def __draw_bars(self, y, bars):\n        \"\"\"Draw bars\n\n        Parameters\n        ----------\n        y: int\n            vertical offset of bars\n\n        bars: List[FilterGraphicsRectItem]\n            list of bars to draw\n        \"\"\"\n        for item in bars:\n            item.setPos(0, y)\n            self.box_scene.addItem(item)\n\n    # noinspection PyPep8Naming\n    def _compute_tests_cont(self):\n        # The t-test and ANOVA are implemented here since they efficiently use\n        # the widget-specific data in self.stats.\n        # The non-parametric tests can't do this, so we use statistics.tests\n\n        # pylint: disable=comparison-with-itself\n        def stat_ttest():\n            d1, d2 = self.stats\n            if d1.n < 2 or d2.n < 2:\n                return np.nan, np.nan\n            pooled_var = d1.var / d1.n + d2.var / d2.n\n            # pylint: disable=comparison-with-itself\n            if pooled_var == 0 or np.isnan(pooled_var):\n                return np.nan, np.nan\n            df = pooled_var ** 2 / \\\n                ((d1.var / d1.n) ** 2 / (d1.n - 1) +\n                 (d2.var / d2.n) ** 2 / (d2.n - 1))\n            t = abs(d1.mean - d2.mean) / math.sqrt(pooled_var)\n            p = 2 * (1 - scipy.special.stdtr(df, t))\n            return t, p\n\n        # TODO: Check this function\n        # noinspection PyPep8Naming\n        def stat_ANOVA():\n            if any(stat.n == 0 for stat in self.stats):\n                return np.nan, np.nan\n            n = sum(stat.n for stat in self.stats)\n            grand_avg = sum(stat.n * stat.mean for stat in self.stats) / n\n            var_between = sum(stat.n * (stat.mean - grand_avg) ** 2\n                              for stat in self.stats)\n            df_between = len(self.stats) - 1\n\n            var_within = sum(stat.n * stat.var for stat in self.stats)\n            df_within = n - len(self.stats)\n            if var_within == 0 or df_within == 0 or df_between == 0:\n                return np.nan, np.nan\n            F = (var_between / df_between) / (var_within / df_within)\n            p = 1 - scipy.special.fdtr(df_between, df_within, F)\n            return F, p\n\n        n = len(self.dataset)\n        if self.compare == OWBoxPlot.CompareNone or len(self.stats) < 2:\n            t = \"\"\n        elif any(s.n <= 1 for s in self.stats):\n            t = \"At least one group has just one instance, \" \\\n                \"cannot compute significance\"\n        elif len(self.stats) == 2:\n            if self.compare == OWBoxPlot.CompareMedians:\n                t = \"\"\n                # z, p = tests.wilcoxon_rank_sum(\n                #    self.stats[0].dist, self.stats[1].dist)\n                # t = \"Mann-Whitney's z: %.1f (p=%.3f)\" % (z, p)\n            else:\n                t, p = stat_ttest()\n                t = \"\" if np.isnan(t) else f\"Student's t: {t:.3f} (p={p:.3f}, N={n})\"\n        else:\n            if self.compare == OWBoxPlot.CompareMedians:\n                t = \"\"\n                # U, p = -1, -1\n                # t = \"Kruskal Wallis's U: %.1f (p=%.3f)\" % (U, p)\n            else:\n                F, p = stat_ANOVA()\n                t = \"\" if np.isnan(F) else f\"ANOVA: {F:.3f} (p={p:.3f}, N={n})\"\n        self.stat_test = t\n\n    def _compute_tests_disc(self):\n        if self.group_var is None or self.attribute is None:\n            self.stat_test = \"\"\n        else:\n            chi, p, dof = self._chi_square(self.group_var, self.attribute)\n            if np.isnan(p):\n                self.stat_test = \"\"\n            else:\n                self.stat_test = f\"χ²: {chi:.2f} (p={p:.3f}, dof={dof})\"\n\n    def mean_label(self, stat, attr, val_name):\n        label = QGraphicsItemGroup()\n        t = QGraphicsSimpleTextItem(attr.str_val(stat.mean), label)\n        t.setFont(self._label_font)\n        bbox = t.boundingRect()\n        w2, h = bbox.width() / 2, bbox.height()\n        t.setPos(-w2, -h)\n        tpm = QGraphicsSimpleTextItem(\n            \" \\u00b1 \" + \"%.*f\" % (attr.number_of_decimals + 1, stat.dev),\n            label)\n        tpm.setFont(self._label_font)\n        tpm.setPos(w2, -h)\n        if val_name:\n            vnm = QGraphicsSimpleTextItem(val_name + \": \", label)\n            vnm.setFont(self._label_font)\n            vnm.setBrush(self._attr_brush)\n            vb = vnm.boundingRect()\n            label.min_x = -w2 - vb.width()\n            vnm.setPos(label.min_x, -h)\n        else:\n            label.min_x = -w2\n        return label\n\n    def draw_axis(self):\n        \"\"\"Draw the horizontal axis and sets self.scale_x\"\"\"\n        misssing_stats = not self.stats\n        stats = self.stats or [BoxData(np.array([[0.], [1.]]), self.attribute)]\n        mean_labels = self.mean_labels or [self.mean_label(stats[0], self.attribute, \"\")]\n        bottom = min(stat.a_min for stat in stats)\n        top = max(stat.a_max for stat in stats)\n\n        first_val, step = compute_scale(bottom, top)\n        while bottom <= first_val:\n            first_val -= step\n        bottom = first_val\n        no_ticks = math.ceil((top - first_val) / step) + 1\n        top = max(top, first_val + no_ticks * step)\n\n        gbottom = min(bottom, min(stat.mean - stat.dev for stat in stats))\n        gtop = max(top, max(stat.mean + stat.dev for stat in stats))\n\n        bv = self.box_view\n        viewrect = bv.viewport().rect().adjusted(15, 15, -15, -30)\n        self.scale_x = scale_x = viewrect.width() / (gtop - gbottom)\n\n        # In principle we should repeat this until convergence since the new\n        # scaling is too conservative. (No chance am I doing this.)\n        mlb = min(stat.mean + mean_lab.min_x / scale_x\n                  for stat, mean_lab in zip(stats, mean_labels))\n        if mlb < gbottom:\n            gbottom = mlb\n            self.scale_x = scale_x = viewrect.width() / (gtop - gbottom)\n\n        self.scene_min_x = gbottom * scale_x\n        self.scene_max_x = gtop * scale_x\n        self.scene_width = self.scene_max_x - self.scene_min_x\n\n        val = first_val\n        last_text = self.scene_min_x\n        while True:\n            l = self.box_scene.addLine(val * scale_x, -1, val * scale_x, 1,\n                                       self._pen_axis_tick)\n            l.setZValue(100)\n            t = QGraphicsSimpleTextItem(\n                self.attribute.str_val(val) if not misssing_stats else \"?\")\n            t.setFont(self._axis_font)\n            t.setFlag(QGraphicsItem.ItemIgnoresTransformations)\n            r = t.boundingRect()\n            x_start = val * scale_x - r.width() / 2\n            x_finish = x_start + r.width()\n            if x_start > last_text + 10 and x_finish < self.scene_max_x:\n                t.setPos(x_start, 8)\n                self.box_scene.addItem(t)\n                last_text = x_finish\n            if val >= top:\n                break\n            val += step\n        self.box_scene.addLine(\n            bottom * scale_x - 4, 0, top * scale_x + 4, 0, self._pen_axis)\n\n    def draw_stat(self):\n        if self.stat_test:\n            label = QGraphicsSimpleTextItem(self.stat_test)\n            brect = self.box_scene.sceneRect()\n            label.setPos(brect.center().x() - label.boundingRect().width()/2,\n                         8 + self._axis_font.pixelSize()*2)\n            label.setFlag(QGraphicsItem.ItemIgnoresTransformations)\n            self.box_scene.addItem(label)\n\n    def draw_axis_disc(self):\n        \"\"\"\n        Draw the horizontal axis and sets self.scale_x for discrete attributes\n        \"\"\"\n        assert not self.is_continuous\n        if self.show_stretched:\n            if not self.attr_labels:\n                return\n            step = steps = 10\n        else:\n            if self.group_var:\n                max_box = max(float(np.sum(dist))\n                              for dist in self.conts.array_with_unknowns)\n            else:\n                max_box = float(np.sum(self.dist.array_with_unknowns))\n            if max_box == 0:\n                self.scale_x = 1\n                return\n            _, step = compute_scale(0, max_box)\n            step = int(step) if step > 1 else 1\n            steps = int(math.ceil(max_box / step))\n        max_box = step * steps\n\n        bv = self.box_view\n        viewrect = bv.viewport().rect().adjusted(15, 15, -15, -30)\n        self.scene_width = viewrect.width()\n\n        lab_width = max(lab.boundingRect().width() for lab in self.attr_labels)\n        lab_width = max(lab_width, 40)\n        lab_width = min(lab_width, self.scene_width / 3)\n        self.label_width = lab_width\n\n        right_offset = 0  # offset for the right label\n        if not self.show_stretched and self.labels:\n            if self.group_var:\n                rows = list(zip(self.conts.array_with_unknowns, self.labels))\n            else:\n                rows = [(self.dist, self.labels[0])]\n            # available space left of the 'group labels'\n            available = self.scene_width - lab_width - 10\n            scale_x = (available - right_offset) / max_box\n            max_right = max(sum(dist) * scale_x + 10 +\n                            lbl.boundingRect().width()\n                            for dist, lbl in rows)\n            right_offset = max(0, max_right - max_box * scale_x)\n\n        self.scale_x = scale_x = \\\n            (self.scene_width - lab_width - 10 - right_offset) / max_box\n\n        self.box_scene.addLine(0, 0, max_box * scale_x, 0, self._pen_axis)\n        for val in range(0, step * steps + 1, step):\n            l = self.box_scene.addLine(val * scale_x, -1, val * scale_x, 1,\n                                       self._pen_axis_tick)\n            l.setZValue(100)\n            t = self.box_scene.addSimpleText(str(val), self._axis_font)\n            t.setPos(val * scale_x - t.boundingRect().width() / 2, 8)\n        if self.show_stretched:\n            self.scale_x *= 100\n\n    def label_group(self, stat, attr, mean_lab):\n        def centered_text(val, pos):\n            t = QGraphicsSimpleTextItem(attr.str_val(val), labels)\n            t.setFont(self._label_font)\n            bbox = t.boundingRect()\n            t.setPos(pos - bbox.width() / 2, 22)\n            return t\n\n        def line(x, down=1):\n            QGraphicsLineItem(x, 12 * down, x, 20 * down, labels)\n\n        def move_label(label, frm, to):\n            label.setX(to)\n            to += t_box.width() / 2\n            path = QPainterPath()\n            path.lineTo(0, 4)\n            path.lineTo(to - frm, 4)\n            path.lineTo(to - frm, 8)\n            p = QGraphicsPathItem(path)\n            p.setPos(frm, 12)\n            labels.addToGroup(p)\n\n        labels = QGraphicsItemGroup()\n\n        labels.addToGroup(mean_lab)\n        m = stat.mean * self.scale_x\n        mean_lab.setPos(m, -22)\n        line(m, -1)\n\n        if stat.median is not None:\n            msc = stat.median * self.scale_x\n            med_t = centered_text(stat.median, msc)\n            med_box_width2 = med_t.boundingRect().width() / 2\n            line(msc)\n\n        if stat.q25 is not None:\n            x = stat.q25 * self.scale_x\n            t = centered_text(stat.q25, x)\n            t_box = t.boundingRect()\n            med_left = msc - med_box_width2\n            if x + t_box.width() / 2 >= med_left - 5:\n                move_label(t, x, med_left - t_box.width() - 5)\n            else:\n                line(x)\n\n        if stat.q75 is not None:\n            x = stat.q75 * self.scale_x\n            t = centered_text(stat.q75, x)\n            t_box = t.boundingRect()\n            med_right = msc + med_box_width2\n            if x - t_box.width() / 2 <= med_right + 5:\n                move_label(t, x, med_right + 5)\n            else:\n                line(x)\n\n        return labels\n\n    def box_group(self, stat, height=20):\n        def line(x0, y0, x1, y1, *args):\n            return QGraphicsLineItem(x0 * scale_x, y0, x1 * scale_x, y1, *args)\n\n        scale_x = self.scale_x\n        box = []\n        whisker1 = line(stat.a_min, -1.5, stat.a_min, 1.5)\n        whisker2 = line(stat.a_max, -1.5, stat.a_max, 1.5)\n        vert_line = line(stat.a_min, 0, stat.a_max, 0)\n        mean_line = line(stat.mean, -height / 3, stat.mean, height / 3)\n        for it in (whisker1, whisker2, mean_line):\n            it.setPen(self._pen_paramet)\n        vert_line.setPen(self._pen_dotted)\n        var_line = line(stat.mean - stat.dev, 0, stat.mean + stat.dev, 0)\n        var_line.setPen(self._pen_paramet)\n        box.extend([whisker1, whisker2, vert_line, mean_line, var_line])\n        if stat.q25 is not None or stat.q75 is not None:\n            # if any of them is None it means that its value is equal to median\n            box_from = stat.q25 or stat.median\n            box_to = stat.q75 or stat.median\n            mbox = FilterGraphicsRectItem(\n                stat.conditions, box_from * scale_x, -height / 2,\n                (box_to - box_from) * scale_x, height)\n            mbox.setBrush(self._box_brush)\n            mbox.setPen(QPen(Qt.NoPen))\n            mbox.setZValue(-200)\n            box.append(mbox)\n\n        if stat.median is not None:\n            median_line = line(stat.median, -height / 2,\n                               stat.median, height / 2)\n            median_line.setPen(self._pen_median)\n            median_line.setZValue(-150)\n            box.append(median_line)\n\n        return box\n\n    def strudel(self, dist, group_val_index=None):\n        attr = self.attribute\n        ss = np.sum(dist)\n        box = []\n        if ss < 1e-6:\n            cond = [FilterDiscrete(attr, None)]\n            if group_val_index is not None:\n                cond.append(FilterDiscrete(self.group_var, [group_val_index]))\n            box.append(FilterGraphicsRectItem(cond, 0, -10, 1, 10))\n        cum = 0\n        values = attr.values + [\"Missing values\"]\n        colors = np.vstack((attr.colors, [128, 128, 128]))\n        for i, v in enumerate(dist):\n            if v < 1e-6:\n                continue\n            if self.show_stretched:\n                v /= ss\n            v *= self.scale_x\n            cond = [FilterDiscrete(attr, [i])]\n            if group_val_index is not None:\n                cond.append(FilterDiscrete(self.group_var, [group_val_index]))\n            rect = FilterGraphicsRectItem(cond, cum + 1, -6, v - 2, 12)\n            rect.setBrush(QBrush(QColor(*colors[i])))\n            rect.setPen(QPen(Qt.NoPen))\n            if self.show_stretched:\n                tooltip = \"{}: {:.2f}%\".format(\n                    values[i],\n                    100 * dist[i] / sum(dist))\n            else:\n                tooltip = \"{}: {}\".format(values[i], int(dist[i]))\n            rect.setToolTip(tooltip)\n            text = QGraphicsTextItem(values[i])\n            box.append(rect)\n            box.append(text)\n            cum += v\n        return box\n\n    def commit(self):\n        self.conditions = [item.filter for item in\n                           self.box_scene.selectedItems() if item.filter]\n        selected, selection = None, []\n        if self.conditions:\n            selected = Values(self.conditions, conjunction=False)(self.dataset)\n            selection = np.in1d(\n                self.dataset.ids, selected.ids, assume_unique=True).nonzero()[0]\n        self.Outputs.selected_data.send(selected)\n        self.Outputs.annotated_data.send(\n            create_annotated_table(self.dataset, selection))\n\n    def _show_posthoc(self):\n        def line(y0, y1):\n            it = self.box_scene.addLine(x, y0, x, y1, self._post_line_pen)\n            it.setZValue(-100)\n            self.posthoc_lines.append(it)\n\n        while self.posthoc_lines:\n            self.box_scene.removeItem(self.posthoc_lines.pop())\n\n        if self.compare == OWBoxPlot.CompareNone or len(self.stats) < 2:\n            return\n\n        if self.compare == OWBoxPlot.CompareMedians:\n            crit_line = \"median\"\n        else:\n            crit_line = \"mean\"\n\n        xs = []\n\n        height = 90 if self.show_annotations else 60\n\n        y_up = -len(self.stats) * height + 10\n        for pos, box_index in enumerate(self.order):\n            stat = self.stats[box_index]\n            x = getattr(stat, crit_line)\n            if x is None:\n                continue\n            x *= self.scale_x\n            xs.append(x * self.scale_x)\n            by = y_up + pos * height\n            line(by + 12, 0)\n\n        used_to = []\n        last_to = to = 0\n        for frm, frm_x in enumerate(xs[:-1]):\n            for to in range(frm + 1, len(xs)):\n                if xs[to] - frm_x > 1.5:\n                    to -= 1\n                    break\n            if to in (last_to, frm):\n                continue\n            for rowi, used in enumerate(used_to):\n                if used < frm:\n                    used_to[rowi] = to\n                    break\n            else:\n                rowi = len(used_to)\n                used_to.append(to)\n            y = - 6 - rowi * 6\n            it = self.box_scene.addLine(frm_x - 2, y, xs[to] + 2, y,\n                                        self._post_grp_pen)\n            self.posthoc_lines.append(it)\n            last_to = to\n\n    def get_widget_name_extension(self):\n        return self.attribute.name if self.attribute else None\n\n    def send_report(self):\n        self.report_plot()\n        text = \"\"\n        if self.attribute:\n            text += \"Box plot for attribute '{}' \".format(self.attribute.name)\n        if self.group_var:\n            text += \"grouped by '{}'\".format(self.group_var.name)\n        if text:\n            self.report_caption(text)\n\n    class Label(QGraphicsSimpleTextItem):\n        \"\"\"Boxplot Label with settable maxWidth\"\"\"\n        # Minimum width to display label text\n        MIN_LABEL_WIDTH = 25\n\n        # padding bellow the text\n        PADDING = 3\n\n        __max_width = None\n\n        def maxWidth(self):\n            return self.__max_width\n\n        def setMaxWidth(self, max_width):\n            self.__max_width = max_width\n\n        def paint(self, painter, option, widget):\n            \"\"\"Overrides QGraphicsSimpleTextItem.paint\n\n            If label text is too long, it is elided\n            to fit into the allowed region\n            \"\"\"\n            if self.__max_width is None:\n                width = option.rect.width()\n            else:\n                width = self.__max_width\n\n            if width < self.MIN_LABEL_WIDTH:\n                # if space is too narrow, no label\n                return\n\n            fm = painter.fontMetrics()\n            text = fm.elidedText(self.text(), Qt.ElideRight, width)\n            painter.drawText(\n                option.rect.x(),\n                option.rect.y() + self.boundingRect().height() - self.PADDING,\n                text)",
                                "ast_type_str": "ClassDef",
                                "start_lineno": 108,
                                "end_lineno": 1216,
                                "parent": null
                            }
                        },
                        {
                            "file_name": "Orange/widgets/visualize/owboxplot.py",
                            "type": "class",
                            "name": "OWBoxPlot",
                            "content": "class OWBoxPlot(widget.OWWidget):\n    \"\"\"\n    Here's how the widget's functions call each other:\n\n    - `set_data` is a signal handler fills the list boxes and calls\n    `grouping_changed`.\n\n    - `grouping_changed` handles changes of grouping attribute: it enables or\n    disables the box for ordering, orders attributes and calls `attr_changed`.\n\n    - `attr_changed` handles changes of attribute. It recomputes box data by\n    calling `compute_box_data`, shows the appropriate display box\n    (discrete/continuous) and then calls`layout_changed`\n\n    - `layout_changed` constructs all the elements for the scene (as lists of\n    QGraphicsItemGroup) and calls `display_changed`. It is called when the\n    attribute or grouping is changed (by attr_changed) and on resize event.\n\n    - `display_changed` puts the elements corresponding to the current display\n    settings on the scene. It is called when the elements are reconstructed\n    (layout is changed due to selection of attributes or resize event), or\n    when the user changes display settings or colors.\n\n    For discrete attributes, the flow is a bit simpler: the elements are not\n    constructed in advance (by layout_changed). Instead, _display_changed_disc\n    draws everything.\n    \"\"\"\n    name = \"Box Plot\"\n    description = \"Visualize the distribution of feature values in a box plot.\"\n    icon = \"icons/BoxPlot.svg\"\n    priority = 100\n    keywords = [\"whisker\"]\n\n    class Inputs:\n        data = Input(\"Data\", Orange.data.Table)\n\n    class Outputs:\n        selected_data = Output(\"Selected Data\", Orange.data.Table, default=True)\n        annotated_data = Output(ANNOTATED_DATA_SIGNAL_NAME, Orange.data.Table)\n\n    #: Comparison types for continuous variables\n    CompareNone, CompareMedians, CompareMeans = 0, 1, 2\n\n    settingsHandler = DomainContextHandler()\n    conditions = ContextSetting([])\n\n    attribute = ContextSetting(None)\n    order_by_importance = Setting(False)\n    order_grouping_by_importance = Setting(False)\n    group_var = ContextSetting(None)\n    show_annotations = Setting(True)\n    compare = Setting(CompareMeans)\n    stattest = Setting(0)\n    sig_threshold = Setting(0.05)\n    stretched = Setting(True)\n    show_labels = Setting(True)\n    sort_freqs = Setting(False)\n\n    _sorting_criteria_attrs = {\n        CompareNone: \"\", CompareMedians: \"median\", CompareMeans: \"mean\"\n    }\n\n    _pen_axis_tick = QPen(Qt.white, 5)\n    _pen_axis = QPen(Qt.darkGray, 3)\n    _pen_median = QPen(QBrush(QColor(0xff, 0xff, 0x00)), 2)\n    _pen_paramet = QPen(QBrush(QColor(0x33, 0x00, 0xff)), 2)\n    _pen_dotted = QPen(QBrush(QColor(0x33, 0x00, 0xff)), 1)\n    _pen_dotted.setStyle(Qt.DotLine)\n    _post_line_pen = QPen(Qt.lightGray, 2)\n    _post_grp_pen = QPen(Qt.lightGray, 4)\n    for pen in (_pen_paramet, _pen_median, _pen_dotted,\n                _pen_axis, _pen_axis_tick, _post_line_pen, _post_grp_pen):\n        pen.setCosmetic(True)\n        pen.setCapStyle(Qt.RoundCap)\n        pen.setJoinStyle(Qt.RoundJoin)\n    _pen_axis_tick.setCapStyle(Qt.FlatCap)\n\n    _box_brush = QBrush(QColor(0x33, 0x88, 0xff, 0xc0))\n\n    _axis_font = QFont()\n    _axis_font.setPixelSize(12)\n    _label_font = QFont()\n    _label_font.setPixelSize(11)\n    _attr_brush = QBrush(QColor(0x33, 0x00, 0xff))\n\n    graph_name = \"box_scene\"\n\n    def __init__(self):\n        super().__init__()\n        self.stats = []\n        self.dataset = None\n        self.posthoc_lines = []\n\n        self.label_txts = self.mean_labels = self.boxes = self.labels = \\\n            self.label_txts_all = self.attr_labels = self.order = []\n        self.scale_x = 1\n        self.scene_min_x = self.scene_max_x = self.scene_width = 0\n        self.label_width = 0\n\n        self.attrs = VariableListModel()\n        view = gui.listView(\n            self.controlArea, self, \"attribute\", box=\"Variable\",\n            model=self.attrs, callback=self.attr_changed)\n        view.setMinimumSize(QSize(30, 30))\n        # Any other policy than Ignored will let the QListBox's scrollbar\n        # set the minimal height (see the penultimate paragraph of\n        # http://doc.qt.io/qt-4.8/qabstractscrollarea.html#addScrollBarWidget)\n        view.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Ignored)\n        gui.checkBox(\n            view.box, self, \"order_by_importance\",\n            \"Order by relevance to subgroups\",\n            tooltip=\"Order by 𝜒² or ANOVA over the subgroups\",\n            callback=self.apply_attr_sorting)\n        self.group_vars = VariableListModel(placeholder=\"None\")\n        view = gui.listView(\n            self.controlArea, self, \"group_var\", box=\"Subgroups\",\n            model=self.group_vars, callback=self.grouping_changed)\n        gui.checkBox(\n            view.box, self, \"order_grouping_by_importance\",\n            \"Order by relevance to variable\",\n            tooltip=\"Order by 𝜒² or ANOVA over the variable values\",\n            callback=self.on_group_sorting_checkbox)\n        view.setMinimumSize(QSize(30, 30))\n        # See the comment above\n        view.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Ignored)\n\n        # TODO: move Compare median/mean to grouping box\n        # The vertical size policy is needed to let only the list views expand\n        self.display_box = gui.vBox(\n            self.controlArea, \"Display\",\n            sizePolicy=(QSizePolicy.Minimum, QSizePolicy.Maximum),\n            addSpace=False)\n\n        gui.checkBox(self.display_box, self, \"show_annotations\", \"Annotate\",\n                     callback=self.display_changed)\n        self.compare_rb = gui.radioButtonsInBox(\n            self.display_box, self, 'compare',\n            btnLabels=[\"No comparison\", \"Compare medians\", \"Compare means\"],\n            callback=self.layout_changed)\n\n        # The vertical size policy is needed to let only the list views expand\n        self.stretching_box = box = gui.vBox(\n            self.controlArea, box=\"Display\",\n            sizePolicy=(QSizePolicy.Minimum, QSizePolicy.Fixed))\n        self.stretching_box.sizeHint = self.display_box.sizeHint\n        gui.checkBox(\n            box, self, 'stretched', \"Stretch bars\",\n            callback=self.display_changed)\n        gui.checkBox(\n            box, self, 'show_labels', \"Show box labels\",\n            callback=self.display_changed)\n        self.sort_cb = gui.checkBox(\n            box, self, 'sort_freqs', \"Sort by subgroup frequencies\",\n            callback=self.display_changed)\n\n        gui.vBox(self.mainArea, addSpace=True)\n        self.box_scene = QGraphicsScene()\n        self.box_scene.selectionChanged.connect(self.commit)\n        self.box_view = QGraphicsView(self.box_scene)\n        self.box_view.setRenderHints(QPainter.Antialiasing |\n                                     QPainter.TextAntialiasing |\n                                     QPainter.SmoothPixmapTransform)\n        self.box_view.viewport().installEventFilter(self)\n\n        self.mainArea.layout().addWidget(self.box_view)\n\n        gui.hBox(self.mainArea, addSpace=False)\n        self.stat_test = \"\"\n        self.mainArea.setMinimumWidth(300)\n\n        self.stats = self.dist = self.conts = []\n        self.is_continuous = False\n\n        self.update_display_box()\n\n    def sizeHint(self):\n        return QSize(900, 500)\n\n    def eventFilter(self, obj, event):\n        if obj is self.box_view.viewport() and \\\n                event.type() == QEvent.Resize:\n            self.layout_changed()\n\n        return super().eventFilter(obj, event)\n\n    @property\n    def show_stretched(self):\n        return self.stretched and self.group_var is not self.attribute\n\n    def reset_attrs(self):\n        domain = self.dataset.domain\n        self.attrs[:] = [\n            var for var in chain(\n                domain.class_vars, domain.metas, domain.attributes)\n            if var.is_primitive()]\n\n    def reset_groups(self):\n        domain = self.dataset.domain\n        self.group_vars[:] = [None] + [\n            var for var in chain(\n                domain.class_vars, domain.metas, domain.attributes)\n            if var.is_discrete]\n\n    # noinspection PyTypeChecker\n    @Inputs.data\n    def set_data(self, dataset):\n        if dataset is not None and (\n                not bool(dataset) or not len(dataset.domain) and not\n                any(var.is_primitive() for var in dataset.domain.metas)):\n            dataset = None\n        self.closeContext()\n        self.dataset = dataset\n        self.dist = self.stats = self.conts = []\n        self.group_var = None\n        self.attribute = None\n        if dataset:\n            self.reset_attrs()\n            self.reset_groups()\n            self.select_default_variables()\n            self.openContext(self.dataset)\n            self.grouping_changed()\n            self.attr_changed()\n        else:\n            self.reset_all_data()\n        self.commit()\n\n    def select_default_variables(self):\n        # visualize first non-class variable, group by class (if present)\n        domain = self.dataset.domain\n        if len(self.attrs) > len(domain.class_vars):\n            self.attribute = self.attrs[len(domain.class_vars)]\n        elif self.attrs:\n            self.attribute = self.attrs[0]\n\n        if domain.class_var and domain.class_var.is_discrete:\n            self.group_var = domain.class_var\n        else:\n            self.group_var = None  # Reset to trigger selection via callback\n\n    def apply_attr_sorting(self):\n        def compute_score(attr):\n            # This function and the one in apply_group_sorting are similar, but\n            # different in too many details, so they are kept as separate\n            # functions.\n            # If you discover a bug in this function, check the other one, too.\n            if attr is group_var:\n                return 3\n            if attr.is_continuous:\n                # One-way ANOVA\n                col = data.get_column_view(attr)[0].astype(float)\n                groups = (col[group_col == i] for i in range(n_groups))\n                groups = (col[~np.isnan(col)] for col in groups)\n                groups = [group for group in groups if len(group)]\n                p = f_oneway(*groups)[1] if len(groups) > 1 else 2\n            else:\n                p = self._chi_square(group_var, attr)[1]\n            if math.isnan(p):\n                return 2\n            return p\n\n        data = self.dataset\n        if data is None:\n            return\n        domain = data.domain\n        attribute = self.attribute\n        group_var = self.group_var\n        if self.order_by_importance and group_var is not None:\n            n_groups = len(group_var.values)\n            group_col = data.get_column_view(group_var)[0] if \\\n                domain.has_continuous_attributes(\n                    include_class=True, include_metas=True) else None\n            self.attrs.sort(key=compute_score)\n        else:\n            self.reset_attrs()\n        self.attribute = attribute  # reset selection\n        self._ensure_selection_visible(self.controls.attribute)\n\n    def on_group_sorting_checkbox(self):\n        if self.order_grouping_by_importance:\n            self.apply_group_sorting()\n        else:\n            self.reset_groups()\n            self.group_var = self.group_var  # reset selection\n            self._ensure_selection_visible(self.controls.group_var)\n\n    def apply_group_sorting(self):\n        def compute_stat(group):\n            # This function and the one in apply_attr_sorting are similar, but\n            # different in too many details, so they are kept as separate\n            # functions.\n            # If you discover a bug in this function, check the other one, too.\n            if group is attr:\n                return 3\n            if group is None:\n                return -1\n            if attr.is_continuous:\n                group_col = data.get_column_view(group)[0].astype(float)\n                groups = (attr_col[group_col == i]\n                          for i in range(len(group.values)))\n                groups = (col[~np.isnan(col)] for col in groups)\n                groups = [group for group in groups if len(group)]\n                p = f_oneway(*groups)[1] if len(groups) > 1 else 2\n            else:\n                p = self._chi_square(group, attr)[1]\n            if math.isnan(p):\n                return 2\n            return p\n\n        data = self.dataset\n        if data is None or not self.order_grouping_by_importance:\n            return\n        attr = self.attribute\n        group_var = self.group_var\n        if attr.is_continuous:\n            attr_col = data.get_column_view(attr)[0].astype(float)\n        self.group_vars.sort(key=compute_stat)\n        self.group_var = group_var  # reset selection\n        self._ensure_selection_visible(self.controls.group_var)\n\n    @staticmethod\n    def _ensure_selection_visible(view):\n        selection = view.selectedIndexes()\n        if len(selection) == 1:\n            view.scrollTo(selection[0])\n\n    def _chi_square(self, group_var, attr):\n        # Chi-square with the given distribution into groups\n        if not attr.values or not group_var.values:\n            return 0, 2, 0\n        observed = np.array(\n            contingency.get_contingency(self.dataset, group_var, attr))\n        observed = observed[observed.sum(axis=1) != 0, :]\n        observed = observed[:, observed.sum(axis=0) != 0]\n        if min(observed.shape) < 2:\n            return 0, 2, 0\n        return chi2_contingency(observed)[:3]\n\n    def reset_all_data(self):\n        self.clear_scene()\n        self.stat_test = \"\"\n        self.attrs[:] = []\n        self.group_vars[:] = [None]\n        self.is_continuous = False\n        self.update_display_box()\n\n    def grouping_changed(self):\n        self.controls.stretched.setDisabled(self.group_var is self.attribute)\n        self.apply_attr_sorting()\n        self.update_graph()\n\n    def select_box_items(self):\n        temp_cond = self.conditions.copy()\n        for box in self.box_scene.items():\n            if isinstance(box, FilterGraphicsRectItem):\n                box.setSelected(box.filter.conditions in\n                                [c.conditions for c in temp_cond])\n\n    def attr_changed(self):\n        self.controls.stretched.setDisabled(self.group_var is self.attribute)\n        self.apply_group_sorting()\n        self.update_graph()\n\n    def update_graph(self):\n        self.compute_box_data()\n        self.update_display_box()\n        self.layout_changed()\n\n        if self.is_continuous:\n            heights = 90 if self.show_annotations else 60\n            self.box_view.centerOn(self.scene_min_x + self.scene_width / 2,\n                                   -30 - len(self.stats) * heights / 2 + 45)\n        else:\n            self.box_view.centerOn(self.scene_width / 2,\n                                   -30 - len(self.boxes) * 40 / 2 + 45)\n\n    def compute_box_data(self):\n        attr = self.attribute\n        if not attr:\n            return\n        dataset = self.dataset\n        self.is_continuous = attr.is_continuous\n        if dataset is None or not self.is_continuous and not attr.values or \\\n                        self.group_var and not self.group_var.values:\n            self.stats = self.dist = self.conts = []\n            return\n        if self.group_var:\n            self.dist = []\n            self.conts = contingency.get_contingency(\n                dataset, attr, self.group_var)\n            if self.is_continuous:\n                stats, label_texts = [], []\n                for i, cont in enumerate(self.conts):\n                    if np.sum(cont[1]):\n                        stats.append(BoxData(cont, attr, i, self.group_var))\n                        label_texts.append(self.group_var.values[i])\n                self.stats = stats\n                self.label_txts_all = label_texts\n            else:\n                self.label_txts_all = \\\n                    [v for v, c in zip(\n                        self.group_var.values + [\"Missing values\"],\n                        self.conts.array_with_unknowns)\n                     if np.sum(c) > 0]\n        else:\n            self.dist = distribution.get_distribution(dataset, attr)\n            self.conts = []\n            if self.is_continuous:\n                self.stats = [BoxData(self.dist, attr, None)]\n            self.label_txts_all = [\"\"]\n        self.label_txts = [txts for stat, txts in zip(self.stats,\n                                                      self.label_txts_all)\n                           if stat.n > 0]\n        self.stats = [stat for stat in self.stats if stat.n > 0]\n\n    def update_display_box(self):\n        if self.is_continuous:\n            self.stretching_box.hide()\n            self.display_box.show()\n            self.compare_rb.setEnabled(self.group_var is not None)\n        else:\n            self.stretching_box.show()\n            self.display_box.hide()\n            self.sort_cb.setEnabled(self.group_var is not None)\n\n    def clear_scene(self):\n        self.closeContext()\n        self.box_scene.clearSelection()\n        self.box_scene.clear()\n        self.box_view.viewport().update()\n        self.attr_labels = []\n        self.labels = []\n        self.boxes = []\n        self.mean_labels = []\n        self.posthoc_lines = []\n        self.openContext(self.dataset)\n\n    def layout_changed(self):\n        attr = self.attribute\n        if not attr:\n            return\n        self.clear_scene()\n        if self.dataset is None or len(self.conts) == len(self.dist) == 0:\n            return\n\n        if self.is_continuous:\n            self.mean_labels = [self.mean_label(stat, attr, lab)\n                                for stat, lab in zip(self.stats, self.label_txts)]\n            self.draw_axis()\n            self.boxes = [self.box_group(stat) for stat in self.stats]\n            self.labels = [self.label_group(stat, attr, mean_lab)\n                           for stat, mean_lab in zip(self.stats, self.mean_labels)]\n            self.attr_labels = [QGraphicsSimpleTextItem(lab)\n                                for lab in self.label_txts]\n            for it in chain(self.labels, self.attr_labels):\n                self.box_scene.addItem(it)\n\n        self.display_changed()\n\n    def display_changed(self):\n        if self.dataset is None or self.attribute is None:\n            return\n\n        if self.is_continuous:\n            self._display_changed_cont()\n        else:\n            self._display_changed_disc()\n        self.draw_stat()\n        self.select_box_items()\n\n    def _display_changed_cont(self):\n        self.order = list(range(len(self.stats)))\n        criterion = self._sorting_criteria_attrs[self.compare]\n        if criterion:\n            vals = [getattr(stat, criterion) for stat in self.stats]\n            overmax = max((val for val in vals if val is not None), default=0) \\\n                      + 1\n            vals = [val if val is not None else overmax for val in vals]\n            self.order = sorted(self.order, key=vals.__getitem__)\n\n        heights = 90 if self.show_annotations else 60\n\n        for row, box_index in enumerate(self.order):\n            y = (-len(self.stats) + row) * heights + 10\n            for item in self.boxes[box_index]:\n                self.box_scene.addItem(item)\n                item.setY(y)\n            labels = self.labels[box_index]\n\n            if self.show_annotations:\n                labels.show()\n                labels.setY(y)\n            else:\n                labels.hide()\n\n            label = self.attr_labels[box_index]\n            label.setY(y - 15 - label.boundingRect().height())\n            if self.show_annotations:\n                label.hide()\n            else:\n                stat = self.stats[box_index]\n\n                if self.compare == OWBoxPlot.CompareMedians and \\\n                        stat.median is not None:\n                    pos = stat.median + 5 / self.scale_x\n                elif self.compare == OWBoxPlot.CompareMeans or stat.q25 is None:\n                    pos = stat.mean + 5 / self.scale_x\n                else:\n                    pos = stat.q25\n                label.setX(pos * self.scale_x)\n                label.show()\n\n        r = QRectF(self.scene_min_x, -30 - len(self.stats) * heights,\n                   self.scene_width, len(self.stats) * heights + 90)\n        self.box_scene.setSceneRect(r)\n\n        self._compute_tests_cont()\n        self._show_posthoc()\n\n    def _display_changed_disc(self):\n        self.clear_scene()\n        self.attr_labels = [QGraphicsSimpleTextItem(lab)\n                            for lab in self.label_txts_all]\n\n        if not self.show_stretched:\n            if self.group_var:\n                self.labels = [\n                    QGraphicsTextItem(\"{}\".format(int(sum(cont))))\n                    for cont in self.conts.array_with_unknowns\n                    if np.sum(cont) > 0]\n            else:\n                self.labels = [\n                    QGraphicsTextItem(str(int(sum(self.dist))))]\n\n        self.order = list(range(len(self.attr_labels)))\n\n        self.draw_axis_disc()\n        if self.group_var:\n            self.boxes = \\\n                [self.strudel(cont, i)\n                 for i, cont in enumerate(self.conts.array_with_unknowns)\n                 if np.sum(cont) > 0]\n\n            if self.sort_freqs:\n                # pylint: disable=invalid-unary-operand-type\n                self.order = sorted(\n                    self.order, key=(-np.sum(\n                        self.conts.array_with_unknowns, axis=1)).__getitem__)\n        else:\n            self.boxes = [self.strudel(self.dist.array_with_unknowns)]\n\n        for row, box_index in enumerate(self.order):\n            y = (-len(self.boxes) + row) * 40 + 10\n            box = self.boxes[box_index]\n            bars, labels = box[::2], box[1::2]\n\n            self.__draw_group_labels(y, box_index)\n            if not self.show_stretched:\n                self.__draw_row_counts(y, box_index)\n            if self.show_labels and self.attribute is not self.group_var:\n                self.__draw_bar_labels(y, bars, labels)\n            self.__draw_bars(y, bars)\n\n        self.box_scene.setSceneRect(-self.label_width - 5,\n                                    -30 - len(self.boxes) * 40,\n                                    self.scene_width, len(self.boxes * 40) + 90)\n        self._compute_tests_disc()\n\n    def __draw_group_labels(self, y, row):\n        \"\"\"Draw group labels\n\n        Parameters\n        ----------\n        y: int\n            vertical offset of bars\n        row: int\n            row index\n        \"\"\"\n        label = self.attr_labels[row]\n        b = label.boundingRect()\n        label.setPos(-b.width() - 10, y - b.height() / 2)\n        self.box_scene.addItem(label)\n\n    def __draw_row_counts(self, y, row):\n        \"\"\"Draw row counts\n\n        Parameters\n        ----------\n        y: int\n            vertical offset of bars\n        row: int\n            row index\n        \"\"\"\n        assert not self.is_continuous\n        label = self.labels[row]\n        b = label.boundingRect()\n        if self.group_var:\n            right = self.scale_x * sum(self.conts.array_with_unknowns[row])\n        else:\n            right = self.scale_x * sum(self.dist)\n        label.setPos(right + 10, y - b.height() / 2)\n        self.box_scene.addItem(label)\n\n    def __draw_bar_labels(self, y, bars, labels):\n        \"\"\"Draw bar labels\n\n        Parameters\n        ----------\n        y: int\n            vertical offset of bars\n        bars: List[FilterGraphicsRectItem]\n            list of bars being drawn\n        labels: List[QGraphicsTextItem]\n            list of labels for corresponding bars\n        \"\"\"\n        label = bar_part = None\n        for text_item, bar_part in zip(labels, bars):\n            label = self.Label(\n                text_item.toPlainText())\n            label.setPos(bar_part.boundingRect().x(),\n                         y - label.boundingRect().height() - 8)\n            label.setMaxWidth(bar_part.boundingRect().width())\n            self.box_scene.addItem(label)\n\n    def __draw_bars(self, y, bars):\n        \"\"\"Draw bars\n\n        Parameters\n        ----------\n        y: int\n            vertical offset of bars\n\n        bars: List[FilterGraphicsRectItem]\n            list of bars to draw\n        \"\"\"\n        for item in bars:\n            item.setPos(0, y)\n            self.box_scene.addItem(item)\n\n    # noinspection PyPep8Naming\n    def _compute_tests_cont(self):\n        # The t-test and ANOVA are implemented here since they efficiently use\n        # the widget-specific data in self.stats.\n        # The non-parametric tests can't do this, so we use statistics.tests\n\n        # pylint: disable=comparison-with-itself\n        def stat_ttest():\n            d1, d2 = self.stats\n            if d1.n < 2 or d2.n < 2:\n                return np.nan, np.nan\n            pooled_var = d1.var / d1.n + d2.var / d2.n\n            # pylint: disable=comparison-with-itself\n            if pooled_var == 0 or np.isnan(pooled_var):\n                return np.nan, np.nan\n            df = pooled_var ** 2 / \\\n                ((d1.var / d1.n) ** 2 / (d1.n - 1) +\n                 (d2.var / d2.n) ** 2 / (d2.n - 1))\n            t = abs(d1.mean - d2.mean) / math.sqrt(pooled_var)\n            p = 2 * (1 - scipy.special.stdtr(df, t))\n            return t, p\n\n        # TODO: Check this function\n        # noinspection PyPep8Naming\n        def stat_ANOVA():\n            if any(stat.n == 0 for stat in self.stats):\n                return np.nan, np.nan\n            n = sum(stat.n for stat in self.stats)\n            grand_avg = sum(stat.n * stat.mean for stat in self.stats) / n\n            var_between = sum(stat.n * (stat.mean - grand_avg) ** 2\n                              for stat in self.stats)\n            df_between = len(self.stats) - 1\n\n            var_within = sum(stat.n * stat.var for stat in self.stats)\n            df_within = n - len(self.stats)\n            if var_within == 0 or df_within == 0 or df_between == 0:\n                return np.nan, np.nan\n            F = (var_between / df_between) / (var_within / df_within)\n            p = 1 - scipy.special.fdtr(df_between, df_within, F)\n            return F, p\n\n        n = len(self.dataset)\n        if self.compare == OWBoxPlot.CompareNone or len(self.stats) < 2:\n            t = \"\"\n        elif any(s.n <= 1 for s in self.stats):\n            t = \"At least one group has just one instance, \" \\\n                \"cannot compute significance\"\n        elif len(self.stats) == 2:\n            if self.compare == OWBoxPlot.CompareMedians:\n                t = \"\"\n                # z, p = tests.wilcoxon_rank_sum(\n                #    self.stats[0].dist, self.stats[1].dist)\n                # t = \"Mann-Whitney's z: %.1f (p=%.3f)\" % (z, p)\n            else:\n                t, p = stat_ttest()\n                t = \"\" if np.isnan(t) else f\"Student's t: {t:.3f} (p={p:.3f}, N={n})\"\n        else:\n            if self.compare == OWBoxPlot.CompareMedians:\n                t = \"\"\n                # U, p = -1, -1\n                # t = \"Kruskal Wallis's U: %.1f (p=%.3f)\" % (U, p)\n            else:\n                F, p = stat_ANOVA()\n                t = \"\" if np.isnan(F) else f\"ANOVA: {F:.3f} (p={p:.3f}, N={n})\"\n        self.stat_test = t\n\n    def _compute_tests_disc(self):\n        if self.group_var is None or self.attribute is None:\n            self.stat_test = \"\"\n        else:\n            chi, p, dof = self._chi_square(self.group_var, self.attribute)\n            if np.isnan(p):\n                self.stat_test = \"\"\n            else:\n                self.stat_test = f\"χ²: {chi:.2f} (p={p:.3f}, dof={dof})\"\n\n    def mean_label(self, stat, attr, val_name):\n        label = QGraphicsItemGroup()\n        t = QGraphicsSimpleTextItem(attr.str_val(stat.mean), label)\n        t.setFont(self._label_font)\n        bbox = t.boundingRect()\n        w2, h = bbox.width() / 2, bbox.height()\n        t.setPos(-w2, -h)\n        tpm = QGraphicsSimpleTextItem(\n            \" \\u00b1 \" + \"%.*f\" % (attr.number_of_decimals + 1, stat.dev),\n            label)\n        tpm.setFont(self._label_font)\n        tpm.setPos(w2, -h)\n        if val_name:\n            vnm = QGraphicsSimpleTextItem(val_name + \": \", label)\n            vnm.setFont(self._label_font)\n            vnm.setBrush(self._attr_brush)\n            vb = vnm.boundingRect()\n            label.min_x = -w2 - vb.width()\n            vnm.setPos(label.min_x, -h)\n        else:\n            label.min_x = -w2\n        return label\n\n    def draw_axis(self):\n        \"\"\"Draw the horizontal axis and sets self.scale_x\"\"\"\n        misssing_stats = not self.stats\n        stats = self.stats or [BoxData(np.array([[0.], [1.]]), self.attribute)]\n        mean_labels = self.mean_labels or [self.mean_label(stats[0], self.attribute, \"\")]\n        bottom = min(stat.a_min for stat in stats)\n        top = max(stat.a_max for stat in stats)\n\n        first_val, step = compute_scale(bottom, top)\n        while bottom <= first_val:\n            first_val -= step\n        bottom = first_val\n        no_ticks = math.ceil((top - first_val) / step) + 1\n        top = max(top, first_val + no_ticks * step)\n\n        gbottom = min(bottom, min(stat.mean - stat.dev for stat in stats))\n        gtop = max(top, max(stat.mean + stat.dev for stat in stats))\n\n        bv = self.box_view\n        viewrect = bv.viewport().rect().adjusted(15, 15, -15, -30)\n        self.scale_x = scale_x = viewrect.width() / (gtop - gbottom)\n\n        # In principle we should repeat this until convergence since the new\n        # scaling is too conservative. (No chance am I doing this.)\n        mlb = min(stat.mean + mean_lab.min_x / scale_x\n                  for stat, mean_lab in zip(stats, mean_labels))\n        if mlb < gbottom:\n            gbottom = mlb\n            self.scale_x = scale_x = viewrect.width() / (gtop - gbottom)\n\n        self.scene_min_x = gbottom * scale_x\n        self.scene_max_x = gtop * scale_x\n        self.scene_width = self.scene_max_x - self.scene_min_x\n\n        val = first_val\n        last_text = self.scene_min_x\n        while True:\n            l = self.box_scene.addLine(val * scale_x, -1, val * scale_x, 1,\n                                       self._pen_axis_tick)\n            l.setZValue(100)\n            t = QGraphicsSimpleTextItem(\n                self.attribute.str_val(val) if not misssing_stats else \"?\")\n            t.setFont(self._axis_font)\n            t.setFlag(QGraphicsItem.ItemIgnoresTransformations)\n            r = t.boundingRect()\n            x_start = val * scale_x - r.width() / 2\n            x_finish = x_start + r.width()\n            if x_start > last_text + 10 and x_finish < self.scene_max_x:\n                t.setPos(x_start, 8)\n                self.box_scene.addItem(t)\n                last_text = x_finish\n            if val >= top:\n                break\n            val += step\n        self.box_scene.addLine(\n            bottom * scale_x - 4, 0, top * scale_x + 4, 0, self._pen_axis)\n\n    def draw_stat(self):\n        if self.stat_test:\n            label = QGraphicsSimpleTextItem(self.stat_test)\n            brect = self.box_scene.sceneRect()\n            label.setPos(brect.center().x() - label.boundingRect().width()/2,\n                         8 + self._axis_font.pixelSize()*2)\n            label.setFlag(QGraphicsItem.ItemIgnoresTransformations)\n            self.box_scene.addItem(label)\n\n    def draw_axis_disc(self):\n        \"\"\"\n        Draw the horizontal axis and sets self.scale_x for discrete attributes\n        \"\"\"\n        assert not self.is_continuous\n        if self.show_stretched:\n            if not self.attr_labels:\n                return\n            step = steps = 10\n        else:\n            if self.group_var:\n                max_box = max(float(np.sum(dist))\n                              for dist in self.conts.array_with_unknowns)\n            else:\n                max_box = float(np.sum(self.dist.array_with_unknowns))\n            if max_box == 0:\n                self.scale_x = 1\n                return\n            _, step = compute_scale(0, max_box)\n            step = int(step) if step > 1 else 1\n            steps = int(math.ceil(max_box / step))\n        max_box = step * steps\n\n        bv = self.box_view\n        viewrect = bv.viewport().rect().adjusted(15, 15, -15, -30)\n        self.scene_width = viewrect.width()\n\n        lab_width = max(lab.boundingRect().width() for lab in self.attr_labels)\n        lab_width = max(lab_width, 40)\n        lab_width = min(lab_width, self.scene_width / 3)\n        self.label_width = lab_width\n\n        right_offset = 0  # offset for the right label\n        if not self.show_stretched and self.labels:\n            if self.group_var:\n                rows = list(zip(self.conts.array_with_unknowns, self.labels))\n            else:\n                rows = [(self.dist, self.labels[0])]\n            # available space left of the 'group labels'\n            available = self.scene_width - lab_width - 10\n            scale_x = (available - right_offset) / max_box\n            max_right = max(sum(dist) * scale_x + 10 +\n                            lbl.boundingRect().width()\n                            for dist, lbl in rows)\n            right_offset = max(0, max_right - max_box * scale_x)\n\n        self.scale_x = scale_x = \\\n            (self.scene_width - lab_width - 10 - right_offset) / max_box\n\n        self.box_scene.addLine(0, 0, max_box * scale_x, 0, self._pen_axis)\n        for val in range(0, step * steps + 1, step):\n            l = self.box_scene.addLine(val * scale_x, -1, val * scale_x, 1,\n                                       self._pen_axis_tick)\n            l.setZValue(100)\n            t = self.box_scene.addSimpleText(str(val), self._axis_font)\n            t.setPos(val * scale_x - t.boundingRect().width() / 2, 8)\n        if self.show_stretched:\n            self.scale_x *= 100\n\n    def label_group(self, stat, attr, mean_lab):\n        def centered_text(val, pos):\n            t = QGraphicsSimpleTextItem(attr.str_val(val), labels)\n            t.setFont(self._label_font)\n            bbox = t.boundingRect()\n            t.setPos(pos - bbox.width() / 2, 22)\n            return t\n\n        def line(x, down=1):\n            QGraphicsLineItem(x, 12 * down, x, 20 * down, labels)\n\n        def move_label(label, frm, to):\n            label.setX(to)\n            to += t_box.width() / 2\n            path = QPainterPath()\n            path.lineTo(0, 4)\n            path.lineTo(to - frm, 4)\n            path.lineTo(to - frm, 8)\n            p = QGraphicsPathItem(path)\n            p.setPos(frm, 12)\n            labels.addToGroup(p)\n\n        labels = QGraphicsItemGroup()\n\n        labels.addToGroup(mean_lab)\n        m = stat.mean * self.scale_x\n        mean_lab.setPos(m, -22)\n        line(m, -1)\n\n        if stat.median is not None:\n            msc = stat.median * self.scale_x\n            med_t = centered_text(stat.median, msc)\n            med_box_width2 = med_t.boundingRect().width() / 2\n            line(msc)\n\n        if stat.q25 is not None:\n            x = stat.q25 * self.scale_x\n            t = centered_text(stat.q25, x)\n            t_box = t.boundingRect()\n            med_left = msc - med_box_width2\n            if x + t_box.width() / 2 >= med_left - 5:\n                move_label(t, x, med_left - t_box.width() - 5)\n            else:\n                line(x)\n\n        if stat.q75 is not None:\n            x = stat.q75 * self.scale_x\n            t = centered_text(stat.q75, x)\n            t_box = t.boundingRect()\n            med_right = msc + med_box_width2\n            if x - t_box.width() / 2 <= med_right + 5:\n                move_label(t, x, med_right + 5)\n            else:\n                line(x)\n\n        return labels\n\n    def box_group(self, stat, height=20):\n        def line(x0, y0, x1, y1, *args):\n            return QGraphicsLineItem(x0 * scale_x, y0, x1 * scale_x, y1, *args)\n\n        scale_x = self.scale_x\n        box = []\n        whisker1 = line(stat.a_min, -1.5, stat.a_min, 1.5)\n        whisker2 = line(stat.a_max, -1.5, stat.a_max, 1.5)\n        vert_line = line(stat.a_min, 0, stat.a_max, 0)\n        mean_line = line(stat.mean, -height / 3, stat.mean, height / 3)\n        for it in (whisker1, whisker2, mean_line):\n            it.setPen(self._pen_paramet)\n        vert_line.setPen(self._pen_dotted)\n        var_line = line(stat.mean - stat.dev, 0, stat.mean + stat.dev, 0)\n        var_line.setPen(self._pen_paramet)\n        box.extend([whisker1, whisker2, vert_line, mean_line, var_line])\n        if stat.q25 is not None or stat.q75 is not None:\n            # if any of them is None it means that its value is equal to median\n            box_from = stat.q25 or stat.median\n            box_to = stat.q75 or stat.median\n            mbox = FilterGraphicsRectItem(\n                stat.conditions, box_from * scale_x, -height / 2,\n                (box_to - box_from) * scale_x, height)\n            mbox.setBrush(self._box_brush)\n            mbox.setPen(QPen(Qt.NoPen))\n            mbox.setZValue(-200)\n            box.append(mbox)\n\n        if stat.median is not None:\n            median_line = line(stat.median, -height / 2,\n                               stat.median, height / 2)\n            median_line.setPen(self._pen_median)\n            median_line.setZValue(-150)\n            box.append(median_line)\n\n        return box\n\n    def strudel(self, dist, group_val_index=None):\n        attr = self.attribute\n        ss = np.sum(dist)\n        box = []\n        if ss < 1e-6:\n            cond = [FilterDiscrete(attr, None)]\n            if group_val_index is not None:\n                cond.append(FilterDiscrete(self.group_var, [group_val_index]))\n            box.append(FilterGraphicsRectItem(cond, 0, -10, 1, 10))\n        cum = 0\n        values = attr.values + [\"Missing values\"]\n        colors = np.vstack((attr.colors, [128, 128, 128]))\n        for i, v in enumerate(dist):\n            if v < 1e-6:\n                continue\n            if self.show_stretched:\n                v /= ss\n            v *= self.scale_x\n            cond = [FilterDiscrete(attr, [i])]\n            if group_val_index is not None:\n                cond.append(FilterDiscrete(self.group_var, [group_val_index]))\n            rect = FilterGraphicsRectItem(cond, cum + 1, -6, v - 2, 12)\n            rect.setBrush(QBrush(QColor(*colors[i])))\n            rect.setPen(QPen(Qt.NoPen))\n            if self.show_stretched:\n                tooltip = \"{}: {:.2f}%\".format(\n                    values[i],\n                    100 * dist[i] / sum(dist))\n            else:\n                tooltip = \"{}: {}\".format(values[i], int(dist[i]))\n            rect.setToolTip(tooltip)\n            text = QGraphicsTextItem(values[i])\n            box.append(rect)\n            box.append(text)\n            cum += v\n        return box\n\n    def commit(self):\n        self.conditions = [item.filter for item in\n                           self.box_scene.selectedItems() if item.filter]\n        selected, selection = None, []\n        if self.conditions:\n            selected = Values(self.conditions, conjunction=False)(self.dataset)\n            selection = np.in1d(\n                self.dataset.ids, selected.ids, assume_unique=True).nonzero()[0]\n        self.Outputs.selected_data.send(selected)\n        self.Outputs.annotated_data.send(\n            create_annotated_table(self.dataset, selection))\n\n    def _show_posthoc(self):\n        def line(y0, y1):\n            it = self.box_scene.addLine(x, y0, x, y1, self._post_line_pen)\n            it.setZValue(-100)\n            self.posthoc_lines.append(it)\n\n        while self.posthoc_lines:\n            self.box_scene.removeItem(self.posthoc_lines.pop())\n\n        if self.compare == OWBoxPlot.CompareNone or len(self.stats) < 2:\n            return\n\n        if self.compare == OWBoxPlot.CompareMedians:\n            crit_line = \"median\"\n        else:\n            crit_line = \"mean\"\n\n        xs = []\n\n        height = 90 if self.show_annotations else 60\n\n        y_up = -len(self.stats) * height + 10\n        for pos, box_index in enumerate(self.order):\n            stat = self.stats[box_index]\n            x = getattr(stat, crit_line)\n            if x is None:\n                continue\n            x *= self.scale_x\n            xs.append(x * self.scale_x)\n            by = y_up + pos * height\n            line(by + 12, 0)\n\n        used_to = []\n        last_to = to = 0\n        for frm, frm_x in enumerate(xs[:-1]):\n            for to in range(frm + 1, len(xs)):\n                if xs[to] - frm_x > 1.5:\n                    to -= 1\n                    break\n            if to in (last_to, frm):\n                continue\n            for rowi, used in enumerate(used_to):\n                if used < frm:\n                    used_to[rowi] = to\n                    break\n            else:\n                rowi = len(used_to)\n                used_to.append(to)\n            y = - 6 - rowi * 6\n            it = self.box_scene.addLine(frm_x - 2, y, xs[to] + 2, y,\n                                        self._post_grp_pen)\n            self.posthoc_lines.append(it)\n            last_to = to\n\n    def get_widget_name_extension(self):\n        return self.attribute.name if self.attribute else None\n\n    def send_report(self):\n        self.report_plot()\n        text = \"\"\n        if self.attribute:\n            text += \"Box plot for attribute '{}' \".format(self.attribute.name)\n        if self.group_var:\n            text += \"grouped by '{}'\".format(self.group_var.name)\n        if text:\n            self.report_caption(text)\n\n    class Label(QGraphicsSimpleTextItem):\n        \"\"\"Boxplot Label with settable maxWidth\"\"\"\n        # Minimum width to display label text\n        MIN_LABEL_WIDTH = 25\n\n        # padding bellow the text\n        PADDING = 3\n\n        __max_width = None\n\n        def maxWidth(self):\n            return self.__max_width\n\n        def setMaxWidth(self, max_width):\n            self.__max_width = max_width\n\n        def paint(self, painter, option, widget):\n            \"\"\"Overrides QGraphicsSimpleTextItem.paint\n\n            If label text is too long, it is elided\n            to fit into the allowed region\n            \"\"\"\n            if self.__max_width is None:\n                width = option.rect.width()\n            else:\n                width = self.__max_width\n\n            if width < self.MIN_LABEL_WIDTH:\n                # if space is too narrow, no label\n                return\n\n            fm = painter.fontMetrics()\n            text = fm.elidedText(self.text(), Qt.ElideRight, width)\n            painter.drawText(\n                option.rect.x(),\n                option.rect.y() + self.boundingRect().height() - self.PADDING,\n                text)",
                            "ast_type_str": "ClassDef",
                            "start_lineno": 108,
                            "end_lineno": 1216,
                            "parent": null
                        }
                    ],
                    "added_entities": [],
                    "deleted_entities": []
                }
            ]
        },
        {
            "old_file_content": "# Test methods with long descriptive names can omit docstrings\n# pylint: disable=missing-docstring\nimport unittest\nfrom unittest.mock import patch\n\nimport numpy as np\nfrom AnyQt.QtCore import QItemSelectionModel\n\nfrom Orange.data import Table, ContinuousVariable, StringVariable, Domain, \\\n    DiscreteVariable\nfrom Orange.widgets.visualize.owboxplot import (\n    OWBoxPlot, FilterGraphicsRectItem, _quantiles\n)\nfrom Orange.widgets.tests.base import WidgetTest, WidgetOutputsTestMixin\nfrom Orange.tests import test_filename\n\n\nclass TestOWBoxPlot(WidgetTest, WidgetOutputsTestMixin):\n    @classmethod\n    def setUpClass(cls):\n        super().setUpClass()\n        WidgetOutputsTestMixin.init(cls)\n\n        cls.iris = Table(\"iris\")\n        cls.zoo = Table(\"zoo\")\n        cls.housing = Table(\"housing\")\n        cls.titanic = Table(\"titanic\")\n        cls.heart = Table(\"heart_disease\")\n        cls.data = cls.iris\n        cls.signal_name = \"Data\"\n        cls.signal_data = cls.data\n\n    def setUp(self):\n        self.widget = self.create_widget(OWBoxPlot)\n\n    def test_input_data(self):\n        \"\"\"Check widget's data\"\"\"\n        self.send_signal(self.widget.Inputs.data, self.iris)\n        self.assertEqual(len(self.widget.attrs), 5)\n        self.assertEqual(len(self.widget.group_vars), 2)\n        self.assertFalse(self.widget.display_box.isHidden())\n        self.assertTrue(self.widget.stretching_box.isHidden())\n\n        self.send_signal(self.widget.Inputs.data, None)\n        self.assertEqual(len(self.widget.attrs), 0)\n        self.assertEqual(len(self.widget.group_vars), 1)\n        self.assertTrue(self.widget.display_box.isHidden())\n        self.assertFalse(self.widget.stretching_box.isHidden())\n\n    def test_primitive_metas(self):\n        new_domain = Domain(attributes=[], class_vars=[], metas=(\n            self.data.domain.attributes + self.data.domain.class_vars))\n        attrs_as_metas = self.data.transform(new_domain)\n        self.send_signal(self.widget.Inputs.data, attrs_as_metas)\n        self.assertTrue(self.widget.display_box.isEnabled())\n\n    def test_input_data_missings_cont_group_var(self):\n        \"\"\"Check widget with continuous data with missing values and group variable\"\"\"\n        data = self.iris.copy()\n        data.X[:, 0] = np.nan\n        self.send_signal(self.widget.Inputs.data, data)\n        # used to crash, see #1568\n\n    def test_input_data_missings_cont_no_group_var(self):\n        \"\"\"Check widget with continuous data with missing values and no group variable\"\"\"\n        data = self.housing\n        data.X[:, 0] = np.nan\n        self.send_signal(self.widget.Inputs.data, data)\n        # used to crash, see #1568\n\n    def test_input_data_missings_disc_group_var(self):\n        \"\"\"Check widget with discrete data with missing values and group variable\"\"\"\n        data = self.zoo\n        data.X[:, 1] = np.nan\n        data.domain.attributes[1].values = []\n        self.send_signal(\"Data\", data)\n        self.widget.controls.order_by_importance.setChecked(True)\n        self._select_list_items(self.widget.controls.attribute)\n        self._select_list_items(self.widget.controls.group_var)\n\n    def test_input_data_missings_disc_no_group_var(self):\n        \"\"\"Check widget discrete data with missing values and no group variable\"\"\"\n        data = self.zoo\n        data.domain.class_var = ContinuousVariable(\"cls\")\n        data.X[:, 1] = np.nan\n        data.domain.attributes[1].values = []\n        self.send_signal(\"Data\", data)\n        self._select_list_items(self.widget.controls.attribute)\n        self._select_list_items(self.widget.controls.group_var)\n\n    def test_attribute_combinations(self):\n        self.send_signal(self.widget.Inputs.data, self.heart)\n        group_list = self.widget.controls.group_var\n        m = group_list.selectionModel()\n        for i in range(len(group_list.model())):\n            m.setCurrentIndex(group_list.model().index(i), m.ClearAndSelect)\n            self._select_list_items(self.widget.controls.attribute)\n\n    def test_apply_sorting_group(self):\n        controls = self.widget.controls\n        group_list = controls.group_var\n        order_check = controls.order_by_importance\n        attributes = self.widget.attrs\n\n        def select_group(i):\n            group_selection = group_list.selectionModel()\n            group_selection.setCurrentIndex(\n                group_list.model().index(i),\n                group_selection.ClearAndSelect)\n\n        data = self.titanic\n        self.send_signal(\"Data\", data)\n\n        select_group(2)  # First attribute\n\n        order_check.setChecked(False)\n        self.assertEqual(tuple(attributes),\n                         data.domain.class_vars + data.domain.attributes)\n        order_check.setChecked(True)\n        self.assertEqual([x.name for x in attributes],\n                         ['sex', 'survived', 'age', 'status'])\n        select_group(1)  # Class\n        self.assertEqual([x.name for x in attributes],\n                         ['sex', 'status', 'age', 'survived'])\n\n        data = self.heart\n        self.send_signal(\"Data\", data)\n        select_group(1)  # Class\n        order_check.setChecked(True)\n        self.assertEqual([x.name for x in attributes],\n                         ['thal',\n                          'chest pain',\n                          'major vessels colored',\n                          'ST by exercise',\n                          'max HR',\n                          'exerc ind ang',\n                          'slope peak exc ST',\n                          'gender',\n                          'age',\n                          'rest ECG',\n                          'rest SBP',\n                          'cholesterol',\n                          'fasting blood sugar > 120',\n                          'diameter narrowing'])\n\n    def test_apply_sorting_vars(self):\n        controls = self.widget.controls\n        attr_list = self.widget.attrs\n        order_check = controls.order_grouping_by_importance\n        groups = self.widget.group_vars\n\n        def select_attr(i):\n            attr_selection = controls.attribute.selectionModel()\n            attr_selection.setCurrentIndex(\n                attr_list.index(i),\n                attr_selection.ClearAndSelect)\n\n        data = self.titanic\n        self.send_signal(\"Data\", data)\n\n        select_attr(1)  # First attribute\n\n        order_check.setChecked(False)\n        self.assertEqual(\n            tuple(groups),\n            (None, ) + data.domain.class_vars + data.domain.attributes)\n        order_check.setChecked(True)\n        self.assertIsNone(groups[0])\n        self.assertEqual([x.name for x in groups[1:]],\n                         ['sex', 'survived', 'age', 'status'])\n        select_attr(0)  # Class\n        self.assertIsNone(groups[0])\n        self.assertEqual([x.name for x in groups[1:]],\n                         ['sex', 'status', 'age', 'survived'])\n\n        data = self.heart\n        self.send_signal(\"Data\", data)\n        select_attr(0)  # Class\n        self.assertIsNone(groups[0])\n        self.assertEqual([x.name for x in groups[1:]],\n                         ['thal',\n                          'chest pain',\n                          'exerc ind ang',\n                          'slope peak exc ST',\n                          'gender',\n                          'rest ECG',\n                          'fasting blood sugar > 120',\n                          'diameter narrowing'])\n\n    def test_box_order_when_missing_stats(self):\n        self.widget.compare = 1\n        # The widget can't do anything smart here, but shouldn't crash\n        self.send_signal(self.widget.Inputs.data, self.iris[49:51])\n\n    def test_saved_selection(self):\n        self.send_signal(self.widget.Inputs.data, self.data)\n        selected_indices = self._select_data()\n        self.send_signal(self.widget.Inputs.data, self.zoo)\n        self.assertIsNone(self.get_output(self.widget.Outputs.selected_data))\n        self.send_signal(self.widget.Inputs.data, self.data)\n        np.testing.assert_array_equal(self.get_output(self.widget.Outputs.selected_data).X,\n                                      self.data.X[selected_indices])\n\n    def test_continuous_metas(self):\n        domain = self.iris.domain\n        metas = domain.attributes[:-1] + (StringVariable(\"str\"),)\n        domain = Domain([], domain.class_var, metas)\n        data = Table.from_table(domain, self.iris)\n        self.send_signal(self.widget.Inputs.data, data)\n\n    def test_label_overlap(self):\n        self.send_signal(self.widget.Inputs.data, self.heart)\n        self.widget.stretched = False\n        self.__select_variable(\"chest pain\")\n        self.__select_group(\"gender\")\n        self.widget.adjustSize()\n        self.widget.layout().activate()\n        self.widget.grab()  # ensure that the painting code is run\n\n    def test_empty_groups(self):\n        \"\"\"Test if groups with zero elements are not shown\"\"\"\n        table = Table(test_filename(\"datasets/cyber-security-breaches.tab\"))\n        self.send_signal(self.widget.Inputs.data, table)\n        self.__select_variable(\"US State\")\n        self.__select_group(\"US State\")\n        self.assertEqual(52, len(self.widget.boxes))\n\n        # select rows with US State equal to TX or MO\n        use_indexes = np.array([0, 1, 25, 26, 27])\n        table.X = table.X[use_indexes]\n        self.send_signal(self.widget.Inputs.data, table)\n        self.assertEqual(2, len(self.widget.boxes))\n\n    def test_sorting_disc_group_var(self):\n        \"\"\"Test if subgroups are sorted by their size\"\"\"\n        table = Table(\"heart_disease\")\n        self.send_signal(self.widget.Inputs.data, table)\n        self.__select_variable(\"gender\")\n        self.__select_group(\"chest pain\")\n\n        # checkbox not checked - preserve original order of selected grouping attribute\n        self.assertListEqual(self.widget.order, [0, 1, 2, 3])\n\n        # checkbox checked - sort by frequencies\n        self.widget.controls.sort_freqs.setChecked(True)\n        self.assertListEqual(self.widget.order, [0, 2, 1, 3])\n\n    def _select_data(self):\n        items = [item for item in self.widget.box_scene.items()\n                 if isinstance(item, FilterGraphicsRectItem)]\n        items[0].setSelected(True)\n        return [100, 103, 104, 108, 110, 111, 112, 115, 116,\n                120, 123, 124, 126, 128, 132, 133, 136, 137,\n                139, 140, 141, 143, 144, 145, 146, 147, 148]\n\n    def _select_list_items(self, _list):\n        model = _list.selectionModel()\n        for i in range(len(_list.model())):\n            model.setCurrentIndex(_list.model().index(i), model.ClearAndSelect)\n\n    def __select_variable(self, name, widget=None):\n        if widget is None:\n            widget = self.widget\n\n        self.__select_value(widget.controls.attribute, name)\n\n    def __select_group(self, name, widget=None):\n        if widget is None:\n            widget = self.widget\n\n        self.__select_value(widget.controls.group_var, name)\n\n    def __select_value(self, list, value):\n        m = list.model()\n        for i in range(m.rowCount()):\n            idx = m.index(i)\n            if m.data(idx) == value:\n                list.selectionModel().setCurrentIndex(\n                    idx, QItemSelectionModel.ClearAndSelect)\n\n    def test_unconditional_commit_on_new_signal(self):\n        with patch.object(self.widget, 'commit') as apply:\n            apply.reset_mock()\n            self.send_signal(self.widget.Inputs.data, self.zoo)\n            apply.assert_called()\n\n    def test_stretching(self):\n        self.send_signal(self.widget.Inputs.data, self.heart)\n        enabled = self.widget.controls.stretched.isEnabled\n\n        self.__select_variable(\"chest pain\")\n        self.__select_group(\"gender\")\n        self.assertTrue(enabled())\n\n        self.__select_variable(\"gender\")\n        self.__select_group(\"gender\")\n        self.assertFalse(enabled())\n\n        self.__select_variable(\"gender\")\n        self.__select_group(\"chest pain\")\n        self.assertTrue(enabled())\n\n    def test_value_all_missing_for_group(self):\n        \"\"\"\n        This is one of the extreme cases when we have a subgroup value\n        where all values in selected variable are missing. Box plot should\n        handle this.\n        \"\"\"\n        data = Table.from_list(\n            Domain([DiscreteVariable(\"a\", values=[\"v1\", \"v2\", \"v3\"]),\n                    DiscreteVariable(\"b\", values=[\"v3\", \"v4\"])]),\n            [[0., 0.],\n             [0., 1.],\n             [1., np.nan],\n             [1., np.nan],\n             [2., 0.],\n             [2., 0.]])\n        self.send_signal(self.widget.Inputs.data, data)\n\n        self.__select_variable(\"b\")\n        self.__select_group(\"a\")\n        self.assertTupleEqual(self.widget.conts.shape, (3, 2))\n\n\nclass TestUtils(unittest.TestCase):\n    def test(self):\n        np.testing.assert_array_equal(\n            _quantiles(range(1, 8 + 1), [1.] * 8, [0.0, 0.25, 0.5, 0.75, 1.0]),\n            [1., 2.5, 4.5, 6.5, 8.]\n        )\n        np.testing.assert_array_equal(\n            _quantiles(range(1, 8 + 1), [1.] * 8, [0.0, 0.25, 0.5, 0.75, 1.0]),\n            [1., 2.5, 4.5, 6.5, 8.]\n        )\n        np.testing.assert_array_equal(\n            _quantiles(range(1, 4 + 1), [1., 2., 1., 2],\n                       [0.0, 0.25, 0.5, 0.75, 1.0]),\n            [1.0, 2.0, 2.5, 4.0, 4.0]\n        )\n        np.testing.assert_array_equal(\n            _quantiles(range(1, 4 + 1), [2., 1., 1., 2.],\n                       [0.0, 0.25, 0.5, 0.75, 1.0]),\n            [1.0, 1.0, 2.5, 4.0, 4.0]\n        )\n        np.testing.assert_array_equal(\n            _quantiles(range(1, 4 + 1), [1., 1., 1., 1.],\n                       [0.0, 0.25, 0.5, 0.75, 1.0]),\n            [1.0, 1.5, 2.5, 3.5, 4.0]\n        )\n        np.testing.assert_array_equal(\n            _quantiles(range(1, 4 + 1), [1., 1., 1., 1.],\n                       [0.0, 0.25, 0.5, 0.75, 1.0], interpolation=\"higher\"),\n            [1, 2, 3, 4, 4]\n        )\n        np.testing.assert_array_equal(\n            _quantiles(range(1, 4 + 1), [1., 1., 1., 1.],\n                       [0.0, 0.25, 0.5, 0.75, 1.0], interpolation=\"lower\"),\n            [1, 1, 2, 3, 4]\n        )\n",
            "new_file_content": "# Test methods with long descriptive names can omit docstrings\n# pylint: disable=missing-docstring\nimport unittest\nfrom unittest.mock import patch\n\nimport numpy as np\nfrom AnyQt.QtCore import QItemSelectionModel\n\nfrom Orange.data import Table, ContinuousVariable, StringVariable, Domain, \\\n    DiscreteVariable\nfrom Orange.widgets.visualize.owboxplot import (\n    OWBoxPlot, FilterGraphicsRectItem, _quantiles\n)\nfrom Orange.widgets.tests.base import WidgetTest, WidgetOutputsTestMixin\nfrom Orange.tests import test_filename\n\n\nclass TestOWBoxPlot(WidgetTest, WidgetOutputsTestMixin):\n    @classmethod\n    def setUpClass(cls):\n        super().setUpClass()\n        WidgetOutputsTestMixin.init(cls)\n\n        cls.iris = Table(\"iris\")\n        cls.zoo = Table(\"zoo\")\n        cls.housing = Table(\"housing\")\n        cls.titanic = Table(\"titanic\")\n        cls.heart = Table(\"heart_disease\")\n        cls.data = cls.iris\n        cls.signal_name = \"Data\"\n        cls.signal_data = cls.data\n\n    def setUp(self):\n        self.widget = self.create_widget(OWBoxPlot)\n\n    def test_input_data(self):\n        \"\"\"Check widget's data\"\"\"\n        self.send_signal(self.widget.Inputs.data, self.iris)\n        self.assertEqual(len(self.widget.attrs), 5)\n        self.assertEqual(len(self.widget.group_vars), 2)\n        self.assertFalse(self.widget.display_box.isHidden())\n        self.assertTrue(self.widget.stretching_box.isHidden())\n\n        self.send_signal(self.widget.Inputs.data, None)\n        self.assertEqual(len(self.widget.attrs), 0)\n        self.assertEqual(len(self.widget.group_vars), 1)\n        self.assertTrue(self.widget.display_box.isHidden())\n        self.assertFalse(self.widget.stretching_box.isHidden())\n\n    def test_dont_show_hidden_attrs(self):\n        \"\"\"Check widget's data\"\"\"\n        iris = Table(\"iris\")\n        iris.domain[\"iris\"].attributes[\"hidden\"] = True\n        iris.domain[\"petal length\"].attributes[\"hidden\"] = True\n        self.send_signal(self.widget.Inputs.data, iris)\n        self.assertEqual(len(self.widget.attrs), 3)\n        self.assertEqual(len(self.widget.group_vars), 1)\n\n    def test_primitive_metas(self):\n        new_domain = Domain(attributes=[], class_vars=[], metas=(\n            self.data.domain.attributes + self.data.domain.class_vars))\n        attrs_as_metas = self.data.transform(new_domain)\n        self.send_signal(self.widget.Inputs.data, attrs_as_metas)\n        self.assertTrue(self.widget.display_box.isEnabled())\n\n    def test_input_data_missings_cont_group_var(self):\n        \"\"\"Check widget with continuous data with missing values and group variable\"\"\"\n        data = self.iris.copy()\n        data.X[:, 0] = np.nan\n        self.send_signal(self.widget.Inputs.data, data)\n        # used to crash, see #1568\n\n    def test_input_data_missings_cont_no_group_var(self):\n        \"\"\"Check widget with continuous data with missing values and no group variable\"\"\"\n        data = self.housing\n        data.X[:, 0] = np.nan\n        self.send_signal(self.widget.Inputs.data, data)\n        # used to crash, see #1568\n\n    def test_input_data_missings_disc_group_var(self):\n        \"\"\"Check widget with discrete data with missing values and group variable\"\"\"\n        data = self.zoo\n        data.X[:, 1] = np.nan\n        data.domain.attributes[1].values = []\n        self.send_signal(\"Data\", data)\n        self.widget.controls.order_by_importance.setChecked(True)\n        self._select_list_items(self.widget.controls.attribute)\n        self._select_list_items(self.widget.controls.group_var)\n\n    def test_input_data_missings_disc_no_group_var(self):\n        \"\"\"Check widget discrete data with missing values and no group variable\"\"\"\n        data = self.zoo\n        data.domain.class_var = ContinuousVariable(\"cls\")\n        data.X[:, 1] = np.nan\n        data.domain.attributes[1].values = []\n        self.send_signal(\"Data\", data)\n        self._select_list_items(self.widget.controls.attribute)\n        self._select_list_items(self.widget.controls.group_var)\n\n    def test_attribute_combinations(self):\n        self.send_signal(self.widget.Inputs.data, self.heart)\n        group_list = self.widget.controls.group_var\n        m = group_list.selectionModel()\n        for i in range(len(group_list.model())):\n            m.setCurrentIndex(group_list.model().index(i), m.ClearAndSelect)\n            self._select_list_items(self.widget.controls.attribute)\n\n    def test_apply_sorting_group(self):\n        controls = self.widget.controls\n        group_list = controls.group_var\n        order_check = controls.order_by_importance\n        attributes = self.widget.attrs\n\n        def select_group(i):\n            group_selection = group_list.selectionModel()\n            group_selection.setCurrentIndex(\n                group_list.model().index(i),\n                group_selection.ClearAndSelect)\n\n        data = self.titanic\n        self.send_signal(\"Data\", data)\n\n        select_group(2)  # First attribute\n\n        order_check.setChecked(False)\n        self.assertEqual(tuple(attributes),\n                         data.domain.class_vars + data.domain.attributes)\n        order_check.setChecked(True)\n        self.assertEqual([x.name for x in attributes],\n                         ['sex', 'survived', 'age', 'status'])\n        select_group(1)  # Class\n        self.assertEqual([x.name for x in attributes],\n                         ['sex', 'status', 'age', 'survived'])\n\n        data = self.heart\n        self.send_signal(\"Data\", data)\n        select_group(1)  # Class\n        order_check.setChecked(True)\n        self.assertEqual([x.name for x in attributes],\n                         ['thal',\n                          'chest pain',\n                          'major vessels colored',\n                          'ST by exercise',\n                          'max HR',\n                          'exerc ind ang',\n                          'slope peak exc ST',\n                          'gender',\n                          'age',\n                          'rest ECG',\n                          'rest SBP',\n                          'cholesterol',\n                          'fasting blood sugar > 120',\n                          'diameter narrowing'])\n\n    def test_apply_sorting_vars(self):\n        controls = self.widget.controls\n        attr_list = self.widget.attrs\n        order_check = controls.order_grouping_by_importance\n        groups = self.widget.group_vars\n\n        def select_attr(i):\n            attr_selection = controls.attribute.selectionModel()\n            attr_selection.setCurrentIndex(\n                attr_list.index(i),\n                attr_selection.ClearAndSelect)\n\n        data = self.titanic\n        self.send_signal(\"Data\", data)\n\n        select_attr(1)  # First attribute\n\n        order_check.setChecked(False)\n        self.assertEqual(\n            tuple(groups),\n            (None, ) + data.domain.class_vars + data.domain.attributes)\n        order_check.setChecked(True)\n        self.assertIsNone(groups[0])\n        self.assertEqual([x.name for x in groups[1:]],\n                         ['sex', 'survived', 'age', 'status'])\n        select_attr(0)  # Class\n        self.assertIsNone(groups[0])\n        self.assertEqual([x.name for x in groups[1:]],\n                         ['sex', 'status', 'age', 'survived'])\n\n        data = self.heart\n        self.send_signal(\"Data\", data)\n        select_attr(0)  # Class\n        self.assertIsNone(groups[0])\n        self.assertEqual([x.name for x in groups[1:]],\n                         ['thal',\n                          'chest pain',\n                          'exerc ind ang',\n                          'slope peak exc ST',\n                          'gender',\n                          'rest ECG',\n                          'fasting blood sugar > 120',\n                          'diameter narrowing'])\n\n    def test_box_order_when_missing_stats(self):\n        self.widget.compare = 1\n        # The widget can't do anything smart here, but shouldn't crash\n        self.send_signal(self.widget.Inputs.data, self.iris[49:51])\n\n    def test_saved_selection(self):\n        self.send_signal(self.widget.Inputs.data, self.data)\n        selected_indices = self._select_data()\n        self.send_signal(self.widget.Inputs.data, self.zoo)\n        self.assertIsNone(self.get_output(self.widget.Outputs.selected_data))\n        self.send_signal(self.widget.Inputs.data, self.data)\n        np.testing.assert_array_equal(self.get_output(self.widget.Outputs.selected_data).X,\n                                      self.data.X[selected_indices])\n\n    def test_continuous_metas(self):\n        domain = self.iris.domain\n        metas = domain.attributes[:-1] + (StringVariable(\"str\"),)\n        domain = Domain([], domain.class_var, metas)\n        data = Table.from_table(domain, self.iris)\n        self.send_signal(self.widget.Inputs.data, data)\n\n    def test_label_overlap(self):\n        self.send_signal(self.widget.Inputs.data, self.heart)\n        self.widget.stretched = False\n        self.__select_variable(\"chest pain\")\n        self.__select_group(\"gender\")\n        self.widget.adjustSize()\n        self.widget.layout().activate()\n        self.widget.grab()  # ensure that the painting code is run\n\n    def test_empty_groups(self):\n        \"\"\"Test if groups with zero elements are not shown\"\"\"\n        table = Table(test_filename(\"datasets/cyber-security-breaches.tab\"))\n        self.send_signal(self.widget.Inputs.data, table)\n        self.__select_variable(\"US State\")\n        self.__select_group(\"US State\")\n        self.assertEqual(52, len(self.widget.boxes))\n\n        # select rows with US State equal to TX or MO\n        use_indexes = np.array([0, 1, 25, 26, 27])\n        table.X = table.X[use_indexes]\n        self.send_signal(self.widget.Inputs.data, table)\n        self.assertEqual(2, len(self.widget.boxes))\n\n    def test_sorting_disc_group_var(self):\n        \"\"\"Test if subgroups are sorted by their size\"\"\"\n        table = Table(\"heart_disease\")\n        self.send_signal(self.widget.Inputs.data, table)\n        self.__select_variable(\"gender\")\n        self.__select_group(\"chest pain\")\n\n        # checkbox not checked - preserve original order of selected grouping attribute\n        self.assertListEqual(self.widget.order, [0, 1, 2, 3])\n\n        # checkbox checked - sort by frequencies\n        self.widget.controls.sort_freqs.setChecked(True)\n        self.assertListEqual(self.widget.order, [0, 2, 1, 3])\n\n    def _select_data(self):\n        items = [item for item in self.widget.box_scene.items()\n                 if isinstance(item, FilterGraphicsRectItem)]\n        items[0].setSelected(True)\n        return [100, 103, 104, 108, 110, 111, 112, 115, 116,\n                120, 123, 124, 126, 128, 132, 133, 136, 137,\n                139, 140, 141, 143, 144, 145, 146, 147, 148]\n\n    def _select_list_items(self, _list):\n        model = _list.selectionModel()\n        for i in range(len(_list.model())):\n            model.setCurrentIndex(_list.model().index(i), model.ClearAndSelect)\n\n    def __select_variable(self, name, widget=None):\n        if widget is None:\n            widget = self.widget\n\n        self.__select_value(widget.controls.attribute, name)\n\n    def __select_group(self, name, widget=None):\n        if widget is None:\n            widget = self.widget\n\n        self.__select_value(widget.controls.group_var, name)\n\n    def __select_value(self, list, value):\n        m = list.model()\n        for i in range(m.rowCount()):\n            idx = m.index(i)\n            if m.data(idx) == value:\n                list.selectionModel().setCurrentIndex(\n                    idx, QItemSelectionModel.ClearAndSelect)\n\n    def test_unconditional_commit_on_new_signal(self):\n        with patch.object(self.widget, 'commit') as apply:\n            apply.reset_mock()\n            self.send_signal(self.widget.Inputs.data, self.zoo)\n            apply.assert_called()\n\n    def test_stretching(self):\n        self.send_signal(self.widget.Inputs.data, self.heart)\n        enabled = self.widget.controls.stretched.isEnabled\n\n        self.__select_variable(\"chest pain\")\n        self.__select_group(\"gender\")\n        self.assertTrue(enabled())\n\n        self.__select_variable(\"gender\")\n        self.__select_group(\"gender\")\n        self.assertFalse(enabled())\n\n        self.__select_variable(\"gender\")\n        self.__select_group(\"chest pain\")\n        self.assertTrue(enabled())\n\n    def test_value_all_missing_for_group(self):\n        \"\"\"\n        This is one of the extreme cases when we have a subgroup value\n        where all values in selected variable are missing. Box plot should\n        handle this.\n        \"\"\"\n        data = Table.from_list(\n            Domain([DiscreteVariable(\"a\", values=[\"v1\", \"v2\", \"v3\"]),\n                    DiscreteVariable(\"b\", values=[\"v3\", \"v4\"])]),\n            [[0., 0.],\n             [0., 1.],\n             [1., np.nan],\n             [1., np.nan],\n             [2., 0.],\n             [2., 0.]])\n        self.send_signal(self.widget.Inputs.data, data)\n\n        self.__select_variable(\"b\")\n        self.__select_group(\"a\")\n        self.assertTupleEqual(self.widget.conts.shape, (3, 2))\n\n\nclass TestUtils(unittest.TestCase):\n    def test(self):\n        np.testing.assert_array_equal(\n            _quantiles(range(1, 8 + 1), [1.] * 8, [0.0, 0.25, 0.5, 0.75, 1.0]),\n            [1., 2.5, 4.5, 6.5, 8.]\n        )\n        np.testing.assert_array_equal(\n            _quantiles(range(1, 8 + 1), [1.] * 8, [0.0, 0.25, 0.5, 0.75, 1.0]),\n            [1., 2.5, 4.5, 6.5, 8.]\n        )\n        np.testing.assert_array_equal(\n            _quantiles(range(1, 4 + 1), [1., 2., 1., 2],\n                       [0.0, 0.25, 0.5, 0.75, 1.0]),\n            [1.0, 2.0, 2.5, 4.0, 4.0]\n        )\n        np.testing.assert_array_equal(\n            _quantiles(range(1, 4 + 1), [2., 1., 1., 2.],\n                       [0.0, 0.25, 0.5, 0.75, 1.0]),\n            [1.0, 1.0, 2.5, 4.0, 4.0]\n        )\n        np.testing.assert_array_equal(\n            _quantiles(range(1, 4 + 1), [1., 1., 1., 1.],\n                       [0.0, 0.25, 0.5, 0.75, 1.0]),\n            [1.0, 1.5, 2.5, 3.5, 4.0]\n        )\n        np.testing.assert_array_equal(\n            _quantiles(range(1, 4 + 1), [1., 1., 1., 1.],\n                       [0.0, 0.25, 0.5, 0.75, 1.0], interpolation=\"higher\"),\n            [1, 2, 3, 4, 4]\n        )\n        np.testing.assert_array_equal(\n            _quantiles(range(1, 4 + 1), [1., 1., 1., 1.],\n                       [0.0, 0.25, 0.5, 0.75, 1.0], interpolation=\"lower\"),\n            [1, 1, 2, 3, 4]\n        )\n",
            "header": {
                "file": {
                    "path": "Orange/widgets/visualize/tests/test_owboxplot.py"
                },
                "misc_line": null
            },
            "index_line": {
                "old_commit_hash": "078294be8",
                "new_commit_hash": "18bb8986c",
                "mode": "100644"
            },
            "is_binary_file": false,
            "binary_line": null,
            "minus_file": {
                "path": "a/Orange/widgets/visualize/tests/test_owboxplot.py"
            },
            "plus_file": {
                "path": "b/Orange/widgets/visualize/tests/test_owboxplot.py"
            },
            "hunks": [
                {
                    "descriptor": {
                        "old_range": {
                            "start": 47,
                            "length": 6
                        },
                        "new_range": {
                            "start": 47,
                            "length": 15
                        },
                        "section": "class TestOWBoxPlot(WidgetTest, WidgetOutputsTestMixin):"
                    },
                    "line_group": {
                        "all_lines": [
                            {
                                "content": "        self.assertTrue(self.widget.display_box.isHidden())",
                                "type": "context"
                            },
                            {
                                "content": "        self.assertFalse(self.widget.stretching_box.isHidden())",
                                "type": "context"
                            },
                            {
                                "content": "",
                                "type": "context"
                            },
                            {
                                "content": "    def test_dont_show_hidden_attrs(self):",
                                "type": "added"
                            },
                            {
                                "content": "        \"\"\"Check widget's data\"\"\"",
                                "type": "added"
                            },
                            {
                                "content": "        iris = Table(\"iris\")",
                                "type": "added"
                            },
                            {
                                "content": "        iris.domain[\"iris\"].attributes[\"hidden\"] = True",
                                "type": "added"
                            },
                            {
                                "content": "        iris.domain[\"petal length\"].attributes[\"hidden\"] = True",
                                "type": "added"
                            },
                            {
                                "content": "        self.send_signal(self.widget.Inputs.data, iris)",
                                "type": "added"
                            },
                            {
                                "content": "        self.assertEqual(len(self.widget.attrs), 3)",
                                "type": "added"
                            },
                            {
                                "content": "        self.assertEqual(len(self.widget.group_vars), 1)",
                                "type": "added"
                            },
                            {
                                "content": "",
                                "type": "added"
                            },
                            {
                                "content": "    def test_primitive_metas(self):",
                                "type": "context"
                            },
                            {
                                "content": "        new_domain = Domain(attributes=[], class_vars=[], metas=(",
                                "type": "context"
                            },
                            {
                                "content": "            self.data.domain.attributes + self.data.domain.class_vars))",
                                "type": "context"
                            },
                            {
                                "content": "",
                                "type": "context"
                            }
                        ]
                    },
                    "modified_entities": [
                        {
                            "file_name": "Orange/widgets/visualize/tests/test_owboxplot.py",
                            "type": "class",
                            "name": "TestOWBoxPlot",
                            "content": "class TestOWBoxPlot(WidgetTest, WidgetOutputsTestMixin):\n    @classmethod\n    def setUpClass(cls):\n        super().setUpClass()\n        WidgetOutputsTestMixin.init(cls)\n\n        cls.iris = Table(\"iris\")\n        cls.zoo = Table(\"zoo\")\n        cls.housing = Table(\"housing\")\n        cls.titanic = Table(\"titanic\")\n        cls.heart = Table(\"heart_disease\")\n        cls.data = cls.iris\n        cls.signal_name = \"Data\"\n        cls.signal_data = cls.data\n\n    def setUp(self):\n        self.widget = self.create_widget(OWBoxPlot)\n\n    def test_input_data(self):\n        \"\"\"Check widget's data\"\"\"\n        self.send_signal(self.widget.Inputs.data, self.iris)\n        self.assertEqual(len(self.widget.attrs), 5)\n        self.assertEqual(len(self.widget.group_vars), 2)\n        self.assertFalse(self.widget.display_box.isHidden())\n        self.assertTrue(self.widget.stretching_box.isHidden())\n\n        self.send_signal(self.widget.Inputs.data, None)\n        self.assertEqual(len(self.widget.attrs), 0)\n        self.assertEqual(len(self.widget.group_vars), 1)\n        self.assertTrue(self.widget.display_box.isHidden())\n        self.assertFalse(self.widget.stretching_box.isHidden())\n\n    def test_dont_show_hidden_attrs(self):\n        \"\"\"Check widget's data\"\"\"\n        iris = Table(\"iris\")\n        iris.domain[\"iris\"].attributes[\"hidden\"] = True\n        iris.domain[\"petal length\"].attributes[\"hidden\"] = True\n        self.send_signal(self.widget.Inputs.data, iris)\n        self.assertEqual(len(self.widget.attrs), 3)\n        self.assertEqual(len(self.widget.group_vars), 1)\n\n    def test_primitive_metas(self):\n        new_domain = Domain(attributes=[], class_vars=[], metas=(\n            self.data.domain.attributes + self.data.domain.class_vars))\n        attrs_as_metas = self.data.transform(new_domain)\n        self.send_signal(self.widget.Inputs.data, attrs_as_metas)\n        self.assertTrue(self.widget.display_box.isEnabled())\n\n    def test_input_data_missings_cont_group_var(self):\n        \"\"\"Check widget with continuous data with missing values and group variable\"\"\"\n        data = self.iris.copy()\n        data.X[:, 0] = np.nan\n        self.send_signal(self.widget.Inputs.data, data)\n        # used to crash, see #1568\n\n    def test_input_data_missings_cont_no_group_var(self):\n        \"\"\"Check widget with continuous data with missing values and no group variable\"\"\"\n        data = self.housing\n        data.X[:, 0] = np.nan\n        self.send_signal(self.widget.Inputs.data, data)\n        # used to crash, see #1568\n\n    def test_input_data_missings_disc_group_var(self):\n        \"\"\"Check widget with discrete data with missing values and group variable\"\"\"\n        data = self.zoo\n        data.X[:, 1] = np.nan\n        data.domain.attributes[1].values = []\n        self.send_signal(\"Data\", data)\n        self.widget.controls.order_by_importance.setChecked(True)\n        self._select_list_items(self.widget.controls.attribute)\n        self._select_list_items(self.widget.controls.group_var)\n\n    def test_input_data_missings_disc_no_group_var(self):\n        \"\"\"Check widget discrete data with missing values and no group variable\"\"\"\n        data = self.zoo\n        data.domain.class_var = ContinuousVariable(\"cls\")\n        data.X[:, 1] = np.nan\n        data.domain.attributes[1].values = []\n        self.send_signal(\"Data\", data)\n        self._select_list_items(self.widget.controls.attribute)\n        self._select_list_items(self.widget.controls.group_var)\n\n    def test_attribute_combinations(self):\n        self.send_signal(self.widget.Inputs.data, self.heart)\n        group_list = self.widget.controls.group_var\n        m = group_list.selectionModel()\n        for i in range(len(group_list.model())):\n            m.setCurrentIndex(group_list.model().index(i), m.ClearAndSelect)\n            self._select_list_items(self.widget.controls.attribute)\n\n    def test_apply_sorting_group(self):\n        controls = self.widget.controls\n        group_list = controls.group_var\n        order_check = controls.order_by_importance\n        attributes = self.widget.attrs\n\n        def select_group(i):\n            group_selection = group_list.selectionModel()\n            group_selection.setCurrentIndex(\n                group_list.model().index(i),\n                group_selection.ClearAndSelect)\n\n        data = self.titanic\n        self.send_signal(\"Data\", data)\n\n        select_group(2)  # First attribute\n\n        order_check.setChecked(False)\n        self.assertEqual(tuple(attributes),\n                         data.domain.class_vars + data.domain.attributes)\n        order_check.setChecked(True)\n        self.assertEqual([x.name for x in attributes],\n                         ['sex', 'survived', 'age', 'status'])\n        select_group(1)  # Class\n        self.assertEqual([x.name for x in attributes],\n                         ['sex', 'status', 'age', 'survived'])\n\n        data = self.heart\n        self.send_signal(\"Data\", data)\n        select_group(1)  # Class\n        order_check.setChecked(True)\n        self.assertEqual([x.name for x in attributes],\n                         ['thal',\n                          'chest pain',\n                          'major vessels colored',\n                          'ST by exercise',\n                          'max HR',\n                          'exerc ind ang',\n                          'slope peak exc ST',\n                          'gender',\n                          'age',\n                          'rest ECG',\n                          'rest SBP',\n                          'cholesterol',\n                          'fasting blood sugar > 120',\n                          'diameter narrowing'])\n\n    def test_apply_sorting_vars(self):\n        controls = self.widget.controls\n        attr_list = self.widget.attrs\n        order_check = controls.order_grouping_by_importance\n        groups = self.widget.group_vars\n\n        def select_attr(i):\n            attr_selection = controls.attribute.selectionModel()\n            attr_selection.setCurrentIndex(\n                attr_list.index(i),\n                attr_selection.ClearAndSelect)\n\n        data = self.titanic\n        self.send_signal(\"Data\", data)\n\n        select_attr(1)  # First attribute\n\n        order_check.setChecked(False)\n        self.assertEqual(\n            tuple(groups),\n            (None, ) + data.domain.class_vars + data.domain.attributes)\n        order_check.setChecked(True)\n        self.assertIsNone(groups[0])\n        self.assertEqual([x.name for x in groups[1:]],\n                         ['sex', 'survived', 'age', 'status'])\n        select_attr(0)  # Class\n        self.assertIsNone(groups[0])\n        self.assertEqual([x.name for x in groups[1:]],\n                         ['sex', 'status', 'age', 'survived'])\n\n        data = self.heart\n        self.send_signal(\"Data\", data)\n        select_attr(0)  # Class\n        self.assertIsNone(groups[0])\n        self.assertEqual([x.name for x in groups[1:]],\n                         ['thal',\n                          'chest pain',\n                          'exerc ind ang',\n                          'slope peak exc ST',\n                          'gender',\n                          'rest ECG',\n                          'fasting blood sugar > 120',\n                          'diameter narrowing'])\n\n    def test_box_order_when_missing_stats(self):\n        self.widget.compare = 1\n        # The widget can't do anything smart here, but shouldn't crash\n        self.send_signal(self.widget.Inputs.data, self.iris[49:51])\n\n    def test_saved_selection(self):\n        self.send_signal(self.widget.Inputs.data, self.data)\n        selected_indices = self._select_data()\n        self.send_signal(self.widget.Inputs.data, self.zoo)\n        self.assertIsNone(self.get_output(self.widget.Outputs.selected_data))\n        self.send_signal(self.widget.Inputs.data, self.data)\n        np.testing.assert_array_equal(self.get_output(self.widget.Outputs.selected_data).X,\n                                      self.data.X[selected_indices])\n\n    def test_continuous_metas(self):\n        domain = self.iris.domain\n        metas = domain.attributes[:-1] + (StringVariable(\"str\"),)\n        domain = Domain([], domain.class_var, metas)\n        data = Table.from_table(domain, self.iris)\n        self.send_signal(self.widget.Inputs.data, data)\n\n    def test_label_overlap(self):\n        self.send_signal(self.widget.Inputs.data, self.heart)\n        self.widget.stretched = False\n        self.__select_variable(\"chest pain\")\n        self.__select_group(\"gender\")\n        self.widget.adjustSize()\n        self.widget.layout().activate()\n        self.widget.grab()  # ensure that the painting code is run\n\n    def test_empty_groups(self):\n        \"\"\"Test if groups with zero elements are not shown\"\"\"\n        table = Table(test_filename(\"datasets/cyber-security-breaches.tab\"))\n        self.send_signal(self.widget.Inputs.data, table)\n        self.__select_variable(\"US State\")\n        self.__select_group(\"US State\")\n        self.assertEqual(52, len(self.widget.boxes))\n\n        # select rows with US State equal to TX or MO\n        use_indexes = np.array([0, 1, 25, 26, 27])\n        table.X = table.X[use_indexes]\n        self.send_signal(self.widget.Inputs.data, table)\n        self.assertEqual(2, len(self.widget.boxes))\n\n    def test_sorting_disc_group_var(self):\n        \"\"\"Test if subgroups are sorted by their size\"\"\"\n        table = Table(\"heart_disease\")\n        self.send_signal(self.widget.Inputs.data, table)\n        self.__select_variable(\"gender\")\n        self.__select_group(\"chest pain\")\n\n        # checkbox not checked - preserve original order of selected grouping attribute\n        self.assertListEqual(self.widget.order, [0, 1, 2, 3])\n\n        # checkbox checked - sort by frequencies\n        self.widget.controls.sort_freqs.setChecked(True)\n        self.assertListEqual(self.widget.order, [0, 2, 1, 3])\n\n    def _select_data(self):\n        items = [item for item in self.widget.box_scene.items()\n                 if isinstance(item, FilterGraphicsRectItem)]\n        items[0].setSelected(True)\n        return [100, 103, 104, 108, 110, 111, 112, 115, 116,\n                120, 123, 124, 126, 128, 132, 133, 136, 137,\n                139, 140, 141, 143, 144, 145, 146, 147, 148]\n\n    def _select_list_items(self, _list):\n        model = _list.selectionModel()\n        for i in range(len(_list.model())):\n            model.setCurrentIndex(_list.model().index(i), model.ClearAndSelect)\n\n    def __select_variable(self, name, widget=None):\n        if widget is None:\n            widget = self.widget\n\n        self.__select_value(widget.controls.attribute, name)\n\n    def __select_group(self, name, widget=None):\n        if widget is None:\n            widget = self.widget\n\n        self.__select_value(widget.controls.group_var, name)\n\n    def __select_value(self, list, value):\n        m = list.model()\n        for i in range(m.rowCount()):\n            idx = m.index(i)\n            if m.data(idx) == value:\n                list.selectionModel().setCurrentIndex(\n                    idx, QItemSelectionModel.ClearAndSelect)\n\n    def test_unconditional_commit_on_new_signal(self):\n        with patch.object(self.widget, 'commit') as apply:\n            apply.reset_mock()\n            self.send_signal(self.widget.Inputs.data, self.zoo)\n            apply.assert_called()\n\n    def test_stretching(self):\n        self.send_signal(self.widget.Inputs.data, self.heart)\n        enabled = self.widget.controls.stretched.isEnabled\n\n        self.__select_variable(\"chest pain\")\n        self.__select_group(\"gender\")\n        self.assertTrue(enabled())\n\n        self.__select_variable(\"gender\")\n        self.__select_group(\"gender\")\n        self.assertFalse(enabled())\n\n        self.__select_variable(\"gender\")\n        self.__select_group(\"chest pain\")\n        self.assertTrue(enabled())\n\n    def test_value_all_missing_for_group(self):\n        \"\"\"\n        This is one of the extreme cases when we have a subgroup value\n        where all values in selected variable are missing. Box plot should\n        handle this.\n        \"\"\"\n        data = Table.from_list(\n            Domain([DiscreteVariable(\"a\", values=[\"v1\", \"v2\", \"v3\"]),\n                    DiscreteVariable(\"b\", values=[\"v3\", \"v4\"])]),\n            [[0., 0.],\n             [0., 1.],\n             [1., np.nan],\n             [1., np.nan],\n             [2., 0.],\n             [2., 0.]])\n        self.send_signal(self.widget.Inputs.data, data)\n\n        self.__select_variable(\"b\")\n        self.__select_group(\"a\")\n        self.assertTupleEqual(self.widget.conts.shape, (3, 2))",
                            "ast_type_str": "ClassDef",
                            "start_lineno": 18,
                            "end_lineno": 331,
                            "parent": null
                        }
                    ],
                    "added_entities": [
                        {
                            "file_name": "Orange/widgets/visualize/tests/test_owboxplot.py",
                            "type": "method",
                            "name": "TestOWBoxPlot.test_dont_show_hidden_attrs",
                            "content": "class TestOWBoxPlot(WidgetTest, WidgetOutputsTestMixin):\n    @classmethod\n    def setUpClass(cls):\n        super().setUpClass()\n        WidgetOutputsTestMixin.init(cls)\n\n        cls.iris = Table(\"iris\")\n        cls.zoo = Table(\"zoo\")\n        cls.housing = Table(\"housing\")\n        cls.titanic = Table(\"titanic\")\n        cls.heart = Table(\"heart_disease\")\n        cls.data = cls.iris\n        cls.signal_name = \"Data\"\n        cls.signal_data = cls.data\n\n    def setUp(self):\n        self.widget = self.create_widget(OWBoxPlot)\n\n    def test_input_data(self):\n        \"\"\"Check widget's data\"\"\"\n        self.send_signal(self.widget.Inputs.data, self.iris)\n        self.assertEqual(len(self.widget.attrs), 5)\n        self.assertEqual(len(self.widget.group_vars), 2)\n        self.assertFalse(self.widget.display_box.isHidden())\n        self.assertTrue(self.widget.stretching_box.isHidden())\n\n        self.send_signal(self.widget.Inputs.data, None)\n        self.assertEqual(len(self.widget.attrs), 0)\n        self.assertEqual(len(self.widget.group_vars), 1)\n        self.assertTrue(self.widget.display_box.isHidden())\n        self.assertFalse(self.widget.stretching_box.isHidden())\n\n    def test_dont_show_hidden_attrs(self):\n        \"\"\"Check widget's data\"\"\"\n        iris = Table(\"iris\")\n        iris.domain[\"iris\"].attributes[\"hidden\"] = True\n        iris.domain[\"petal length\"].attributes[\"hidden\"] = True\n        self.send_signal(self.widget.Inputs.data, iris)\n        self.assertEqual(len(self.widget.attrs), 3)\n        self.assertEqual(len(self.widget.group_vars), 1)\n\n    def test_primitive_metas(self):\n        new_domain = Domain(attributes=[], class_vars=[], metas=(\n            self.data.domain.attributes + self.data.domain.class_vars))\n        attrs_as_metas = self.data.transform(new_domain)\n        self.send_signal(self.widget.Inputs.data, attrs_as_metas)\n        self.assertTrue(self.widget.display_box.isEnabled())\n\n    def test_input_data_missings_cont_group_var(self):\n        \"\"\"Check widget with continuous data with missing values and group variable\"\"\"\n        data = self.iris.copy()\n        data.X[:, 0] = np.nan\n        self.send_signal(self.widget.Inputs.data, data)\n        # used to crash, see #1568\n\n    def test_input_data_missings_cont_no_group_var(self):\n        \"\"\"Check widget with continuous data with missing values and no group variable\"\"\"\n        data = self.housing\n        data.X[:, 0] = np.nan\n        self.send_signal(self.widget.Inputs.data, data)\n        # used to crash, see #1568\n\n    def test_input_data_missings_disc_group_var(self):\n        \"\"\"Check widget with discrete data with missing values and group variable\"\"\"\n        data = self.zoo\n        data.X[:, 1] = np.nan\n        data.domain.attributes[1].values = []\n        self.send_signal(\"Data\", data)\n        self.widget.controls.order_by_importance.setChecked(True)\n        self._select_list_items(self.widget.controls.attribute)\n        self._select_list_items(self.widget.controls.group_var)\n\n    def test_input_data_missings_disc_no_group_var(self):\n        \"\"\"Check widget discrete data with missing values and no group variable\"\"\"\n        data = self.zoo\n        data.domain.class_var = ContinuousVariable(\"cls\")\n        data.X[:, 1] = np.nan\n        data.domain.attributes[1].values = []\n        self.send_signal(\"Data\", data)\n        self._select_list_items(self.widget.controls.attribute)\n        self._select_list_items(self.widget.controls.group_var)\n\n    def test_attribute_combinations(self):\n        self.send_signal(self.widget.Inputs.data, self.heart)\n        group_list = self.widget.controls.group_var\n        m = group_list.selectionModel()\n        for i in range(len(group_list.model())):\n            m.setCurrentIndex(group_list.model().index(i), m.ClearAndSelect)\n            self._select_list_items(self.widget.controls.attribute)\n\n    def test_apply_sorting_group(self):\n        controls = self.widget.controls\n        group_list = controls.group_var\n        order_check = controls.order_by_importance\n        attributes = self.widget.attrs\n\n        def select_group(i):\n            group_selection = group_list.selectionModel()\n            group_selection.setCurrentIndex(\n                group_list.model().index(i),\n                group_selection.ClearAndSelect)\n\n        data = self.titanic\n        self.send_signal(\"Data\", data)\n\n        select_group(2)  # First attribute\n\n        order_check.setChecked(False)\n        self.assertEqual(tuple(attributes),\n                         data.domain.class_vars + data.domain.attributes)\n        order_check.setChecked(True)\n        self.assertEqual([x.name for x in attributes],\n                         ['sex', 'survived', 'age', 'status'])\n        select_group(1)  # Class\n        self.assertEqual([x.name for x in attributes],\n                         ['sex', 'status', 'age', 'survived'])\n\n        data = self.heart\n        self.send_signal(\"Data\", data)\n        select_group(1)  # Class\n        order_check.setChecked(True)\n        self.assertEqual([x.name for x in attributes],\n                         ['thal',\n                          'chest pain',\n                          'major vessels colored',\n                          'ST by exercise',\n                          'max HR',\n                          'exerc ind ang',\n                          'slope peak exc ST',\n                          'gender',\n                          'age',\n                          'rest ECG',\n                          'rest SBP',\n                          'cholesterol',\n                          'fasting blood sugar > 120',\n                          'diameter narrowing'])\n\n    def test_apply_sorting_vars(self):\n        controls = self.widget.controls\n        attr_list = self.widget.attrs\n        order_check = controls.order_grouping_by_importance\n        groups = self.widget.group_vars\n\n        def select_attr(i):\n            attr_selection = controls.attribute.selectionModel()\n            attr_selection.setCurrentIndex(\n                attr_list.index(i),\n                attr_selection.ClearAndSelect)\n\n        data = self.titanic\n        self.send_signal(\"Data\", data)\n\n        select_attr(1)  # First attribute\n\n        order_check.setChecked(False)\n        self.assertEqual(\n            tuple(groups),\n            (None, ) + data.domain.class_vars + data.domain.attributes)\n        order_check.setChecked(True)\n        self.assertIsNone(groups[0])\n        self.assertEqual([x.name for x in groups[1:]],\n                         ['sex', 'survived', 'age', 'status'])\n        select_attr(0)  # Class\n        self.assertIsNone(groups[0])\n        self.assertEqual([x.name for x in groups[1:]],\n                         ['sex', 'status', 'age', 'survived'])\n\n        data = self.heart\n        self.send_signal(\"Data\", data)\n        select_attr(0)  # Class\n        self.assertIsNone(groups[0])\n        self.assertEqual([x.name for x in groups[1:]],\n                         ['thal',\n                          'chest pain',\n                          'exerc ind ang',\n                          'slope peak exc ST',\n                          'gender',\n                          'rest ECG',\n                          'fasting blood sugar > 120',\n                          'diameter narrowing'])\n\n    def test_box_order_when_missing_stats(self):\n        self.widget.compare = 1\n        # The widget can't do anything smart here, but shouldn't crash\n        self.send_signal(self.widget.Inputs.data, self.iris[49:51])\n\n    def test_saved_selection(self):\n        self.send_signal(self.widget.Inputs.data, self.data)\n        selected_indices = self._select_data()\n        self.send_signal(self.widget.Inputs.data, self.zoo)\n        self.assertIsNone(self.get_output(self.widget.Outputs.selected_data))\n        self.send_signal(self.widget.Inputs.data, self.data)\n        np.testing.assert_array_equal(self.get_output(self.widget.Outputs.selected_data).X,\n                                      self.data.X[selected_indices])\n\n    def test_continuous_metas(self):\n        domain = self.iris.domain\n        metas = domain.attributes[:-1] + (StringVariable(\"str\"),)\n        domain = Domain([], domain.class_var, metas)\n        data = Table.from_table(domain, self.iris)\n        self.send_signal(self.widget.Inputs.data, data)\n\n    def test_label_overlap(self):\n        self.send_signal(self.widget.Inputs.data, self.heart)\n        self.widget.stretched = False\n        self.__select_variable(\"chest pain\")\n        self.__select_group(\"gender\")\n        self.widget.adjustSize()\n        self.widget.layout().activate()\n        self.widget.grab()  # ensure that the painting code is run\n\n    def test_empty_groups(self):\n        \"\"\"Test if groups with zero elements are not shown\"\"\"\n        table = Table(test_filename(\"datasets/cyber-security-breaches.tab\"))\n        self.send_signal(self.widget.Inputs.data, table)\n        self.__select_variable(\"US State\")\n        self.__select_group(\"US State\")\n        self.assertEqual(52, len(self.widget.boxes))\n\n        # select rows with US State equal to TX or MO\n        use_indexes = np.array([0, 1, 25, 26, 27])\n        table.X = table.X[use_indexes]\n        self.send_signal(self.widget.Inputs.data, table)\n        self.assertEqual(2, len(self.widget.boxes))\n\n    def test_sorting_disc_group_var(self):\n        \"\"\"Test if subgroups are sorted by their size\"\"\"\n        table = Table(\"heart_disease\")\n        self.send_signal(self.widget.Inputs.data, table)\n        self.__select_variable(\"gender\")\n        self.__select_group(\"chest pain\")\n\n        # checkbox not checked - preserve original order of selected grouping attribute\n        self.assertListEqual(self.widget.order, [0, 1, 2, 3])\n\n        # checkbox checked - sort by frequencies\n        self.widget.controls.sort_freqs.setChecked(True)\n        self.assertListEqual(self.widget.order, [0, 2, 1, 3])\n\n    def _select_data(self):\n        items = [item for item in self.widget.box_scene.items()\n                 if isinstance(item, FilterGraphicsRectItem)]\n        items[0].setSelected(True)\n        return [100, 103, 104, 108, 110, 111, 112, 115, 116,\n                120, 123, 124, 126, 128, 132, 133, 136, 137,\n                139, 140, 141, 143, 144, 145, 146, 147, 148]\n\n    def _select_list_items(self, _list):\n        model = _list.selectionModel()\n        for i in range(len(_list.model())):\n            model.setCurrentIndex(_list.model().index(i), model.ClearAndSelect)\n\n    def __select_variable(self, name, widget=None):\n        if widget is None:\n            widget = self.widget\n\n        self.__select_value(widget.controls.attribute, name)\n\n    def __select_group(self, name, widget=None):\n        if widget is None:\n            widget = self.widget\n\n        self.__select_value(widget.controls.group_var, name)\n\n    def __select_value(self, list, value):\n        m = list.model()\n        for i in range(m.rowCount()):\n            idx = m.index(i)\n            if m.data(idx) == value:\n                list.selectionModel().setCurrentIndex(\n                    idx, QItemSelectionModel.ClearAndSelect)\n\n    def test_unconditional_commit_on_new_signal(self):\n        with patch.object(self.widget, 'commit') as apply:\n            apply.reset_mock()\n            self.send_signal(self.widget.Inputs.data, self.zoo)\n            apply.assert_called()\n\n    def test_stretching(self):\n        self.send_signal(self.widget.Inputs.data, self.heart)\n        enabled = self.widget.controls.stretched.isEnabled\n\n        self.__select_variable(\"chest pain\")\n        self.__select_group(\"gender\")\n        self.assertTrue(enabled())\n\n        self.__select_variable(\"gender\")\n        self.__select_group(\"gender\")\n        self.assertFalse(enabled())\n\n        self.__select_variable(\"gender\")\n        self.__select_group(\"chest pain\")\n        self.assertTrue(enabled())\n\n    def test_value_all_missing_for_group(self):\n        \"\"\"\n        This is one of the extreme cases when we have a subgroup value\n        where all values in selected variable are missing. Box plot should\n        handle this.\n        \"\"\"\n        data = Table.from_list(\n            Domain([DiscreteVariable(\"a\", values=[\"v1\", \"v2\", \"v3\"]),\n                    DiscreteVariable(\"b\", values=[\"v3\", \"v4\"])]),\n            [[0., 0.],\n             [0., 1.],\n             [1., np.nan],\n             [1., np.nan],\n             [2., 0.],\n             [2., 0.]])\n        self.send_signal(self.widget.Inputs.data, data)\n\n        self.__select_variable(\"b\")\n        self.__select_group(\"a\")\n        self.assertTupleEqual(self.widget.conts.shape, (3, 2))",
                            "ast_type_str": "ClassDef",
                            "start_lineno": 50,
                            "end_lineno": 57,
                            "parent": {
                                "file_name": "Orange/widgets/visualize/tests/test_owboxplot.py",
                                "type": "class",
                                "name": "TestOWBoxPlot",
                                "content": "class TestOWBoxPlot(WidgetTest, WidgetOutputsTestMixin):\n    @classmethod\n    def setUpClass(cls):\n        super().setUpClass()\n        WidgetOutputsTestMixin.init(cls)\n\n        cls.iris = Table(\"iris\")\n        cls.zoo = Table(\"zoo\")\n        cls.housing = Table(\"housing\")\n        cls.titanic = Table(\"titanic\")\n        cls.heart = Table(\"heart_disease\")\n        cls.data = cls.iris\n        cls.signal_name = \"Data\"\n        cls.signal_data = cls.data\n\n    def setUp(self):\n        self.widget = self.create_widget(OWBoxPlot)\n\n    def test_input_data(self):\n        \"\"\"Check widget's data\"\"\"\n        self.send_signal(self.widget.Inputs.data, self.iris)\n        self.assertEqual(len(self.widget.attrs), 5)\n        self.assertEqual(len(self.widget.group_vars), 2)\n        self.assertFalse(self.widget.display_box.isHidden())\n        self.assertTrue(self.widget.stretching_box.isHidden())\n\n        self.send_signal(self.widget.Inputs.data, None)\n        self.assertEqual(len(self.widget.attrs), 0)\n        self.assertEqual(len(self.widget.group_vars), 1)\n        self.assertTrue(self.widget.display_box.isHidden())\n        self.assertFalse(self.widget.stretching_box.isHidden())\n\n    def test_dont_show_hidden_attrs(self):\n        \"\"\"Check widget's data\"\"\"\n        iris = Table(\"iris\")\n        iris.domain[\"iris\"].attributes[\"hidden\"] = True\n        iris.domain[\"petal length\"].attributes[\"hidden\"] = True\n        self.send_signal(self.widget.Inputs.data, iris)\n        self.assertEqual(len(self.widget.attrs), 3)\n        self.assertEqual(len(self.widget.group_vars), 1)\n\n    def test_primitive_metas(self):\n        new_domain = Domain(attributes=[], class_vars=[], metas=(\n            self.data.domain.attributes + self.data.domain.class_vars))\n        attrs_as_metas = self.data.transform(new_domain)\n        self.send_signal(self.widget.Inputs.data, attrs_as_metas)\n        self.assertTrue(self.widget.display_box.isEnabled())\n\n    def test_input_data_missings_cont_group_var(self):\n        \"\"\"Check widget with continuous data with missing values and group variable\"\"\"\n        data = self.iris.copy()\n        data.X[:, 0] = np.nan\n        self.send_signal(self.widget.Inputs.data, data)\n        # used to crash, see #1568\n\n    def test_input_data_missings_cont_no_group_var(self):\n        \"\"\"Check widget with continuous data with missing values and no group variable\"\"\"\n        data = self.housing\n        data.X[:, 0] = np.nan\n        self.send_signal(self.widget.Inputs.data, data)\n        # used to crash, see #1568\n\n    def test_input_data_missings_disc_group_var(self):\n        \"\"\"Check widget with discrete data with missing values and group variable\"\"\"\n        data = self.zoo\n        data.X[:, 1] = np.nan\n        data.domain.attributes[1].values = []\n        self.send_signal(\"Data\", data)\n        self.widget.controls.order_by_importance.setChecked(True)\n        self._select_list_items(self.widget.controls.attribute)\n        self._select_list_items(self.widget.controls.group_var)\n\n    def test_input_data_missings_disc_no_group_var(self):\n        \"\"\"Check widget discrete data with missing values and no group variable\"\"\"\n        data = self.zoo\n        data.domain.class_var = ContinuousVariable(\"cls\")\n        data.X[:, 1] = np.nan\n        data.domain.attributes[1].values = []\n        self.send_signal(\"Data\", data)\n        self._select_list_items(self.widget.controls.attribute)\n        self._select_list_items(self.widget.controls.group_var)\n\n    def test_attribute_combinations(self):\n        self.send_signal(self.widget.Inputs.data, self.heart)\n        group_list = self.widget.controls.group_var\n        m = group_list.selectionModel()\n        for i in range(len(group_list.model())):\n            m.setCurrentIndex(group_list.model().index(i), m.ClearAndSelect)\n            self._select_list_items(self.widget.controls.attribute)\n\n    def test_apply_sorting_group(self):\n        controls = self.widget.controls\n        group_list = controls.group_var\n        order_check = controls.order_by_importance\n        attributes = self.widget.attrs\n\n        def select_group(i):\n            group_selection = group_list.selectionModel()\n            group_selection.setCurrentIndex(\n                group_list.model().index(i),\n                group_selection.ClearAndSelect)\n\n        data = self.titanic\n        self.send_signal(\"Data\", data)\n\n        select_group(2)  # First attribute\n\n        order_check.setChecked(False)\n        self.assertEqual(tuple(attributes),\n                         data.domain.class_vars + data.domain.attributes)\n        order_check.setChecked(True)\n        self.assertEqual([x.name for x in attributes],\n                         ['sex', 'survived', 'age', 'status'])\n        select_group(1)  # Class\n        self.assertEqual([x.name for x in attributes],\n                         ['sex', 'status', 'age', 'survived'])\n\n        data = self.heart\n        self.send_signal(\"Data\", data)\n        select_group(1)  # Class\n        order_check.setChecked(True)\n        self.assertEqual([x.name for x in attributes],\n                         ['thal',\n                          'chest pain',\n                          'major vessels colored',\n                          'ST by exercise',\n                          'max HR',\n                          'exerc ind ang',\n                          'slope peak exc ST',\n                          'gender',\n                          'age',\n                          'rest ECG',\n                          'rest SBP',\n                          'cholesterol',\n                          'fasting blood sugar > 120',\n                          'diameter narrowing'])\n\n    def test_apply_sorting_vars(self):\n        controls = self.widget.controls\n        attr_list = self.widget.attrs\n        order_check = controls.order_grouping_by_importance\n        groups = self.widget.group_vars\n\n        def select_attr(i):\n            attr_selection = controls.attribute.selectionModel()\n            attr_selection.setCurrentIndex(\n                attr_list.index(i),\n                attr_selection.ClearAndSelect)\n\n        data = self.titanic\n        self.send_signal(\"Data\", data)\n\n        select_attr(1)  # First attribute\n\n        order_check.setChecked(False)\n        self.assertEqual(\n            tuple(groups),\n            (None, ) + data.domain.class_vars + data.domain.attributes)\n        order_check.setChecked(True)\n        self.assertIsNone(groups[0])\n        self.assertEqual([x.name for x in groups[1:]],\n                         ['sex', 'survived', 'age', 'status'])\n        select_attr(0)  # Class\n        self.assertIsNone(groups[0])\n        self.assertEqual([x.name for x in groups[1:]],\n                         ['sex', 'status', 'age', 'survived'])\n\n        data = self.heart\n        self.send_signal(\"Data\", data)\n        select_attr(0)  # Class\n        self.assertIsNone(groups[0])\n        self.assertEqual([x.name for x in groups[1:]],\n                         ['thal',\n                          'chest pain',\n                          'exerc ind ang',\n                          'slope peak exc ST',\n                          'gender',\n                          'rest ECG',\n                          'fasting blood sugar > 120',\n                          'diameter narrowing'])\n\n    def test_box_order_when_missing_stats(self):\n        self.widget.compare = 1\n        # The widget can't do anything smart here, but shouldn't crash\n        self.send_signal(self.widget.Inputs.data, self.iris[49:51])\n\n    def test_saved_selection(self):\n        self.send_signal(self.widget.Inputs.data, self.data)\n        selected_indices = self._select_data()\n        self.send_signal(self.widget.Inputs.data, self.zoo)\n        self.assertIsNone(self.get_output(self.widget.Outputs.selected_data))\n        self.send_signal(self.widget.Inputs.data, self.data)\n        np.testing.assert_array_equal(self.get_output(self.widget.Outputs.selected_data).X,\n                                      self.data.X[selected_indices])\n\n    def test_continuous_metas(self):\n        domain = self.iris.domain\n        metas = domain.attributes[:-1] + (StringVariable(\"str\"),)\n        domain = Domain([], domain.class_var, metas)\n        data = Table.from_table(domain, self.iris)\n        self.send_signal(self.widget.Inputs.data, data)\n\n    def test_label_overlap(self):\n        self.send_signal(self.widget.Inputs.data, self.heart)\n        self.widget.stretched = False\n        self.__select_variable(\"chest pain\")\n        self.__select_group(\"gender\")\n        self.widget.adjustSize()\n        self.widget.layout().activate()\n        self.widget.grab()  # ensure that the painting code is run\n\n    def test_empty_groups(self):\n        \"\"\"Test if groups with zero elements are not shown\"\"\"\n        table = Table(test_filename(\"datasets/cyber-security-breaches.tab\"))\n        self.send_signal(self.widget.Inputs.data, table)\n        self.__select_variable(\"US State\")\n        self.__select_group(\"US State\")\n        self.assertEqual(52, len(self.widget.boxes))\n\n        # select rows with US State equal to TX or MO\n        use_indexes = np.array([0, 1, 25, 26, 27])\n        table.X = table.X[use_indexes]\n        self.send_signal(self.widget.Inputs.data, table)\n        self.assertEqual(2, len(self.widget.boxes))\n\n    def test_sorting_disc_group_var(self):\n        \"\"\"Test if subgroups are sorted by their size\"\"\"\n        table = Table(\"heart_disease\")\n        self.send_signal(self.widget.Inputs.data, table)\n        self.__select_variable(\"gender\")\n        self.__select_group(\"chest pain\")\n\n        # checkbox not checked - preserve original order of selected grouping attribute\n        self.assertListEqual(self.widget.order, [0, 1, 2, 3])\n\n        # checkbox checked - sort by frequencies\n        self.widget.controls.sort_freqs.setChecked(True)\n        self.assertListEqual(self.widget.order, [0, 2, 1, 3])\n\n    def _select_data(self):\n        items = [item for item in self.widget.box_scene.items()\n                 if isinstance(item, FilterGraphicsRectItem)]\n        items[0].setSelected(True)\n        return [100, 103, 104, 108, 110, 111, 112, 115, 116,\n                120, 123, 124, 126, 128, 132, 133, 136, 137,\n                139, 140, 141, 143, 144, 145, 146, 147, 148]\n\n    def _select_list_items(self, _list):\n        model = _list.selectionModel()\n        for i in range(len(_list.model())):\n            model.setCurrentIndex(_list.model().index(i), model.ClearAndSelect)\n\n    def __select_variable(self, name, widget=None):\n        if widget is None:\n            widget = self.widget\n\n        self.__select_value(widget.controls.attribute, name)\n\n    def __select_group(self, name, widget=None):\n        if widget is None:\n            widget = self.widget\n\n        self.__select_value(widget.controls.group_var, name)\n\n    def __select_value(self, list, value):\n        m = list.model()\n        for i in range(m.rowCount()):\n            idx = m.index(i)\n            if m.data(idx) == value:\n                list.selectionModel().setCurrentIndex(\n                    idx, QItemSelectionModel.ClearAndSelect)\n\n    def test_unconditional_commit_on_new_signal(self):\n        with patch.object(self.widget, 'commit') as apply:\n            apply.reset_mock()\n            self.send_signal(self.widget.Inputs.data, self.zoo)\n            apply.assert_called()\n\n    def test_stretching(self):\n        self.send_signal(self.widget.Inputs.data, self.heart)\n        enabled = self.widget.controls.stretched.isEnabled\n\n        self.__select_variable(\"chest pain\")\n        self.__select_group(\"gender\")\n        self.assertTrue(enabled())\n\n        self.__select_variable(\"gender\")\n        self.__select_group(\"gender\")\n        self.assertFalse(enabled())\n\n        self.__select_variable(\"gender\")\n        self.__select_group(\"chest pain\")\n        self.assertTrue(enabled())\n\n    def test_value_all_missing_for_group(self):\n        \"\"\"\n        This is one of the extreme cases when we have a subgroup value\n        where all values in selected variable are missing. Box plot should\n        handle this.\n        \"\"\"\n        data = Table.from_list(\n            Domain([DiscreteVariable(\"a\", values=[\"v1\", \"v2\", \"v3\"]),\n                    DiscreteVariable(\"b\", values=[\"v3\", \"v4\"])]),\n            [[0., 0.],\n             [0., 1.],\n             [1., np.nan],\n             [1., np.nan],\n             [2., 0.],\n             [2., 0.]])\n        self.send_signal(self.widget.Inputs.data, data)\n\n        self.__select_variable(\"b\")\n        self.__select_group(\"a\")\n        self.assertTupleEqual(self.widget.conts.shape, (3, 2))",
                                "ast_type_str": "ClassDef",
                                "start_lineno": 18,
                                "end_lineno": 331,
                                "parent": null
                            }
                        }
                    ],
                    "deleted_entities": []
                }
            ]
        }
    ],
    "old_commit_hash": "06f7630d8753143f4c7f6b029c7dc79264d76318^",
    "new_commit_hash": "06f7630d8753143f4c7f6b029c7dc79264d76318",
    "commit_message": "Merge pull request #4257 from janezd/boxplot-hidden-attributes\n\nBox Plot: Don't show hidden attributes",
    "commit_date": "2019-12-06T19:10:15+01:00",
    "metadata": {}
}
{
    "repo_name": "orange3",
    "new_commit_hash": "06f7630d8753143f4c7f6b029c7dc79264d76318",
    "test_file_codes": [
        "# Test methods with long descriptive names can omit docstrings\n# pylint: disable=missing-docstring\nimport unittest\nfrom unittest.mock import patch\n\nimport numpy as np\nfrom AnyQt.QtCore import QItemSelectionModel\n\nfrom Orange.data import Table, ContinuousVariable, StringVariable, Domain, \\\n    DiscreteVariable\nfrom Orange.widgets.visualize.owboxplot import (\n    OWBoxPlot, FilterGraphicsRectItem, _quantiles\n)\nfrom Orange.widgets.tests.base import WidgetTest, WidgetOutputsTestMixin\nfrom Orange.tests import test_filename\n\n\nclass TestOWBoxPlot(WidgetTest, WidgetOutputsTestMixin):\n    @classmethod\n    def setUpClass(cls):\n        super().setUpClass()\n        WidgetOutputsTestMixin.init(cls)\n\n        cls.iris = Table(\"iris\")\n        cls.zoo = Table(\"zoo\")\n        cls.housing = Table(\"housing\")\n        cls.titanic = Table(\"titanic\")\n        cls.heart = Table(\"heart_disease\")\n        cls.data = cls.iris\n        cls.signal_name = \"Data\"\n        cls.signal_data = cls.data\n\n    def setUp(self):\n        self.widget = self.create_widget(OWBoxPlot)\n\n    def test_input_data(self):\n        \"\"\"Check widget's data\"\"\"\n        self.send_signal(self.widget.Inputs.data, self.iris)\n        self.assertEqual(len(self.widget.attrs), 5)\n        self.assertEqual(len(self.widget.group_vars), 2)\n        self.assertFalse(self.widget.display_box.isHidden())\n        self.assertTrue(self.widget.stretching_box.isHidden())\n\n        self.send_signal(self.widget.Inputs.data, None)\n        self.assertEqual(len(self.widget.attrs), 0)\n        self.assertEqual(len(self.widget.group_vars), 1)\n        self.assertTrue(self.widget.display_box.isHidden())\n        self.assertFalse(self.widget.stretching_box.isHidden())\n\n    def test_dont_show_hidden_attrs(self):\n        \"\"\"Check widget's data\"\"\"\n        iris = Table(\"iris\")\n        iris.domain[\"iris\"].attributes[\"hidden\"] = True\n        iris.domain[\"petal length\"].attributes[\"hidden\"] = True\n        self.send_signal(self.widget.Inputs.data, iris)\n        self.assertEqual(len(self.widget.attrs), 3)\n        self.assertEqual(len(self.widget.group_vars), 1)\n\n    def test_primitive_metas(self):\n        new_domain = Domain(attributes=[], class_vars=[], metas=(\n            self.data.domain.attributes + self.data.domain.class_vars))\n        attrs_as_metas = self.data.transform(new_domain)\n        self.send_signal(self.widget.Inputs.data, attrs_as_metas)\n        self.assertTrue(self.widget.display_box.isEnabled())\n\n    def test_input_data_missings_cont_group_var(self):\n        \"\"\"Check widget with continuous data with missing values and group variable\"\"\"\n        data = self.iris.copy()\n        data.X[:, 0] = np.nan\n        self.send_signal(self.widget.Inputs.data, data)\n        # used to crash, see #1568\n\n    def test_input_data_missings_cont_no_group_var(self):\n        \"\"\"Check widget with continuous data with missing values and no group variable\"\"\"\n        data = self.housing\n        data.X[:, 0] = np.nan\n        self.send_signal(self.widget.Inputs.data, data)\n        # used to crash, see #1568\n\n    def test_input_data_missings_disc_group_var(self):\n        \"\"\"Check widget with discrete data with missing values and group variable\"\"\"\n        data = self.zoo\n        data.X[:, 1] = np.nan\n        data.domain.attributes[1].values = []\n        self.send_signal(\"Data\", data)\n        self.widget.controls.order_by_importance.setChecked(True)\n        self._select_list_items(self.widget.controls.attribute)\n        self._select_list_items(self.widget.controls.group_var)\n\n    def test_input_data_missings_disc_no_group_var(self):\n        \"\"\"Check widget discrete data with missing values and no group variable\"\"\"\n        data = self.zoo\n        data.domain.class_var = ContinuousVariable(\"cls\")\n        data.X[:, 1] = np.nan\n        data.domain.attributes[1].values = []\n        self.send_signal(\"Data\", data)\n        self._select_list_items(self.widget.controls.attribute)\n        self._select_list_items(self.widget.controls.group_var)\n\n    def test_attribute_combinations(self):\n        self.send_signal(self.widget.Inputs.data, self.heart)\n        group_list = self.widget.controls.group_var\n        m = group_list.selectionModel()\n        for i in range(len(group_list.model())):\n            m.setCurrentIndex(group_list.model().index(i), m.ClearAndSelect)\n            self._select_list_items(self.widget.controls.attribute)\n\n    def test_apply_sorting_group(self):\n        controls = self.widget.controls\n        group_list = controls.group_var\n        order_check = controls.order_by_importance\n        attributes = self.widget.attrs\n\n        def select_group(i):\n            group_selection = group_list.selectionModel()\n            group_selection.setCurrentIndex(\n                group_list.model().index(i),\n                group_selection.ClearAndSelect)\n\n        data = self.titanic\n        self.send_signal(\"Data\", data)\n\n        select_group(2)  # First attribute\n\n        order_check.setChecked(False)\n        self.assertEqual(tuple(attributes),\n                         data.domain.class_vars + data.domain.attributes)\n        order_check.setChecked(True)\n        self.assertEqual([x.name for x in attributes],\n                         ['sex', 'survived', 'age', 'status'])\n        select_group(1)  # Class\n        self.assertEqual([x.name for x in attributes],\n                         ['sex', 'status', 'age', 'survived'])\n\n        data = self.heart\n        self.send_signal(\"Data\", data)\n        select_group(1)  # Class\n        order_check.setChecked(True)\n        self.assertEqual([x.name for x in attributes],\n                         ['thal',\n                          'chest pain',\n                          'major vessels colored',\n                          'ST by exercise',\n                          'max HR',\n                          'exerc ind ang',\n                          'slope peak exc ST',\n                          'gender',\n                          'age',\n                          'rest ECG',\n                          'rest SBP',\n                          'cholesterol',\n                          'fasting blood sugar > 120',\n                          'diameter narrowing'])\n\n    def test_apply_sorting_vars(self):\n        controls = self.widget.controls\n        attr_list = self.widget.attrs\n        order_check = controls.order_grouping_by_importance\n        groups = self.widget.group_vars\n\n        def select_attr(i):\n            attr_selection = controls.attribute.selectionModel()\n            attr_selection.setCurrentIndex(\n                attr_list.index(i),\n                attr_selection.ClearAndSelect)\n\n        data = self.titanic\n        self.send_signal(\"Data\", data)\n\n        select_attr(1)  # First attribute\n\n        order_check.setChecked(False)\n        self.assertEqual(\n            tuple(groups),\n            (None, ) + data.domain.class_vars + data.domain.attributes)\n        order_check.setChecked(True)\n        self.assertIsNone(groups[0])\n        self.assertEqual([x.name for x in groups[1:]],\n                         ['sex', 'survived', 'age', 'status'])\n        select_attr(0)  # Class\n        self.assertIsNone(groups[0])\n        self.assertEqual([x.name for x in groups[1:]],\n                         ['sex', 'status', 'age', 'survived'])\n\n        data = self.heart\n        self.send_signal(\"Data\", data)\n        select_attr(0)  # Class\n        self.assertIsNone(groups[0])\n        self.assertEqual([x.name for x in groups[1:]],\n                         ['thal',\n                          'chest pain',\n                          'exerc ind ang',\n                          'slope peak exc ST',\n                          'gender',\n                          'rest ECG',\n                          'fasting blood sugar > 120',\n                          'diameter narrowing'])\n\n    def test_box_order_when_missing_stats(self):\n        self.widget.compare = 1\n        # The widget can't do anything smart here, but shouldn't crash\n        self.send_signal(self.widget.Inputs.data, self.iris[49:51])\n\n    def test_saved_selection(self):\n        self.send_signal(self.widget.Inputs.data, self.data)\n        selected_indices = self._select_data()\n        self.send_signal(self.widget.Inputs.data, self.zoo)\n        self.assertIsNone(self.get_output(self.widget.Outputs.selected_data))\n        self.send_signal(self.widget.Inputs.data, self.data)\n        np.testing.assert_array_equal(self.get_output(self.widget.Outputs.selected_data).X,\n                                      self.data.X[selected_indices])\n\n    def test_continuous_metas(self):\n        domain = self.iris.domain\n        metas = domain.attributes[:-1] + (StringVariable(\"str\"),)\n        domain = Domain([], domain.class_var, metas)\n        data = Table.from_table(domain, self.iris)\n        self.send_signal(self.widget.Inputs.data, data)\n\n    def test_label_overlap(self):\n        self.send_signal(self.widget.Inputs.data, self.heart)\n        self.widget.stretched = False\n        self.__select_variable(\"chest pain\")\n        self.__select_group(\"gender\")\n        self.widget.adjustSize()\n        self.widget.layout().activate()\n        self.widget.grab()  # ensure that the painting code is run\n\n    def test_empty_groups(self):\n        \"\"\"Test if groups with zero elements are not shown\"\"\"\n        table = Table(test_filename(\"datasets/cyber-security-breaches.tab\"))\n        self.send_signal(self.widget.Inputs.data, table)\n        self.__select_variable(\"US State\")\n        self.__select_group(\"US State\")\n        self.assertEqual(52, len(self.widget.boxes))\n\n        # select rows with US State equal to TX or MO\n        use_indexes = np.array([0, 1, 25, 26, 27])\n        table.X = table.X[use_indexes]\n        self.send_signal(self.widget.Inputs.data, table)\n        self.assertEqual(2, len(self.widget.boxes))\n\n    def test_sorting_disc_group_var(self):\n        \"\"\"Test if subgroups are sorted by their size\"\"\"\n        table = Table(\"heart_disease\")\n        self.send_signal(self.widget.Inputs.data, table)\n        self.__select_variable(\"gender\")\n        self.__select_group(\"chest pain\")\n\n        # checkbox not checked - preserve original order of selected grouping attribute\n        self.assertListEqual(self.widget.order, [0, 1, 2, 3])\n\n        # checkbox checked - sort by frequencies\n        self.widget.controls.sort_freqs.setChecked(True)\n        self.assertListEqual(self.widget.order, [0, 2, 1, 3])\n\n    def _select_data(self):\n        items = [item for item in self.widget.box_scene.items()\n                 if isinstance(item, FilterGraphicsRectItem)]\n        items[0].setSelected(True)\n        return [100, 103, 104, 108, 110, 111, 112, 115, 116,\n                120, 123, 124, 126, 128, 132, 133, 136, 137,\n                139, 140, 141, 143, 144, 145, 146, 147, 148]\n\n    def _select_list_items(self, _list):\n        model = _list.selectionModel()\n        for i in range(len(_list.model())):\n            model.setCurrentIndex(_list.model().index(i), model.ClearAndSelect)\n\n    def __select_variable(self, name, widget=None):\n        if widget is None:\n            widget = self.widget\n\n        self.__select_value(widget.controls.attribute, name)\n\n    def __select_group(self, name, widget=None):\n        if widget is None:\n            widget = self.widget\n\n        self.__select_value(widget.controls.group_var, name)\n\n    def __select_value(self, list, value):\n        m = list.model()\n        for i in range(m.rowCount()):\n            idx = m.index(i)\n            if m.data(idx) == value:\n                list.selectionModel().setCurrentIndex(\n                    idx, QItemSelectionModel.ClearAndSelect)\n\n    def test_unconditional_commit_on_new_signal(self):\n        with patch.object(self.widget, 'commit') as apply:\n            apply.reset_mock()\n            self.send_signal(self.widget.Inputs.data, self.zoo)\n            apply.assert_called()\n\n    def test_stretching(self):\n        self.send_signal(self.widget.Inputs.data, self.heart)\n        enabled = self.widget.controls.stretched.isEnabled\n\n        self.__select_variable(\"chest pain\")\n        self.__select_group(\"gender\")\n        self.assertTrue(enabled())\n\n        self.__select_variable(\"gender\")\n        self.__select_group(\"gender\")\n        self.assertFalse(enabled())\n\n        self.__select_variable(\"gender\")\n        self.__select_group(\"chest pain\")\n        self.assertTrue(enabled())\n\n    def test_value_all_missing_for_group(self):\n        \"\"\"\n        This is one of the extreme cases when we have a subgroup value\n        where all values in selected variable are missing. Box plot should\n        handle this.\n        \"\"\"\n        data = Table.from_list(\n            Domain([DiscreteVariable(\"a\", values=[\"v1\", \"v2\", \"v3\"]),\n                    DiscreteVariable(\"b\", values=[\"v3\", \"v4\"])]),\n            [[0., 0.],\n             [0., 1.],\n             [1., np.nan],\n             [1., np.nan],\n             [2., 0.],\n             [2., 0.]])\n        self.send_signal(self.widget.Inputs.data, data)\n\n        self.__select_variable(\"b\")\n        self.__select_group(\"a\")\n        self.assertTupleEqual(self.widget.conts.shape, (3, 2))\n\n\nclass TestUtils(unittest.TestCase):\n    def test(self):\n        np.testing.assert_array_equal(\n            _quantiles(range(1, 8 + 1), [1.] * 8, [0.0, 0.25, 0.5, 0.75, 1.0]),\n            [1., 2.5, 4.5, 6.5, 8.]\n        )\n        np.testing.assert_array_equal(\n            _quantiles(range(1, 8 + 1), [1.] * 8, [0.0, 0.25, 0.5, 0.75, 1.0]),\n            [1., 2.5, 4.5, 6.5, 8.]\n        )\n        np.testing.assert_array_equal(\n            _quantiles(range(1, 4 + 1), [1., 2., 1., 2],\n                       [0.0, 0.25, 0.5, 0.75, 1.0]),\n            [1.0, 2.0, 2.5, 4.0, 4.0]\n        )\n        np.testing.assert_array_equal(\n            _quantiles(range(1, 4 + 1), [2., 1., 1., 2.],\n                       [0.0, 0.25, 0.5, 0.75, 1.0]),\n            [1.0, 1.0, 2.5, 4.0, 4.0]\n        )\n        np.testing.assert_array_equal(\n            _quantiles(range(1, 4 + 1), [1., 1., 1., 1.],\n                       [0.0, 0.25, 0.5, 0.75, 1.0]),\n            [1.0, 1.5, 2.5, 3.5, 4.0]\n        )\n        np.testing.assert_array_equal(\n            _quantiles(range(1, 4 + 1), [1., 1., 1., 1.],\n                       [0.0, 0.25, 0.5, 0.75, 1.0], interpolation=\"higher\"),\n            [1, 2, 3, 4, 4]\n        )\n        np.testing.assert_array_equal(\n            _quantiles(range(1, 4 + 1), [1., 1., 1., 1.],\n                       [0.0, 0.25, 0.5, 0.75, 1.0], interpolation=\"lower\"),\n            [1, 1, 2, 3, 4]\n        )\n"
    ],
    "test_file_names": [
        "test_1.py"
    ],
    "setup_res_code": 0,
    "setup_res_stdout": "Starting Orange installation attempts...\nAttempting installation with Python 3.7...\nrunning build_ext\nrunning build_src\nbuild_src\nbuilding extension \"Orange.classification._simple_tree\" sources\nbuilding extension \"Orange.classification._tree_scorers\" sources\nbuilding extension \"Orange.data._valuecount\" sources\nbuilding extension \"Orange.data._contingency\" sources\nbuilding extension \"Orange.data._io\" sources\nbuilding extension \"Orange.data._variable\" sources\nbuilding extension \"Orange.distance._distance\" sources\nbuilding extension \"Orange.preprocess._discretize\" sources\nbuilding extension \"Orange.preprocess._relieff\" sources\nbuilding extension \"Orange.projection._som\" sources\nbuilding extension \"Orange.widgets.utils._grid_density\" sources\nbuild_src: building npy-pkg config files\ncustomize UnixCCompiler\nC compiler: cc -pthread -Wno-unused-result -Wsign-compare -Wunreachable-code -DNDEBUG -g -fwrapv -O3 -Wall -fPIC -I/tools/deps/include -I/tools/deps/include/ncursesw -fPIC\n\ncreating /tmp/tmpovo8psl6/tmp\ncreating /tmp/tmpovo8psl6/tmp/tmpovo8psl6\ncompile options: '-MMD -MF /tmp/tmpovo8psl6/file.c.d -c'\ncc: /tmp/tmpovo8psl6/file.c\ncustomize UnixCCompiler using build_ext\ncustomize UnixCCompiler\nC compiler: cc -pthread -Wno-unused-result -Wsign-compare -Wunreachable-code -DNDEBUG -g -fwrapv -O3 -Wall -fPIC -I/tools/deps/include -I/tools/deps/include/ncursesw -fPIC\n\ncreating /tmp/tmp5nw7zwjb/tmp\ncreating /tmp/tmp5nw7zwjb/tmp/tmp5nw7zwjb\ncompile options: '-MMD -MF /tmp/tmp5nw7zwjb/file.c.d -c'\ncc: /tmp/tmp5nw7zwjb/file.c\ncustomize UnixCCompiler using build_ext\nbuilding 'Orange.classification._simple_tree' extension\ncompiling C sources\nC compiler: cc -pthread -Wno-unused-result -Wsign-compare -Wunreachable-code -DNDEBUG -g -fwrapv -O3 -Wall -fPIC -I/tools/deps/include -I/tools/deps/include/ncursesw -fPIC\n\ncreating build/temp.linux-x86_64-3.7/Orange\ncreating build/temp.linux-x86_64-3.7/Orange/classification\ncompile options: '-I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_06f7630d8753143f4c7f6b029c7dc79264d76318/.venv/lib/python3.7/site-packages/numpy/core/include -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_06f7630d8753143f4c7f6b029c7dc79264d76318/.venv/include -I/home/gcpuser/.local/share/uv/python/cpython-3.7.9-linux-x86_64-gnu/include/python3.7m -c'\ncc: Orange/classification/_simple_tree.c\ncc -pthread -shared -L/tools/deps/lib build/temp.linux-x86_64-3.7/Orange/classification/_simple_tree.o -L/home/gcpuser/.local/share/uv/python/cpython-3.7.9-linux-x86_64-gnu/lib -lm -lpython3.7m -o Orange/classification/_simple_tree.cpython-37m-x86_64-linux-gnu.so\nbuilding 'Orange.classification._tree_scorers' extension\ncompiling C sources\nC compiler: cc -pthread -Wno-unused-result -Wsign-compare -Wunreachable-code -DNDEBUG -g -fwrapv -O3 -Wall -fPIC -I/tools/deps/include -I/tools/deps/include/ncursesw -fPIC\n\ncompile options: '-I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_06f7630d8753143f4c7f6b029c7dc79264d76318/.venv/lib/python3.7/site-packages/numpy/core/include -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_06f7630d8753143f4c7f6b029c7dc79264d76318/.venv/lib/python3.7/site-packages/numpy/core/include -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_06f7630d8753143f4c7f6b029c7dc79264d76318/.venv/include -I/home/gcpuser/.local/share/uv/python/cpython-3.7.9-linux-x86_64-gnu/include/python3.7m -c'\ncc: Orange/classification/_tree_scorers.c\ncc -pthread -shared -L/tools/deps/lib build/temp.linux-x86_64-3.7/Orange/classification/_tree_scorers.o -L/home/gcpuser/.local/share/uv/python/cpython-3.7.9-linux-x86_64-gnu/lib -lm -lpython3.7m -o Orange/classification/_tree_scorers.cpython-37m-x86_64-linux-gnu.so\nbuilding 'Orange.data._valuecount' extension\ncompiling C sources\nC compiler: cc -pthread -Wno-unused-result -Wsign-compare -Wunreachable-code -DNDEBUG -g -fwrapv -O3 -Wall -fPIC -I/tools/deps/include -I/tools/deps/include/ncursesw -fPIC\n\ncreating build/temp.linux-x86_64-3.7/Orange/data\ncompile options: '-I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_06f7630d8753143f4c7f6b029c7dc79264d76318/.venv/lib/python3.7/site-packages/numpy/core/include -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_06f7630d8753143f4c7f6b029c7dc79264d76318/.venv/lib/python3.7/site-packages/numpy/core/include -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_06f7630d8753143f4c7f6b029c7dc79264d76318/.venv/include -I/home/gcpuser/.local/share/uv/python/cpython-3.7.9-linux-x86_64-gnu/include/python3.7m -c'\ncc: Orange/data/_valuecount.c\ncc -pthread -shared -L/tools/deps/lib build/temp.linux-x86_64-3.7/Orange/data/_valuecount.o -L/home/gcpuser/.local/share/uv/python/cpython-3.7.9-linux-x86_64-gnu/lib -lm -lpython3.7m -o Orange/data/_valuecount.cpython-37m-x86_64-linux-gnu.so\nbuilding 'Orange.data._contingency' extension\ncompiling C sources\nC compiler: cc -pthread -Wno-unused-result -Wsign-compare -Wunreachable-code -DNDEBUG -g -fwrapv -O3 -Wall -fPIC -I/tools/deps/include -I/tools/deps/include/ncursesw -fPIC\n\ncompile options: '-I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_06f7630d8753143f4c7f6b029c7dc79264d76318/.venv/lib/python3.7/site-packages/numpy/core/include -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_06f7630d8753143f4c7f6b029c7dc79264d76318/.venv/lib/python3.7/site-packages/numpy/core/include -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_06f7630d8753143f4c7f6b029c7dc79264d76318/.venv/include -I/home/gcpuser/.local/share/uv/python/cpython-3.7.9-linux-x86_64-gnu/include/python3.7m -c'\ncc: Orange/data/_contingency.c\ncc -pthread -shared -L/tools/deps/lib build/temp.linux-x86_64-3.7/Orange/data/_contingency.o -L/home/gcpuser/.local/share/uv/python/cpython-3.7.9-linux-x86_64-gnu/lib -lm -lpython3.7m -o Orange/data/_contingency.cpython-37m-x86_64-linux-gnu.so\nbuilding 'Orange.data._io' extension\ncompiling C sources\nC compiler: cc -pthread -Wno-unused-result -Wsign-compare -Wunreachable-code -DNDEBUG -g -fwrapv -O3 -Wall -fPIC -I/tools/deps/include -I/tools/deps/include/ncursesw -fPIC\n\ncompile options: '-I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_06f7630d8753143f4c7f6b029c7dc79264d76318/.venv/lib/python3.7/site-packages/numpy/core/include -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_06f7630d8753143f4c7f6b029c7dc79264d76318/.venv/lib/python3.7/site-packages/numpy/core/include -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_06f7630d8753143f4c7f6b029c7dc79264d76318/.venv/include -I/home/gcpuser/.local/share/uv/python/cpython-3.7.9-linux-x86_64-gnu/include/python3.7m -c'\ncc: Orange/data/_io.c\ncc -pthread -shared -L/tools/deps/lib build/temp.linux-x86_64-3.7/Orange/data/_io.o -L/home/gcpuser/.local/share/uv/python/cpython-3.7.9-linux-x86_64-gnu/lib -lm -lpython3.7m -o Orange/data/_io.cpython-37m-x86_64-linux-gnu.so\nbuilding 'Orange.data._variable' extension\ncompiling C sources\nC compiler: cc -pthread -Wno-unused-result -Wsign-compare -Wunreachable-code -DNDEBUG -g -fwrapv -O3 -Wall -fPIC -I/tools/deps/include -I/tools/deps/include/ncursesw -fPIC\n\ncompile options: '-I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_06f7630d8753143f4c7f6b029c7dc79264d76318/.venv/lib/python3.7/site-packages/numpy/core/include -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_06f7630d8753143f4c7f6b029c7dc79264d76318/.venv/lib/python3.7/site-packages/numpy/core/include -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_06f7630d8753143f4c7f6b029c7dc79264d76318/.venv/include -I/home/gcpuser/.local/share/uv/python/cpython-3.7.9-linux-x86_64-gnu/include/python3.7m -c'\ncc: Orange/data/_variable.c\ncc -pthread -shared -L/tools/deps/lib build/temp.linux-x86_64-3.7/Orange/data/_variable.o -L/home/gcpuser/.local/share/uv/python/cpython-3.7.9-linux-x86_64-gnu/lib -lm -lpython3.7m -o Orange/data/_variable.cpython-37m-x86_64-linux-gnu.so\nbuilding 'Orange.distance._distance' extension\ncompiling C sources\nC compiler: cc -pthread -Wno-unused-result -Wsign-compare -Wunreachable-code -DNDEBUG -g -fwrapv -O3 -Wall -fPIC -I/tools/deps/include -I/tools/deps/include/ncursesw -fPIC\n\ncreating build/temp.linux-x86_64-3.7/Orange/distance\ncompile options: '-I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_06f7630d8753143f4c7f6b029c7dc79264d76318/.venv/lib/python3.7/site-packages/numpy/core/include -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_06f7630d8753143f4c7f6b029c7dc79264d76318/.venv/lib/python3.7/site-packages/numpy/core/include -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_06f7630d8753143f4c7f6b029c7dc79264d76318/.venv/include -I/home/gcpuser/.local/share/uv/python/cpython-3.7.9-linux-x86_64-gnu/include/python3.7m -c'\ncc: Orange/distance/_distance.c\ncc -pthread -shared -L/tools/deps/lib build/temp.linux-x86_64-3.7/Orange/distance/_distance.o -L/home/gcpuser/.local/share/uv/python/cpython-3.7.9-linux-x86_64-gnu/lib -lm -lpython3.7m -o Orange/distance/_distance.cpython-37m-x86_64-linux-gnu.so\nbuilding 'Orange.preprocess._discretize' extension\ncompiling C sources\nC compiler: cc -pthread -Wno-unused-result -Wsign-compare -Wunreachable-code -DNDEBUG -g -fwrapv -O3 -Wall -fPIC -I/tools/deps/include -I/tools/deps/include/ncursesw -fPIC\n\ncreating build/temp.linux-x86_64-3.7/Orange/preprocess\ncompile options: '-I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_06f7630d8753143f4c7f6b029c7dc79264d76318/.venv/lib/python3.7/site-packages/numpy/core/include -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_06f7630d8753143f4c7f6b029c7dc79264d76318/.venv/lib/python3.7/site-packages/numpy/core/include -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_06f7630d8753143f4c7f6b029c7dc79264d76318/.venv/include -I/home/gcpuser/.local/share/uv/python/cpython-3.7.9-linux-x86_64-gnu/include/python3.7m -c'\ncc: Orange/preprocess/_discretize.c\ncc -pthread -shared -L/tools/deps/lib build/temp.linux-x86_64-3.7/Orange/preprocess/_discretize.o -L/home/gcpuser/.local/share/uv/python/cpython-3.7.9-linux-x86_64-gnu/lib -lm -lpython3.7m -o Orange/preprocess/_discretize.cpython-37m-x86_64-linux-gnu.so\nbuilding 'Orange.preprocess._relieff' extension\ncompiling C++ sources\nC compiler: c++ -pthread -Wno-unused-result -Wsign-compare -Wunreachable-code -DNDEBUG -g -fwrapv -O3 -Wall -fPIC -I/tools/deps/include -I/tools/deps/include/ncursesw -fPIC\n\ncompile options: '-I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_06f7630d8753143f4c7f6b029c7dc79264d76318/.venv/lib/python3.7/site-packages/numpy/core/include -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_06f7630d8753143f4c7f6b029c7dc79264d76318/.venv/lib/python3.7/site-packages/numpy/core/include -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_06f7630d8753143f4c7f6b029c7dc79264d76318/.venv/include -I/home/gcpuser/.local/share/uv/python/cpython-3.7.9-linux-x86_64-gnu/include/python3.7m -c'\nc++: Orange/preprocess/_relieff.cpp\nc++ -pthread -shared -L/tools/deps/lib build/temp.linux-x86_64-3.7/Orange/preprocess/_relieff.o -L/home/gcpuser/.local/share/uv/python/cpython-3.7.9-linux-x86_64-gnu/lib -lm -lpython3.7m -o Orange/preprocess/_relieff.cpython-37m-x86_64-linux-gnu.so\nbuilding 'Orange.projection._som' extension\ncompiling C sources\nC compiler: cc -pthread -Wno-unused-result -Wsign-compare -Wunreachable-code -DNDEBUG -g -fwrapv -O3 -Wall -fPIC -I/tools/deps/include -I/tools/deps/include/ncursesw -fPIC\n\ncreating build/temp.linux-x86_64-3.7/Orange/projection\ncompile options: '-I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_06f7630d8753143f4c7f6b029c7dc79264d76318/.venv/lib/python3.7/site-packages/numpy/core/include -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_06f7630d8753143f4c7f6b029c7dc79264d76318/.venv/lib/python3.7/site-packages/numpy/core/include -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_06f7630d8753143f4c7f6b029c7dc79264d76318/.venv/include -I/home/gcpuser/.local/share/uv/python/cpython-3.7.9-linux-x86_64-gnu/include/python3.7m -c'\ncc: Orange/projection/_som.c\ncc -pthread -shared -L/tools/deps/lib build/temp.linux-x86_64-3.7/Orange/projection/_som.o -L/home/gcpuser/.local/share/uv/python/cpython-3.7.9-linux-x86_64-gnu/lib -lm -lpython3.7m -o Orange/projection/_som.cpython-37m-x86_64-linux-gnu.so\nbuilding 'Orange.widgets.utils._grid_density' extension\ncompiling C++ sources\nC compiler: c++ -pthread -Wno-unused-result -Wsign-compare -Wunreachable-code -DNDEBUG -g -fwrapv -O3 -Wall -fPIC -I/tools/deps/include -I/tools/deps/include/ncursesw -fPIC\n\ncreating build/temp.linux-x86_64-3.7/Orange/widgets\ncreating build/temp.linux-x86_64-3.7/Orange/widgets/utils\ncompile options: '-I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_06f7630d8753143f4c7f6b029c7dc79264d76318/.venv/lib/python3.7/site-packages/numpy/core/include -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_06f7630d8753143f4c7f6b029c7dc79264d76318/.venv/include -I/home/gcpuser/.local/share/uv/python/cpython-3.7.9-linux-x86_64-gnu/include/python3.7m -c'\nc++: Orange/widgets/utils/_grid_density.cpp\nc++ -pthread -shared -L/tools/deps/lib build/temp.linux-x86_64-3.7/Orange/widgets/utils/_grid_density.o -L/home/gcpuser/.local/share/uv/python/cpython-3.7.9-linux-x86_64-gnu/lib -lm -lpython3.7m -o Orange/widgets/utils/_grid_density.cpython-37m-x86_64-linux-gnu.so\nrunning develop\nrunning build_scripts\nrunning egg_info\nrunning build_src\nbuild_src\nbuilding extension \"Orange.classification._simple_tree\" sources\nbuilding extension \"Orange.classification._tree_scorers\" sources\nbuilding extension \"Orange.data._valuecount\" sources\nbuilding extension \"Orange.data._contingency\" sources\nbuilding extension \"Orange.data._io\" sources\nbuilding extension \"Orange.data._variable\" sources\nbuilding extension \"Orange.distance._distance\" sources\nbuilding extension \"Orange.preprocess._discretize\" sources\nbuilding extension \"Orange.preprocess._relieff\" sources\nbuilding extension \"Orange.projection._som\" sources\nbuilding extension \"Orange.widgets.utils._grid_density\" sources\nbuild_src: building npy-pkg config files\ncreating Orange3.egg-info\nwriting Orange3.egg-info/PKG-INFO\nwriting dependency_links to Orange3.egg-info/dependency_links.txt\nwriting entry points to Orange3.egg-info/entry_points.txt\nwriting requirements to Orange3.egg-info/requires.txt\nwriting top-level names to Orange3.egg-info/top_level.txt\nwriting manifest file 'Orange3.egg-info/SOURCES.txt'\nreading manifest file 'Orange3.egg-info/SOURCES.txt'\nreading manifest template 'MANIFEST.in'\nwarning: no files found matching 'README*' under directory 'Orange'\nwarning: no files found matching 'LICENSE' under directory 'Orange'\nwarning: no files found matching '*.basket' under directory 'Orange/datasets'\nwarning: no files found matching '*.js' under directory 'Orange/widgets'\nwarning: no files found matching '*.css' under directory 'Orange/widgets'\nwarning: no files found matching '*.html' under directory 'Orange/widgets'\nwarning: no files found matching 'COPYING'\nadding license file 'LICENSE'\nwriting manifest file 'Orange3.egg-info/SOURCES.txt'\nrunning build_ext\ncustomize UnixCCompiler\nC compiler: cc -pthread -Wno-unused-result -Wsign-compare -Wunreachable-code -DNDEBUG -g -fwrapv -O3 -Wall -fPIC -I/tools/deps/include -I/tools/deps/include/ncursesw -fPIC\n\ncreating /tmp/tmpr2ndaxzs/tmp\ncreating /tmp/tmpr2ndaxzs/tmp/tmpr2ndaxzs\ncompile options: '-MMD -MF /tmp/tmpr2ndaxzs/file.c.d -c'\ncc: /tmp/tmpr2ndaxzs/file.c\ncustomize UnixCCompiler using build_ext\ncustomize UnixCCompiler\nC compiler: cc -pthread -Wno-unused-result -Wsign-compare -Wunreachable-code -DNDEBUG -g -fwrapv -O3 -Wall -fPIC -I/tools/deps/include -I/tools/deps/include/ncursesw -fPIC\n\ncreating /tmp/tmpeqvs2114/tmp\ncreating /tmp/tmpeqvs2114/tmp/tmpeqvs2114\ncompile options: '-MMD -MF /tmp/tmpeqvs2114/file.c.d -c'\ncc: /tmp/tmpeqvs2114/file.c\ncustomize UnixCCompiler using build_ext\nCreating /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_06f7630d8753143f4c7f6b029c7dc79264d76318/.venv/lib/python3.7/site-packages/Orange3.egg-link (link to .)\nAdding Orange3 3.24.0.dev0+06f7630 to easy-install.pth file\nInstalling orange-canvas script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_06f7630d8753143f4c7f6b029c7dc79264d76318/.venv/bin\n\nInstalled /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_06f7630d8753143f4c7f6b029c7dc79264d76318\nProcessing dependencies for Orange3==3.24.0.dev0+06f7630\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_06f7630d8753143f4c7f6b029c7dc79264d76318/.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_06f7630d8753143f4c7f6b029c7dc79264d76318/.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_06f7630d8753143f4c7f6b029c7dc79264d76318/.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_06f7630d8753143f4c7f6b029c7dc79264d76318/.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_06f7630d8753143f4c7f6b029c7dc79264d76318/.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_06f7630d8753143f4c7f6b029c7dc79264d76318/.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_06f7630d8753143f4c7f6b029c7dc79264d76318/.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_06f7630d8753143f4c7f6b029c7dc79264d76318/.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_06f7630d8753143f4c7f6b029c7dc79264d76318/.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_06f7630d8753143f4c7f6b029c7dc79264d76318/.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_06f7630d8753143f4c7f6b029c7dc79264d76318/.venv/bin\nInstalling pip3 script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_06f7630d8753143f4c7f6b029c7dc79264d76318/.venv/bin\nInstalling pip3.10 script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_06f7630d8753143f4c7f6b029c7dc79264d76318/.venv/bin\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_06f7630d8753143f4c7f6b029c7dc79264d76318/.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_06f7630d8753143f4c7f6b029c7dc79264d76318/.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_06f7630d8753143f4c7f6b029c7dc79264d76318/.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_06f7630d8753143f4c7f6b029c7dc79264d76318/.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_06f7630d8753143f4c7f6b029c7dc79264d76318/.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_06f7630d8753143f4c7f6b029c7dc79264d76318/.venv/bin\nInstalling f2py3 script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_06f7630d8753143f4c7f6b029c7dc79264d76318/.venv/bin\nInstalling f2py3.7 script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_06f7630d8753143f4c7f6b029c7dc79264d76318/.venv/bin\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_06f7630d8753143f4c7f6b029c7dc79264d76318/.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_06f7630d8753143f4c7f6b029c7dc79264d76318/.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_06f7630d8753143f4c7f6b029c7dc79264d76318/.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_06f7630d8753143f4c7f6b029c7dc79264d76318/.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_06f7630d8753143f4c7f6b029c7dc79264d76318/.venv/bin\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_06f7630d8753143f4c7f6b029c7dc79264d76318/.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_06f7630d8753143f4c7f6b029c7dc79264d76318/.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_06f7630d8753143f4c7f6b029c7dc79264d76318/.venv/bin\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_06f7630d8753143f4c7f6b029c7dc79264d76318/.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_06f7630d8753143f4c7f6b029c7dc79264d76318/.venv/bin\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_06f7630d8753143f4c7f6b029c7dc79264d76318/.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_06f7630d8753143f4c7f6b029c7dc79264d76318/.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_06f7630d8753143f4c7f6b029c7dc79264d76318/.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_06f7630d8753143f4c7f6b029c7dc79264d76318/.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_06f7630d8753143f4c7f6b029c7dc79264d76318/.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_06f7630d8753143f4c7f6b029c7dc79264d76318/.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_06f7630d8753143f4c7f6b029c7dc79264d76318/.venv/bin\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_06f7630d8753143f4c7f6b029c7dc79264d76318/.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_06f7630d8753143f4c7f6b029c7dc79264d76318/.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_06f7630d8753143f4c7f6b029c7dc79264d76318/.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_06f7630d8753143f4c7f6b029c7dc79264d76318/.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_06f7630d8753143f4c7f6b029c7dc79264d76318/.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_06f7630d8753143f4c7f6b029c7dc79264d76318/.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_06f7630d8753143f4c7f6b029c7dc79264d76318/.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_06f7630d8753143f4c7f6b029c7dc79264d76318/.venv/bin\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_06f7630d8753143f4c7f6b029c7dc79264d76318/.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_06f7630d8753143f4c7f6b029c7dc79264d76318/.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_06f7630d8753143f4c7f6b029c7dc79264d76318/.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_06f7630d8753143f4c7f6b029c7dc79264d76318/.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_06f7630d8753143f4c7f6b029c7dc79264d76318/.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_06f7630d8753143f4c7f6b029c7dc79264d76318/.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_06f7630d8753143f4c7f6b029c7dc79264d76318/.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_06f7630d8753143f4c7f6b029c7dc79264d76318/.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_06f7630d8753143f4c7f6b029c7dc79264d76318/.venv/bin\nInstalling pyftmerge script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_06f7630d8753143f4c7f6b029c7dc79264d76318/.venv/bin\nInstalling pyftsubset script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_06f7630d8753143f4c7f6b029c7dc79264d76318/.venv/bin\nInstalling ttx script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_06f7630d8753143f4c7f6b029c7dc79264d76318/.venv/bin\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_06f7630d8753143f4c7f6b029c7dc79264d76318/.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_06f7630d8753143f4c7f6b029c7dc79264d76318/.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_06f7630d8753143f4c7f6b029c7dc79264d76318/.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_06f7630d8753143f4c7f6b029c7dc79264d76318/.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_06f7630d8753143f4c7f6b029c7dc79264d76318/.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_06f7630d8753143f4c7f6b029c7dc79264d76318/.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_06f7630d8753143f4c7f6b029c7dc79264d76318/.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_06f7630d8753143f4c7f6b029c7dc79264d76318/.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_06f7630d8753143f4c7f6b029c7dc79264d76318/.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_06f7630d8753143f4c7f6b029c7dc79264d76318/.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_06f7630d8753143f4c7f6b029c7dc79264d76318/.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_06f7630d8753143f4c7f6b029c7dc79264d76318/.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_06f7630d8753143f4c7f6b029c7dc79264d76318/.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_06f7630d8753143f4c7f6b029c7dc79264d76318/.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_06f7630d8753143f4c7f6b029c7dc79264d76318/.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_06f7630d8753143f4c7f6b029c7dc79264d76318/.venv/lib/python3.7/site-packages\nFinished processing dependencies for Orange3==3.24.0.dev0+06f7630\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 113ms\nPrepared 18 packages in 2ms\nInstalled 18 packages in 112ms\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 38 packages in 177ms\nInstalled 33 packages in 625ms\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 + 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 + more-itertools==9.1.0\n + networkx==2.6.3\n + opentsne==1.0.1\n + pandas==1.3.5\n + pip==24.0\n + pycparser==2.21\n + python-dateutil==2.9.0.post0\n + python-louvain==0.16\n + pytz==2024.2\n + pyyaml==6.0.1\n + requests==2.31.0\n + scikit-learn==1.0.2\n + scipy==1.7.3\n + secretstorage==3.3.3\n + serverfiles==0.3.1\n + six==1.17.0\n + threadpoolctl==3.1.0\n + urllib3==2.0.7\n + xlrd==2.0.1\n + xlsxwriter==3.2.0\nResolved 34 packages in 152ms\nInstalled 19 packages in 136ms\n + anyqt==0.2.0\n + attrs==24.2.0\n + cattrs==23.1.2\n + commonmark==0.9.1\n + cycler==0.11.0\n + dictdiffer==0.9.0\n + docutils==0.20.1\n + fonttools==4.38.0\n + kiwisolver==1.4.5\n + matplotlib==3.5.3\n + orange-canvas-core==0.1.35\n + orange-widget-base==4.22.0\n + pillow==9.5.0\n + platformdirs==4.0.0\n + pyparsing==3.1.4\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 4ms\nInstalled 2 packages in 28ms\n + psycopg2==2.9.9\n + pymssql==2.3.2\nAudited 1 package in 1ms\nAudited 2 packages in 33ms\nOrange/classification/_simple_tree.c: In function ‘gain_ratio_c’:\nOrange/classification/_simple_tree.c:63:9: warning: implicit declaration of function ‘qsort_r’; did you mean ‘qsort’? [-Wimplicit-function-declaration]\n   63 |         qsort_r(base, nel, size, compar, thunk)\n      |         ^~~~~~~\nOrange/classification/_simple_tree.c:147:9: note: in expansion of macro ‘QSORT_R_FUNC’\n  147 |         QSORT_R_FUNC(examples, size, sizeof(struct Example), (void*) &compar_attr, compar_examples);\n      |         ^~~~~~~~~~~~\nIn file included from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_06f7630d8753143f4c7f6b029c7dc79264d76318/.venv/lib/python3.7/site-packages/numpy/core/include/numpy/ndarraytypes.h:1830,\n                 from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_06f7630d8753143f4c7f6b029c7dc79264d76318/.venv/lib/python3.7/site-packages/numpy/core/include/numpy/ndarrayobject.h:12,\n                 from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_06f7630d8753143f4c7f6b029c7dc79264d76318/.venv/lib/python3.7/site-packages/numpy/core/include/numpy/arrayobject.h:4,\n                 from Orange/classification/_tree_scorers.c:567:\n/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_06f7630d8753143f4c7f6b029c7dc79264d76318/.venv/lib/python3.7/site-packages/numpy/core/include/numpy/npy_1_7_deprecated_api.h:17:2: warning: #warning \"Using deprecated NumPy API, disable it with \" \"#define NPY_NO_DEPRECATED_API NPY_1_7_API_VERSION\" [-Wcpp]\n   17 | #warning \"Using deprecated NumPy API, disable it with \" \\\n      |  ^~~~~~~\nOrange/classification/_tree_scorers.c: In function ‘__pyx_pf_6Orange_14classification_13_tree_scorers_2find_threshold_entropy’:\nOrange/classification/_tree_scorers.c:3115:27: warning: comparison of integer expressions of different signedness: ‘unsigned int’ and ‘int’ [-Wsign-compare]\n 3115 |   __pyx_t_7 = ((__pyx_v_N <= __pyx_v_min_leaf) != 0);\n      |                           ^~\nOrange/classification/_tree_scorers.c: In function ‘__pyx_pf_6Orange_14classification_13_tree_scorers_6find_threshold_MSE’:\nOrange/classification/_tree_scorers.c:4619:27: warning: comparison of integer expressions of different signedness: ‘unsigned int’ and ‘int’ [-Wsign-compare]\n 4619 |   __pyx_t_1 = ((__pyx_v_N <= __pyx_v_min_leaf) != 0);\n      |                           ^~\nOrange/classification/_tree_scorers.c: In function ‘__pyx_pf_6Orange_14classification_13_tree_scorers_8find_binarization_MSE’:\nOrange/classification/_tree_scorers.c:5396:43: warning: comparison of integer expressions of different signedness: ‘unsigned int’ and ‘int’ [-Wsign-compare]\n 5396 |           for (__pyx_t_22 = 0; __pyx_t_22 < __pyx_t_21; __pyx_t_22+=1) {\n      |                                           ^\nOrange/classification/_tree_scorers.c:5517:39: warning: comparison of integer expressions of different signedness: ‘unsigned int’ and ‘int’ [-Wsign-compare]\n 5517 |           __pyx_t_27 = ((__pyx_v_left >= __pyx_v_min_leaf) != 0);\n      |                                       ^~\nOrange/classification/_tree_scorers.c:5523:53: warning: comparison of integer expressions of different signedness: ‘unsigned int’ and ‘int’ [-Wsign-compare]\n 5523 |           __pyx_t_27 = (((__pyx_v_N - __pyx_v_left) >= __pyx_v_min_leaf) != 0);\n      |                                                     ^~\nOrange/classification/_tree_scorers.c: In function ‘__pyx_pf_6Orange_14classification_13_tree_scorers_14compute_predictions_csr’:\nOrange/classification/_tree_scorers.c:7051:41: warning: comparison of integer expressions of different signedness: ‘unsigned int’ and ‘int’ [-Wsign-compare]\n 7051 |         for (__pyx_t_11 = 0; __pyx_t_11 < __pyx_t_10; __pyx_t_11+=1) {\n      |                                         ^\nOrange/classification/_tree_scorers.c: In function ‘__pyx_pf_6Orange_14classification_13_tree_scorers_16compute_predictions_csc’:\nOrange/classification/_tree_scorers.c:7741:41: warning: comparison of integer expressions of different signedness: ‘unsigned int’ and ‘int’ [-Wsign-compare]\n 7741 |         for (__pyx_t_11 = 0; __pyx_t_11 < __pyx_t_10; __pyx_t_11+=1) {\n      |                                         ^\nOrange/classification/_tree_scorers.c:7801:143: warning: comparison of integer expressions of different signedness: ‘__pyx_t_5numpy_int32_t’ {aka ‘int’} and ‘unsigned int’ [-Wsign-compare]\n 7801 |               __pyx_t_17 = (((*((__pyx_t_5numpy_int32_t *) ( /* dim=0 */ (__pyx_v_indices.data + __pyx_t_18 * __pyx_v_indices.strides[0]) ))) != __pyx_v_i) != 0);\n      |                                                                                                                                               ^~\nIn file included from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_06f7630d8753143f4c7f6b029c7dc79264d76318/.venv/lib/python3.7/site-packages/numpy/core/include/numpy/ndarraytypes.h:1830,\n                 from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_06f7630d8753143f4c7f6b029c7dc79264d76318/.venv/lib/python3.7/site-packages/numpy/core/include/numpy/ndarrayobject.h:12,\n                 from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_06f7630d8753143f4c7f6b029c7dc79264d76318/.venv/lib/python3.7/site-packages/numpy/core/include/numpy/arrayobject.h:4,\n                 from Orange/data/_valuecount.c:567:\n/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_06f7630d8753143f4c7f6b029c7dc79264d76318/.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_06f7630d8753143f4c7f6b029c7dc79264d76318/.venv/lib/python3.7/site-packages/numpy/core/include/numpy/ndarraytypes.h:1830,\n                 from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_06f7630d8753143f4c7f6b029c7dc79264d76318/.venv/lib/python3.7/site-packages/numpy/core/include/numpy/ndarrayobject.h:12,\n                 from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_06f7630d8753143f4c7f6b029c7dc79264d76318/.venv/lib/python3.7/site-packages/numpy/core/include/numpy/arrayobject.h:4,\n                 from Orange/data/_contingency.c:598:\n/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_06f7630d8753143f4c7f6b029c7dc79264d76318/.venv/lib/python3.7/site-packages/numpy/core/include/numpy/npy_1_7_deprecated_api.h:17:2: warning: #warning \"Using deprecated NumPy API, disable it with \" \"#define NPY_NO_DEPRECATED_API NPY_1_7_API_VERSION\" [-Wcpp]\n   17 | #warning \"Using deprecated NumPy API, disable it with \" \\\n      |  ^~~~~~~\nOrange/data/_io.c: In function ‘__pyx_pf_6Orange_4data_3_io_sparse_read_float.constprop’:\nOrange/data/_io.c:3881:24: warning: ‘__pyx_v_decs’ may be used uninitialized in this function [-Wmaybe-uninitialized]\n 3881 |           __pyx_v_decs = (__pyx_v_decs / 10.0);\n      |           ~~~~~~~~~~~~~^~~~~~~~~~~~~~~~~~~~~~~\nOrange/data/_io.c:4070:24: warning: ‘__pyx_v_value’ may be used uninitialized in this function [-Wmaybe-uninitialized]\n 4070 |           __pyx_t_10 = PyFloat_FromDouble(__pyx_v_value); if (unlikely(!__pyx_t_10)) __PYX_ERR(0, 221, __pyx_L5_error)\n      |                        ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\nOrange/data/_io.c:480:40: warning: ‘__pyx_v_attr_index’ may be used uninitialized in this function [-Wmaybe-uninitialized]\n  480 |   #define PyInt_FromLong               PyLong_FromLong\n      |                                        ^~~~~~~~~~~~~~~\nOrange/data/_io.c:1301:7: note: ‘__pyx_v_attr_index’ was declared here\n 1301 |   int __pyx_v_attr_index;\n      |       ^~~~~~~~~~~~~~~~~~\nOrange/data/_io.c:2935:25: warning: ‘__pyx_v_atomp’ may be used uninitialized in this function [-Wmaybe-uninitialized]\n 2935 |           __pyx_v_atomp = (__pyx_v_atomp + 1);\n      |           ~~~~~~~~~~~~~~^~~~~~~~~~~~~~~~~~~~~\nIn file included from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_06f7630d8753143f4c7f6b029c7dc79264d76318/.venv/lib/python3.7/site-packages/numpy/core/include/numpy/ndarraytypes.h:1830,\n                 from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_06f7630d8753143f4c7f6b029c7dc79264d76318/.venv/lib/python3.7/site-packages/numpy/core/include/numpy/ndarrayobject.h:12,\n                 from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_06f7630d8753143f4c7f6b029c7dc79264d76318/.venv/lib/python3.7/site-packages/numpy/core/include/numpy/arrayobject.h:4,\n                 from Orange/distance/_distance.c:567:\n/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_06f7630d8753143f4c7f6b029c7dc79264d76318/.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_06f7630d8753143f4c7f6b029c7dc79264d76318/.venv/lib/python3.7/site-packages/numpy/core/include/numpy/ndarraytypes.h:1830,\n                 from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_06f7630d8753143f4c7f6b029c7dc79264d76318/.venv/lib/python3.7/site-packages/numpy/core/include/numpy/ndarrayobject.h:12,\n                 from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_06f7630d8753143f4c7f6b029c7dc79264d76318/.venv/lib/python3.7/site-packages/numpy/core/include/numpy/arrayobject.h:4,\n                 from Orange/preprocess/_discretize.c:567:\n/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_06f7630d8753143f4c7f6b029c7dc79264d76318/.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_06f7630d8753143f4c7f6b029c7dc79264d76318/.venv/lib/python3.7/site-packages/numpy/core/include/numpy/ndarraytypes.h:1830,\n                 from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_06f7630d8753143f4c7f6b029c7dc79264d76318/.venv/lib/python3.7/site-packages/numpy/core/include/numpy/ndarrayobject.h:12,\n                 from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_06f7630d8753143f4c7f6b029c7dc79264d76318/.venv/lib/python3.7/site-packages/numpy/core/include/numpy/arrayobject.h:4,\n                 from Orange/preprocess/_relieff.cpp:581:\n/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_06f7630d8753143f4c7f6b029c7dc79264d76318/.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_06f7630d8753143f4c7f6b029c7dc79264d76318/.venv/lib/python3.7/site-packages/numpy/core/include/numpy/ndarraytypes.h:1830,\n                 from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_06f7630d8753143f4c7f6b029c7dc79264d76318/.venv/lib/python3.7/site-packages/numpy/core/include/numpy/ndarrayobject.h:12,\n                 from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_06f7630d8753143f4c7f6b029c7dc79264d76318/.venv/lib/python3.7/site-packages/numpy/core/include/numpy/arrayobject.h:4,\n                 from Orange/projection/_som.c:598:\n/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_06f7630d8753143f4c7f6b029c7dc79264d76318/.venv/lib/python3.7/site-packages/numpy/core/include/numpy/npy_1_7_deprecated_api.h:17:2: warning: #warning \"Using deprecated NumPy API, disable it with \" \"#define NPY_NO_DEPRECATED_API NPY_1_7_API_VERSION\" [-Wcpp]\n   17 | #warning \"Using deprecated NumPy API, disable it with \" \\\n      |  ^~~~~~~\nOrange/projection/_som.c: In function ‘__pyx_pw_6Orange_10projection_4_som_5update_hex’:\nOrange/projection/_som.c:3655:7: warning: ‘__pyx_v_win_y’ may be used uninitialized in this function [-Wmaybe-uninitialized]\n 3655 |   int __pyx_v_win_y;\n      |       ^~~~~~~~~~~~~\nOrange/projection/_som.c:3654:7: warning: ‘__pyx_v_win_x’ may be used uninitialized in this function [-Wmaybe-uninitialized]\n 3654 |   int __pyx_v_win_x;\n      |       ^~~~~~~~~~~~~\nOrange/projection/_som.c: In function ‘__pyx_pw_6Orange_10projection_4_som_7get_winners_sparse’:\nOrange/projection/_som.c:4547:223: warning: ‘__pyx_v_win_y’ may be used uninitialized in this function [-Wmaybe-uninitialized]\n 4547 |           *__Pyx_BufPtrStrided2d(__pyx_t_5numpy_int16_t *, __pyx_pybuffernd_winners.rcbuffer->pybuffer.buf, __pyx_t_38, __pyx_pybuffernd_winners.diminfo[0].strides, __pyx_t_39, __pyx_pybuffernd_winners.diminfo[1].strides) = __pyx_v_win_y;\n      |           ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^~~~~~~~~~~~~~~\nOrange/projection/_som.c:4114:7: note: ‘__pyx_v_win_y’ was declared here\n 4114 |   int __pyx_v_win_y;\n      |       ^~~~~~~~~~~~~\nOrange/projection/_som.c:4536:223: warning: ‘__pyx_v_win_x’ may be used uninitialized in this function [-Wmaybe-uninitialized]\n 4536 |           *__Pyx_BufPtrStrided2d(__pyx_t_5numpy_int16_t *, __pyx_pybuffernd_winners.rcbuffer->pybuffer.buf, __pyx_t_36, __pyx_pybuffernd_winners.diminfo[0].strides, __pyx_t_37, __pyx_pybuffernd_winners.diminfo[1].strides) = __pyx_v_win_x;\n      |           ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^~~~~~~~~~~~~~~\nOrange/projection/_som.c:4113:7: note: ‘__pyx_v_win_x’ was declared here\n 4113 |   int __pyx_v_win_x;\n      |       ^~~~~~~~~~~~~\nOrange/projection/_som.c: In function ‘__pyx_pf_6Orange_10projection_4_som_8update_sparse.constprop’:\nOrange/projection/_som.c:4746:7: warning: ‘__pyx_v_win_y’ may be used uninitialized in this function [-Wmaybe-uninitialized]\n 4746 |   int __pyx_v_win_y;\n      |       ^~~~~~~~~~~~~\nOrange/projection/_som.c:4745:7: warning: ‘__pyx_v_win_x’ may be used uninitialized in this function [-Wmaybe-uninitialized]\n 4745 |   int __pyx_v_win_x;\n      |       ^~~~~~~~~~~~~\nOrange/projection/_som.c: In function ‘__pyx_pw_6Orange_10projection_4_som_3update’:\nOrange/projection/_som.c:3141:7: warning: ‘__pyx_v_win_y’ may be used uninitialized in this function [-Wmaybe-uninitialized]\n 3141 |   int __pyx_v_win_y;\n      |       ^~~~~~~~~~~~~\nOrange/projection/_som.c: In function ‘__pyx_pw_6Orange_10projection_4_som_1get_winners’:\nOrange/projection/_som.c:2964:223: warning: ‘__pyx_v_win_y’ may be used uninitialized in this function [-Wmaybe-uninitialized]\n 2964 |           *__Pyx_BufPtrStrided2d(__pyx_t_5numpy_int16_t *, __pyx_pybuffernd_winners.rcbuffer->pybuffer.buf, __pyx_t_27, __pyx_pybuffernd_winners.diminfo[0].strides, __pyx_t_28, __pyx_pybuffernd_winners.diminfo[1].strides) = __pyx_v_win_y;\n      |           ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^~~~~~~~~~~~~~~\nOrange/projection/_som.c:2665:7: note: ‘__pyx_v_win_y’ was declared here\n 2665 |   int __pyx_v_win_y;\n      |       ^~~~~~~~~~~~~\nOrange/projection/_som.c:2953:223: warning: ‘__pyx_v_win_x’ may be used uninitialized in this function [-Wmaybe-uninitialized]\n 2953 |           *__Pyx_BufPtrStrided2d(__pyx_t_5numpy_int16_t *, __pyx_pybuffernd_winners.rcbuffer->pybuffer.buf, __pyx_t_25, __pyx_pybuffernd_winners.diminfo[0].strides, __pyx_t_26, __pyx_pybuffernd_winners.diminfo[1].strides) = __pyx_v_win_x;\n      |           ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^~~~~~~~~~~~~~~\nOrange/projection/_som.c:2664:7: note: ‘__pyx_v_win_x’ was declared here\n 2664 |   int __pyx_v_win_x;\n      |       ^~~~~~~~~~~~~\nOrange/projection/_som.c: In function ‘__pyx_pf_6Orange_10projection_4_som_10update_sparse_hex.constprop’:\nOrange/projection/_som.c:5515:7: warning: ‘__pyx_v_win_y’ may be used uninitialized in this function [-Wmaybe-uninitialized]\n 5515 |   int __pyx_v_win_y;\n      |       ^~~~~~~~~~~~~\nOrange/projection/_som.c:5514:7: warning: ‘__pyx_v_win_x’ may be used uninitialized in this function [-Wmaybe-uninitialized]\n 5514 |   int __pyx_v_win_x;\n      |       ^~~~~~~~~~~~~\n/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_06f7630d8753143f4c7f6b029c7dc79264d76318/.venv/lib/python3.7/site-packages/setuptools/command/install.py:37: SetuptoolsDeprecationWarning: setup.py install is deprecated. Use build and pip and other standards-based tools.\n  setuptools.SetuptoolsDeprecationWarning,\n/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_06f7630d8753143f4c7f6b029c7dc79264d76318/.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_06f7630d8753143f4c7f6b029c7dc79264d76318/.venv/lib/python3.7/site-packages/setuptools/command/install.py:37: SetuptoolsDeprecationWarning: setup.py install is deprecated. Use build and pip and other standards-based tools.\n  setuptools.SetuptoolsDeprecationWarning,\n/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_06f7630d8753143f4c7f6b029c7dc79264d76318/.venv/lib/python3.7/site-packages/setuptools/command/egg_info.py:628: SetuptoolsDeprecationWarning: Custom 'build_py' does not implement 'get_data_files_without_manifest'.\nPlease extend command classes from setuptools instead of distutils.\n  SetuptoolsDeprecationWarning\n",
    "new_commit_res_code": 1,
    "new_commit_res_stdout": "============================= test session starts ==============================\nplatform linux -- Python 3.7.9, pytest-7.4.4, pluggy-1.2.0\nrootdir: /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_06f7630d8753143f4c7f6b029c7dc79264d76318\ncollected 28 items\n\nr2e_tests/test_1.py sssE........................                         [100%]\n\n==================================== ERRORS ====================================\n_______________________ ERROR at setup of test_filename ________________________\nfile /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_06f7630d8753143f4c7f6b029c7dc79264d76318/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_06f7630d8753143f4c7f6b029c7dc79264d76318/Orange/tests/__init__.py:65\n==================================== PASSES ====================================\n=========================== short test summary info ============================\nPASSED r2e_tests/test_1.py::TestOWBoxPlot::test_apply_sorting_group\nPASSED r2e_tests/test_1.py::TestOWBoxPlot::test_apply_sorting_vars\nPASSED r2e_tests/test_1.py::TestOWBoxPlot::test_attribute_combinations\nPASSED r2e_tests/test_1.py::TestOWBoxPlot::test_box_order_when_missing_stats\nPASSED r2e_tests/test_1.py::TestOWBoxPlot::test_continuous_metas\nPASSED r2e_tests/test_1.py::TestOWBoxPlot::test_dont_show_hidden_attrs\nPASSED r2e_tests/test_1.py::TestOWBoxPlot::test_empty_groups\nPASSED r2e_tests/test_1.py::TestOWBoxPlot::test_image_export\nPASSED r2e_tests/test_1.py::TestOWBoxPlot::test_input_data\nPASSED r2e_tests/test_1.py::TestOWBoxPlot::test_input_data_missings_cont_group_var\nPASSED r2e_tests/test_1.py::TestOWBoxPlot::test_input_data_missings_cont_no_group_var\nPASSED r2e_tests/test_1.py::TestOWBoxPlot::test_input_data_missings_disc_group_var\nPASSED r2e_tests/test_1.py::TestOWBoxPlot::test_input_data_missings_disc_no_group_var\nPASSED r2e_tests/test_1.py::TestOWBoxPlot::test_label_overlap\nPASSED r2e_tests/test_1.py::TestOWBoxPlot::test_minimum_size\nPASSED r2e_tests/test_1.py::TestOWBoxPlot::test_msg_base_class\nPASSED r2e_tests/test_1.py::TestOWBoxPlot::test_outputs\nPASSED r2e_tests/test_1.py::TestOWBoxPlot::test_primitive_metas\nPASSED r2e_tests/test_1.py::TestOWBoxPlot::test_saved_selection\nPASSED r2e_tests/test_1.py::TestOWBoxPlot::test_sorting_disc_group_var\nPASSED r2e_tests/test_1.py::TestOWBoxPlot::test_stretching\nPASSED r2e_tests/test_1.py::TestOWBoxPlot::test_unconditional_commit_on_new_signal\nPASSED r2e_tests/test_1.py::TestOWBoxPlot::test_value_all_missing_for_group\nPASSED r2e_tests/test_1.py::TestUtils::test\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\n==================== 24 passed, 3 skipped, 1 error in 5.83s ====================\nWidgets left: 0    Max widgets: 1818 \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_06f7630d8753143f4c7f6b029c7dc79264d76318\ncollected 28 items\n\nr2e_tests/test_1.py sssE.....F..................                         [100%]\n\n==================================== ERRORS ====================================\n_______________________ ERROR at setup of test_filename ________________________\nfile /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_06f7630d8753143f4c7f6b029c7dc79264d76318/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_06f7630d8753143f4c7f6b029c7dc79264d76318/Orange/tests/__init__.py:65\n=================================== FAILURES ===================================\n__________________ TestOWBoxPlot.test_dont_show_hidden_attrs ___________________\n\nself = <r2e_tests.test_1.TestOWBoxPlot testMethod=test_dont_show_hidden_attrs>\n\n    def test_dont_show_hidden_attrs(self):\n        \"\"\"Check widget's data\"\"\"\n        iris = Table(\"iris\")\n        iris.domain[\"iris\"].attributes[\"hidden\"] = True\n        iris.domain[\"petal length\"].attributes[\"hidden\"] = True\n        self.send_signal(self.widget.Inputs.data, iris)\n>       self.assertEqual(len(self.widget.attrs), 3)\nE       AssertionError: 5 != 3\n\nr2e_tests/test_1.py:56: AssertionError\n==================================== PASSES ====================================\n=========================== short test summary info ============================\nPASSED r2e_tests/test_1.py::TestOWBoxPlot::test_apply_sorting_group\nPASSED r2e_tests/test_1.py::TestOWBoxPlot::test_apply_sorting_vars\nPASSED r2e_tests/test_1.py::TestOWBoxPlot::test_attribute_combinations\nPASSED r2e_tests/test_1.py::TestOWBoxPlot::test_box_order_when_missing_stats\nPASSED r2e_tests/test_1.py::TestOWBoxPlot::test_continuous_metas\nPASSED r2e_tests/test_1.py::TestOWBoxPlot::test_empty_groups\nPASSED r2e_tests/test_1.py::TestOWBoxPlot::test_image_export\nPASSED r2e_tests/test_1.py::TestOWBoxPlot::test_input_data\nPASSED r2e_tests/test_1.py::TestOWBoxPlot::test_input_data_missings_cont_group_var\nPASSED r2e_tests/test_1.py::TestOWBoxPlot::test_input_data_missings_cont_no_group_var\nPASSED r2e_tests/test_1.py::TestOWBoxPlot::test_input_data_missings_disc_group_var\nPASSED r2e_tests/test_1.py::TestOWBoxPlot::test_input_data_missings_disc_no_group_var\nPASSED r2e_tests/test_1.py::TestOWBoxPlot::test_label_overlap\nPASSED r2e_tests/test_1.py::TestOWBoxPlot::test_minimum_size\nPASSED r2e_tests/test_1.py::TestOWBoxPlot::test_msg_base_class\nPASSED r2e_tests/test_1.py::TestOWBoxPlot::test_outputs\nPASSED r2e_tests/test_1.py::TestOWBoxPlot::test_primitive_metas\nPASSED r2e_tests/test_1.py::TestOWBoxPlot::test_saved_selection\nPASSED r2e_tests/test_1.py::TestOWBoxPlot::test_sorting_disc_group_var\nPASSED r2e_tests/test_1.py::TestOWBoxPlot::test_stretching\nPASSED r2e_tests/test_1.py::TestOWBoxPlot::test_unconditional_commit_on_new_signal\nPASSED r2e_tests/test_1.py::TestOWBoxPlot::test_value_all_missing_for_group\nPASSED r2e_tests/test_1.py::TestUtils::test\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::TestOWBoxPlot::test_dont_show_hidden_attrs - Asse...\n=============== 1 failed, 23 passed, 3 skipped, 1 error in 5.01s ===============\nWidgets left: 0    Max widgets: 1818 \n\n",
    "old_commit_res_stderr": ""
}
Orange/widgets/visualize/owboxplot.py Orange/widgets/visualize/tests/test_owboxplot.py
We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
[ISSUE]
**Title:** BoxPlot Widget Displays Hidden Attributes Unexpectedly

**Description:**
When certain attributes in the dataset are marked as hidden, the BoxPlot widget still displays them. This results in cluttered visualizations and does not align with the intended behavior of hiding specific attributes.

**Example Buggy Code:**
```python
iris = Table("iris")
iris.domain["iris"].attributes["hidden"] = True
iris.domain["petal length"].attributes["hidden"] = True
self.send_signal(self.widget.Inputs.data, iris)
```
In this example, the attributes "iris" and "petal length" are set to hidden. After sending the data to the widget, these hidden attributes should not appear in the BoxPlot visualization.

**Expected Behavior:**
Only non-hidden attributes should be displayed in the BoxPlot widget. In this case, after hiding two attributes, only three should be visible.

**Actual Behavior:**
All five attributes, including the ones marked as hidden, are displayed in the BoxPlot widget. This causes the test to fail with an assertion error, indicating that the number of displayed attributes does not match the expected count.

[/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/owboxplot.py
import math
from itertools import chain
import numpy as np

from AnyQt.QtWidgets import (
    QGraphicsView, QGraphicsScene, QGraphicsItem, QGraphicsSimpleTextItem,
    QGraphicsTextItem, QGraphicsItemGroup, QGraphicsLineItem,
    QGraphicsPathItem, QGraphicsRectItem, QSizePolicy
)
from AnyQt.QtGui import QPen, QColor, QBrush, QPainterPath, QPainter, QFont
from AnyQt.QtCore import Qt, QEvent, QRectF, QSize

import scipy.special
from scipy.stats import f_oneway, chi2_contingency

import Orange.data
from Orange.data.filter import FilterDiscrete, FilterContinuous, Values
from Orange.statistics import contingency, distribution

from Orange.widgets import widget, gui
from Orange.widgets.settings import (Setting, DomainContextHandler,
                                     ContextSetting)
from Orange.widgets.utils.itemmodels import VariableListModel
from Orange.widgets.utils.annotated_data import (create_annotated_table,
                                                 ANNOTATED_DATA_SIGNAL_NAME)
from Orange.widgets.utils.widgetpreview import WidgetPreview
from Orange.widgets.widget import Input, Output


def compute_scale(min_, max_):
    if min_ == max_:
        return math.floor(min_), 1
    magnitude = int(3 * math.log10(abs(max_ - min_)) + 1)
    if magnitude % 3 == 0:
        first_place = 1
    elif magnitude % 3 == 1:
        first_place = 2
    else:
        first_place = 5
    magnitude = magnitude // 3 - 1
    step = first_place * pow(10, magnitude)
    first_val = math.ceil(min_ / step) * step
    return first_val, step


def _quantiles(a, freq, q, interpolation="midpoint"):
    """
    Somewhat like np.quantiles, but with explicit sample frequencies.

    * Only 'higher', 'lower' and 'midpoint' interpolation.
    * `a` MUST be sorted.
    """
    a = np.asarray(a)
    freq = np.asarray(freq)
    assert a.size > 0 and a.size == freq.size
    cumdist = np.cumsum(freq)
    cumdist /= cumdist[-1]

    if interpolation == "midpoint":  # R quantile(..., type=2)
        left = np.searchsorted(cumdist, q, side="left")
        right = np.searchsorted(cumdist, q, side="right")
        # no mid point for the right most position
        np.clip(right, 0, a.size - 1, out=right)
        # right and left will be different only on the `q` boundaries
        # (excluding the right most sample)
        return (a[left] + a[right]) / 2
    elif interpolation == "higher":  # R quantile(... type=1)
        right = np.searchsorted(cumdist, q, side="right")
        np.clip(right, 0, a.size - 1, out=right)
        return a[right]
    elif interpolation == "lower":
        left = np.searchsorted(cumdist, q, side="left")
        return a[left]
    else:  # pragma: no cover
        raise ValueError("invalid interpolation: '{}'".format(interpolation))


class BoxData:
    def __init__(self, dist, attr, group_val_index=None, group_var=None):
        self.dist = dist
        self.n = n = np.sum(dist[1])
        if n == 0:
            return
        self.a_min = float(dist[0, 0])
        self.a_max = float(dist[0, -1])
        self.mean = float(np.sum(dist[0] * dist[1]) / n)
        self.var = float(np.sum(dist[1] * (dist[0] - self.mean) ** 2) / n)
        self.dev = math.sqrt(self.var)
        a, freq = np.asarray(dist)
        q25, median, q75 = _quantiles(a, freq, [0.25, 0.5, 0.75])
        self.median = median
        # The code below omits the q25 or q75 in the plot when they are None
        self.q25 = None if q25 == median else q25
        self.q75 = None if q75 == median else q75
        self.conditions = [FilterContinuous(attr, FilterContinuous.Between,
                                            self.q25, self.q75)]
        if group_val_index is not None:
            self.conditions.append(FilterDiscrete(group_var, [group_val_index]))


class FilterGraphicsRectItem(QGraphicsRectItem):
    def __init__(self, conditions, *args):
        super().__init__(*args)
        self.filter = Values(conditions) if conditions else None
        self.setFlag(QGraphicsItem.ItemIsSelectable)


class OWBoxPlot(widget.OWWidget):
    """
    Here's how the widget's functions call each other:

    - `set_data` is a signal handler fills the list boxes and calls
    `grouping_changed`.

    - `grouping_changed` handles changes of grouping attribute: it enables or
    disables the box for ordering, orders attributes and calls `attr_changed`.

    - `attr_changed` handles changes of attribute. It recomputes box data by
    calling `compute_box_data`, shows the appropriate display box
    (discrete/continuous) and then calls`layout_changed`

    - `layout_changed` constructs all the elements for the scene (as lists of
    QGraphicsItemGroup) and calls `display_changed`. It is called when the
    attribute or grouping is changed (by attr_changed) and on resize event.

    - `display_changed` puts the elements corresponding to the current display
    settings on the scene. It is called when the elements are reconstructed
    (layout is changed due to selection of attributes or resize event), or
    when the user changes display settings or colors.

    For discrete attributes, the flow is a bit simpler: the elements are not
    constructed in advance (by layout_changed). Instead, _display_changed_disc
    draws everything.
    """
    name = "Box Plot"
    description = "Visualize the distribution of feature values in a box plot."
    icon = "icons/BoxPlot.svg"
    priority = 100
    keywords = ["whisker"]

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

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

    #: Comparison types for continuous variables
    CompareNone, CompareMedians, CompareMeans = 0, 1, 2

    settingsHandler = DomainContextHandler()
    conditions = ContextSetting([])

    attribute = ContextSetting(None)
    order_by_importance = Setting(False)
    order_grouping_by_importance = Setting(False)
    group_var = ContextSetting(None)
    show_annotations = Setting(True)
    compare = Setting(CompareMeans)
    stattest = Setting(0)
    sig_threshold = Setting(0.05)
    stretched = Setting(True)
    show_labels = Setting(True)
    sort_freqs = Setting(False)

    _sorting_criteria_attrs = {
        CompareNone: "", CompareMedians: "median", CompareMeans: "mean"
    }

    _pen_axis_tick = QPen(Qt.white, 5)
    _pen_axis = QPen(Qt.darkGray, 3)
    _pen_median = QPen(QBrush(QColor(0xff, 0xff, 0x00)), 2)
    _pen_paramet = QPen(QBrush(QColor(0x33, 0x00, 0xff)), 2)
    _pen_dotted = QPen(QBrush(QColor(0x33, 0x00, 0xff)), 1)
    _pen_dotted.setStyle(Qt.DotLine)
    _post_line_pen = QPen(Qt.lightGray, 2)
    _post_grp_pen = QPen(Qt.lightGray, 4)
    for pen in (_pen_paramet, _pen_median, _pen_dotted,
                _pen_axis, _pen_axis_tick, _post_line_pen, _post_grp_pen):
        pen.setCosmetic(True)
        pen.setCapStyle(Qt.RoundCap)
        pen.setJoinStyle(Qt.RoundJoin)
    _pen_axis_tick.setCapStyle(Qt.FlatCap)

    _box_brush = QBrush(QColor(0x33, 0x88, 0xff, 0xc0))

    _axis_font = QFont()
    _axis_font.setPixelSize(12)
    _label_font = QFont()
    _label_font.setPixelSize(11)
    _attr_brush = QBrush(QColor(0x33, 0x00, 0xff))

    graph_name = "box_scene"

    def __init__(self):
        super().__init__()
        self.stats = []
        self.dataset = None
        self.posthoc_lines = []

        self.label_txts = self.mean_labels = self.boxes = self.labels = \
            self.label_txts_all = self.attr_labels = self.order = []
        self.scale_x = 1
        self.scene_min_x = self.scene_max_x = self.scene_width = 0
        self.label_width = 0

        self.attrs = VariableListModel()
        view = gui.listView(
            self.controlArea, self, "attribute", box="Variable",
            model=self.attrs, callback=self.attr_changed)
        view.setMinimumSize(QSize(30, 30))
        # Any other policy than Ignored will let the QListBox's scrollbar
        # set the minimal height (see the penultimate paragraph of
        # http://doc.qt.io/qt-4.8/qabstractscrollarea.html#addScrollBarWidget)
        view.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Ignored)
        gui.checkBox(
            view.box, self, "order_by_importance",
            "Order by relevance to subgroups",
            tooltip="Order by 𝜒² or ANOVA over the subgroups",
            callback=self.apply_attr_sorting)
        self.group_vars = VariableListModel(placeholder="None")
        view = gui.listView(
            self.controlArea, self, "group_var", box="Subgroups",
            model=self.group_vars, callback=self.grouping_changed)
        gui.checkBox(
            view.box, self, "order_grouping_by_importance",
            "Order by relevance to variable",
            tooltip="Order by 𝜒² or ANOVA over the variable values",
            callback=self.on_group_sorting_checkbox)
        view.setMinimumSize(QSize(30, 30))
        # See the comment above
        view.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Ignored)

        # TODO: move Compare median/mean to grouping box
        # The vertical size policy is needed to let only the list views expand
        self.display_box = gui.vBox(
            self.controlArea, "Display",
            sizePolicy=(QSizePolicy.Minimum, QSizePolicy.Maximum),
            addSpace=False)

        gui.checkBox(self.display_box, self, "show_annotations", "Annotate",
                     callback=self.display_changed)
        self.compare_rb = gui.radioButtonsInBox(
            self.display_box, self, 'compare',
            btnLabels=["No comparison", "Compare medians", "Compare means"],
            callback=self.layout_changed)

        # The vertical size policy is needed to let only the list views expand
        self.stretching_box = box = gui.vBox(
            self.controlArea, box="Display",
            sizePolicy=(QSizePolicy.Minimum, QSizePolicy.Fixed))
        self.stretching_box.sizeHint = self.display_box.sizeHint
        gui.checkBox(
            box, self, 'stretched', "Stretch bars",
            callback=self.display_changed)
        gui.checkBox(
            box, self, 'show_labels', "Show box labels",
            callback=self.display_changed)
        self.sort_cb = gui.checkBox(
            box, self, 'sort_freqs', "Sort by subgroup frequencies",
            callback=self.display_changed)

        gui.vBox(self.mainArea, addSpace=True)
        self.box_scene = QGraphicsScene()
        self.box_scene.selectionChanged.connect(self.commit)
        self.box_view = QGraphicsView(self.box_scene)
        self.box_view.setRenderHints(QPainter.Antialiasing |
                                     QPainter.TextAntialiasing |
                                     QPainter.SmoothPixmapTransform)
        self.box_view.viewport().installEventFilter(self)

        self.mainArea.layout().addWidget(self.box_view)

        gui.hBox(self.mainArea, addSpace=False)
        self.stat_test = ""
        self.mainArea.setMinimumWidth(300)

        self.stats = self.dist = self.conts = []
        self.is_continuous = False

        self.update_display_box()

    def sizeHint(self):
        return QSize(900, 500)

    def eventFilter(self, obj, event):
        if obj is self.box_view.viewport() and \
                event.type() == QEvent.Resize:
            self.layout_changed()

        return super().eventFilter(obj, event)

    @property
    def show_stretched(self):
        return self.stretched and self.group_var is not self.attribute

    def reset_attrs(self):
        domain = self.dataset.domain
        self.attrs[:] = [
            var for var in chain(
                domain.class_vars, domain.metas, domain.attributes)
            if var.is_primitive()]

    def reset_groups(self):
        domain = self.dataset.domain
        self.group_vars[:] = [None] + [
            var for var in chain(
                domain.class_vars, domain.metas, domain.attributes)
            if var.is_discrete]

    # noinspection PyTypeChecker
    @Inputs.data
    def set_data(self, dataset):
        if dataset is not None and (
                not bool(dataset) or not len(dataset.domain) and not
                any(var.is_primitive() for var in dataset.domain.metas)):
            dataset = None
        self.closeContext()
        self.dataset = dataset
        self.dist = self.stats = self.conts = []
        self.group_var = None
        self.attribute = None
        if dataset:
            self.reset_attrs()
            self.reset_groups()
            self.select_default_variables()
            self.openContext(self.dataset)
            self.grouping_changed()
            self.attr_changed()
        else:
            self.reset_all_data()
        self.commit()

    def select_default_variables(self):
        # visualize first non-class variable, group by class (if present)
        domain = self.dataset.domain
        if len(self.attrs) > len(domain.class_vars):
            self.attribute = self.attrs[len(domain.class_vars)]
        elif self.attrs:
            self.attribute = self.attrs[0]

        if domain.class_var and domain.class_var.is_discrete:
            self.group_var = domain.class_var
        else:
            self.group_var = None  # Reset to trigger selection via callback

    def apply_attr_sorting(self):
        def compute_score(attr):
            # This function and the one in apply_group_sorting are similar, but
            # different in too many details, so they are kept as separate
            # functions.
            # If you discover a bug in this function, check the other one, too.
            if attr is group_var:
                return 3
            if attr.is_continuous:
                # One-way ANOVA
                col = data.get_column_view(attr)[0].astype(float)
                groups = (col[group_col == i] for i in range(n_groups))
                groups = (col[~np.isnan(col)] for col in groups)
                groups = [group for group in groups if len(group)]
                p = f_oneway(*groups)[1] if len(groups) > 1 else 2
            else:
                p = self._chi_square(group_var, attr)[1]
            if math.isnan(p):
                return 2
            return p

        data = self.dataset
        if data is None:
            return
        domain = data.domain
        attribute = self.attribute
        group_var = self.group_var
        if self.order_by_importance and group_var is not None:
            n_groups = len(group_var.values)
            group_col = data.get_column_view(group_var)[0] if \
                domain.has_continuous_attributes(
                    include_class=True, include_metas=True) else None
            self.attrs.sort(key=compute_score)
        else:
            self.reset_attrs()
        self.attribute = attribute  # reset selection
        self._ensure_selection_visible(self.controls.attribute)

    def on_group_sorting_checkbox(self):
        if self.order_grouping_by_importance:
            self.apply_group_sorting()
        else:
            self.reset_groups()
            self.group_var = self.group_var  # reset selection
            self._ensure_selection_visible(self.controls.group_var)

    def apply_group_sorting(self):
        def compute_stat(group):
            # This function and the one in apply_attr_sorting are similar, but
            # different in too many details, so they are kept as separate
            # functions.
            # If you discover a bug in this function, check the other one, too.
            if group is attr:
                return 3
            if group is None:
                return -1
            if attr.is_continuous:
                group_col = data.get_column_view(group)[0].astype(float)
                groups = (attr_col[group_col == i]
                          for i in range(len(group.values)))
                groups = (col[~np.isnan(col)] for col in groups)
                groups = [group for group in groups if len(group)]
                p = f_oneway(*groups)[1] if len(groups) > 1 else 2
            else:
                p = self._chi_square(group, attr)[1]
            if math.isnan(p):
                return 2
            return p

        data = self.dataset
        if data is None or not self.order_grouping_by_importance:
            return
        attr = self.attribute
        group_var = self.group_var
        if attr.is_continuous:
            attr_col = data.get_column_view(attr)[0].astype(float)
        self.group_vars.sort(key=compute_stat)
        self.group_var = group_var  # reset selection
        self._ensure_selection_visible(self.controls.group_var)

    @staticmethod
    def _ensure_selection_visible(view):
        selection = view.selectedIndexes()
        if len(selection) == 1:
            view.scrollTo(selection[0])

    def _chi_square(self, group_var, attr):
        # Chi-square with the given distribution into groups
        if not attr.values or not group_var.values:
            return 0, 2, 0
        observed = np.array(
            contingency.get_contingency(self.dataset, group_var, attr))
        observed = observed[observed.sum(axis=1) != 0, :]
        observed = observed[:, observed.sum(axis=0) != 0]
        if min(observed.shape) < 2:
            return 0, 2, 0
        return chi2_contingency(observed)[:3]

    def reset_all_data(self):
        self.clear_scene()
        self.stat_test = ""
        self.attrs[:] = []
        self.group_vars[:] = [None]
        self.is_continuous = False
        self.update_display_box()

    def grouping_changed(self):
        self.controls.stretched.setDisabled(self.group_var is self.attribute)
        self.apply_attr_sorting()
        self.update_graph()

    def select_box_items(self):
        temp_cond = self.conditions.copy()
        for box in self.box_scene.items():
            if isinstance(box, FilterGraphicsRectItem):
                box.setSelected(box.filter.conditions in
                                [c.conditions for c in temp_cond])

    def attr_changed(self):
        self.controls.stretched.setDisabled(self.group_var is self.attribute)
        self.apply_group_sorting()
        self.update_graph()

    def update_graph(self):
        self.compute_box_data()
        self.update_display_box()
        self.layout_changed()

        if self.is_continuous:
            heights = 90 if self.show_annotations else 60
            self.box_view.centerOn(self.scene_min_x + self.scene_width / 2,
                                   -30 - len(self.stats) * heights / 2 + 45)
        else:
            self.box_view.centerOn(self.scene_width / 2,
                                   -30 - len(self.boxes) * 40 / 2 + 45)

    def compute_box_data(self):
        attr = self.attribute
        if not attr:
            return
        dataset = self.dataset
        self.is_continuous = attr.is_continuous
        if dataset is None or not self.is_continuous and not attr.values or \
                        self.group_var and not self.group_var.values:
            self.stats = self.dist = self.conts = []
            return
        if self.group_var:
            self.dist = []
            self.conts = contingency.get_contingency(
                dataset, attr, self.group_var)
            if self.is_continuous:
                stats, label_texts = [], []
                for i, cont in enumerate(self.conts):
                    if np.sum(cont[1]):
                        stats.append(BoxData(cont, attr, i, self.group_var))
                        label_texts.append(self.group_var.values[i])
                self.stats = stats
                self.label_txts_all = label_texts
            else:
                self.label_txts_all = \
                    [v for v, c in zip(
                        self.group_var.values + ["Missing values"],
                        self.conts.array_with_unknowns)
                     if np.sum(c) > 0]
        else:
            self.dist = distribution.get_distribution(dataset, attr)
            self.conts = []
            if self.is_continuous:
                self.stats = [BoxData(self.dist, attr, None)]
            self.label_txts_all = [""]
        self.label_txts = [txts for stat, txts in zip(self.stats,
                                                      self.label_txts_all)
                           if stat.n > 0]
        self.stats = [stat for stat in self.stats if stat.n > 0]

    def update_display_box(self):
        if self.is_continuous:
            self.stretching_box.hide()
            self.display_box.show()
            self.compare_rb.setEnabled(self.group_var is not None)
        else:
            self.stretching_box.show()
            self.display_box.hide()
            self.sort_cb.setEnabled(self.group_var is not None)

    def clear_scene(self):
        self.closeContext()
        self.box_scene.clearSelection()
        self.box_scene.clear()
        self.box_view.viewport().update()
        self.attr_labels = []
        self.labels = []
        self.boxes = []
        self.mean_labels = []
        self.posthoc_lines = []
        self.openContext(self.dataset)

    def layout_changed(self):
        attr = self.attribute
        if not attr:
            return
        self.clear_scene()
        if self.dataset is None or len(self.conts) == len(self.dist) == 0:
            return

        if self.is_continuous:
            self.mean_labels = [self.mean_label(stat, attr, lab)
                                for stat, lab in zip(self.stats, self.label_txts)]
            self.draw_axis()
            self.boxes = [self.box_group(stat) for stat in self.stats]
            self.labels = [self.label_group(stat, attr, mean_lab)
                           for stat, mean_lab in zip(self.stats, self.mean_labels)]
            self.attr_labels = [QGraphicsSimpleTextItem(lab)
                                for lab in self.label_txts]
            for it in chain(self.labels, self.attr_labels):
                self.box_scene.addItem(it)

        self.display_changed()

    def display_changed(self):
        if self.dataset is None or self.attribute is None:
            return

        if self.is_continuous:
            self._display_changed_cont()
        else:
            self._display_changed_disc()
        self.draw_stat()
        self.select_box_items()

    def _display_changed_cont(self):
        self.order = list(range(len(self.stats)))
        criterion = self._sorting_criteria_attrs[self.compare]
        if criterion:
            vals = [getattr(stat, criterion) for stat in self.stats]
            overmax = max((val for val in vals if val is not None), default=0) \
                      + 1
            vals = [val if val is not None else overmax for val in vals]
            self.order = sorted(self.order, key=vals.__getitem__)

        heights = 90 if self.show_annotations else 60

        for row, box_index in enumerate(self.order):
            y = (-len(self.stats) + row) * heights + 10
            for item in self.boxes[box_index]:
                self.box_scene.addItem(item)
                item.setY(y)
            labels = self.labels[box_index]

            if self.show_annotations:
                labels.show()
                labels.setY(y)
            else:
                labels.hide()

            label = self.attr_labels[box_index]
            label.setY(y - 15 - label.boundingRect().height())
            if self.show_annotations:
                label.hide()
            else:
                stat = self.stats[box_index]

                if self.compare == OWBoxPlot.CompareMedians and \
                        stat.median is not None:
                    pos = stat.median + 5 / self.scale_x
                elif self.compare == OWBoxPlot.CompareMeans or stat.q25 is None:
                    pos = stat.mean + 5 / self.scale_x
                else:
                    pos = stat.q25
                label.setX(pos * self.scale_x)
                label.show()

        r = QRectF(self.scene_min_x, -30 - len(self.stats) * heights,
                   self.scene_width, len(self.stats) * heights + 90)
        self.box_scene.setSceneRect(r)

        self._compute_tests_cont()
        self._show_posthoc()

    def _display_changed_disc(self):
        self.clear_scene()
        self.attr_labels = [QGraphicsSimpleTextItem(lab)
                            for lab in self.label_txts_all]

        if not self.show_stretched:
            if self.group_var:
                self.labels = [
                    QGraphicsTextItem("{}".format(int(sum(cont))))
                    for cont in self.conts.array_with_unknowns
                    if np.sum(cont) > 0]
            else:
                self.labels = [
                    QGraphicsTextItem(str(int(sum(self.dist))))]

        self.order = list(range(len(self.attr_labels)))

        self.draw_axis_disc()
        if self.group_var:
            self.boxes = \
                [self.strudel(cont, i)
                 for i, cont in enumerate(self.conts.array_with_unknowns)
                 if np.sum(cont) > 0]

            if self.sort_freqs:
                # pylint: disable=invalid-unary-operand-type
                self.order = sorted(
                    self.order, key=(-np.sum(
                        self.conts.array_with_unknowns, axis=1)).__getitem__)
        else:
            self.boxes = [self.strudel(self.dist.array_with_unknowns)]

        for row, box_index in enumerate(self.order):
            y = (-len(self.boxes) + row) * 40 + 10
            box = self.boxes[box_index]
            bars, labels = box[::2], box[1::2]

            self.__draw_group_labels(y, box_index)
            if not self.show_stretched:
                self.__draw_row_counts(y, box_index)
            if self.show_labels and self.attribute is not self.group_var:
                self.__draw_bar_labels(y, bars, labels)
            self.__draw_bars(y, bars)

        self.box_scene.setSceneRect(-self.label_width - 5,
                                    -30 - len(self.boxes) * 40,
                                    self.scene_width, len(self.boxes * 40) + 90)
        self._compute_tests_disc()

    def __draw_group_labels(self, y, row):
        """Draw group labels

        Parameters
        ----------
        y: int
            vertical offset of bars
        row: int
            row index
        """
        label = self.attr_labels[row]
        b = label.boundingRect()
        label.setPos(-b.width() - 10, y - b.height() / 2)
        self.box_scene.addItem(label)

    def __draw_row_counts(self, y, row):
        """Draw row counts

        Parameters
        ----------
        y: int
            vertical offset of bars
        row: int
            row index
        """
        assert not self.is_continuous
        label = self.labels[row]
        b = label.boundingRect()
        if self.group_var:
            right = self.scale_x * sum(self.conts.array_with_unknowns[row])
        else:
            right = self.scale_x * sum(self.dist)
        label.setPos(right + 10, y - b.height() / 2)
        self.box_scene.addItem(label)

    def __draw_bar_labels(self, y, bars, labels):
        """Draw bar labels

        Parameters
        ----------
        y: int
            vertical offset of bars
        bars: List[FilterGraphicsRectItem]
            list of bars being drawn
        labels: List[QGraphicsTextItem]
            list of labels for corresponding bars
        """
        label = bar_part = None
        for text_item, bar_part in zip(labels, bars):
            label = self.Label(
                text_item.toPlainText())
            label.setPos(bar_part.boundingRect().x(),
                         y - label.boundingRect().height() - 8)
            label.setMaxWidth(bar_part.boundingRect().width())
            self.box_scene.addItem(label)

    def __draw_bars(self, y, bars):
        """Draw bars

        Parameters
        ----------
        y: int
            vertical offset of bars

        bars: List[FilterGraphicsRectItem]
            list of bars to draw
        """
        for item in bars:
            item.setPos(0, y)
            self.box_scene.addItem(item)

    # noinspection PyPep8Naming
    def _compute_tests_cont(self):
        # The t-test and ANOVA are implemented here since they efficiently use
        # the widget-specific data in self.stats.
        # The non-parametric tests can't do this, so we use statistics.tests

        # pylint: disable=comparison-with-itself
        def stat_ttest():
            d1, d2 = self.stats
            if d1.n < 2 or d2.n < 2:
                return np.nan, np.nan
            pooled_var = d1.var / d1.n + d2.var / d2.n
            # pylint: disable=comparison-with-itself
            if pooled_var == 0 or np.isnan(pooled_var):
                return np.nan, np.nan
            df = pooled_var ** 2 / \
                ((d1.var / d1.n) ** 2 / (d1.n - 1) +
                 (d2.var / d2.n) ** 2 / (d2.n - 1))
            t = abs(d1.mean - d2.mean) / math.sqrt(pooled_var)
            p = 2 * (1 - scipy.special.stdtr(df, t))
            return t, p

        # TODO: Check this function
        # noinspection PyPep8Naming
        def stat_ANOVA():
            if any(stat.n == 0 for stat in self.stats):
                return np.nan, np.nan
            n = sum(stat.n for stat in self.stats)
            grand_avg = sum(stat.n * stat.mean for stat in self.stats) / n
            var_between = sum(stat.n * (stat.mean - grand_avg) ** 2
                              for stat in self.stats)
            df_between = len(self.stats) - 1

            var_within = sum(stat.n * stat.var for stat in self.stats)
            df_within = n - len(self.stats)
            if var_within == 0 or df_within == 0 or df_between == 0:
                return np.nan, np.nan
            F = (var_between / df_between) / (var_within / df_within)
            p = 1 - scipy.special.fdtr(df_between, df_within, F)
            return F, p

        n = len(self.dataset)
        if self.compare == OWBoxPlot.CompareNone or len(self.stats) < 2:
            t = ""
        elif any(s.n <= 1 for s in self.stats):
            t = "At least one group has just one instance, " \
                "cannot compute significance"
        elif len(self.stats) == 2:
            if self.compare == OWBoxPlot.CompareMedians:
                t = ""
                # z, p = tests.wilcoxon_rank_sum(
                #    self.stats[0].dist, self.stats[1].dist)
                # t = "Mann-Whitney's z: %.1f (p=%.3f)" % (z, p)
            else:
                t, p = stat_ttest()
                t = "" if np.isnan(t) else f"Student's t: {t:.3f} (p={p:.3f}, N={n})"
        else:
            if self.compare == OWBoxPlot.CompareMedians:
                t = ""
                # U, p = -1, -1
                # t = "Kruskal Wallis's U: %.1f (p=%.3f)" % (U, p)
            else:
                F, p = stat_ANOVA()
                t = "" if np.isnan(F) else f"ANOVA: {F:.3f} (p={p:.3f}, N={n})"
        self.stat_test = t

    def _compute_tests_disc(self):
        if self.group_var is None or self.attribute is None:
            self.stat_test = ""
        else:
            chi, p, dof = self._chi_square(self.group_var, self.attribute)
            if np.isnan(p):
                self.stat_test = ""
            else:
                self.stat_test = f"χ²: {chi:.2f} (p={p:.3f}, dof={dof})"

    def mean_label(self, stat, attr, val_name):
        label = QGraphicsItemGroup()
        t = QGraphicsSimpleTextItem(attr.str_val(stat.mean), label)
        t.setFont(self._label_font)
        bbox = t.boundingRect()
        w2, h = bbox.width() / 2, bbox.height()
        t.setPos(-w2, -h)
        tpm = QGraphicsSimpleTextItem(
            " \u00b1 " + "%.*f" % (attr.number_of_decimals + 1, stat.dev),
            label)
        tpm.setFont(self._label_font)
        tpm.setPos(w2, -h)
        if val_name:
            vnm = QGraphicsSimpleTextItem(val_name + ": ", label)
            vnm.setFont(self._label_font)
            vnm.setBrush(self._attr_brush)
            vb = vnm.boundingRect()
            label.min_x = -w2 - vb.width()
            vnm.setPos(label.min_x, -h)
        else:
            label.min_x = -w2
        return label

    def draw_axis(self):
        """Draw the horizontal axis and sets self.scale_x"""
        misssing_stats = not self.stats
        stats = self.stats or [BoxData(np.array([[0.], [1.]]), self.attribute)]
        mean_labels = self.mean_labels or [self.mean_label(stats[0], self.attribute, "")]
        bottom = min(stat.a_min for stat in stats)
        top = max(stat.a_max for stat in stats)

        first_val, step = compute_scale(bottom, top)
        while bottom <= first_val:
            first_val -= step
        bottom = first_val
        no_ticks = math.ceil((top - first_val) / step) + 1
        top = max(top, first_val + no_ticks * step)

        gbottom = min(bottom, min(stat.mean - stat.dev for stat in stats))
        gtop = max(top, max(stat.mean + stat.dev for stat in stats))

        bv = self.box_view
        viewrect = bv.viewport().rect().adjusted(15, 15, -15, -30)
        self.scale_x = scale_x = viewrect.width() / (gtop - gbottom)

        # In principle we should repeat this until convergence since the new
        # scaling is too conservative. (No chance am I doing this.)
        mlb = min(stat.mean + mean_lab.min_x / scale_x
                  for stat, mean_lab in zip(stats, mean_labels))
        if mlb < gbottom:
            gbottom = mlb
            self.scale_x = scale_x = viewrect.width() / (gtop - gbottom)

        self.scene_min_x = gbottom * scale_x
        self.scene_max_x = gtop * scale_x
        self.scene_width = self.scene_max_x - self.scene_min_x

        val = first_val
        last_text = self.scene_min_x
        while True:
            l = self.box_scene.addLine(val * scale_x, -1, val * scale_x, 1,
                                       self._pen_axis_tick)
            l.setZValue(100)
            t = QGraphicsSimpleTextItem(
                self.attribute.str_val(val) if not misssing_stats else "?")
            t.setFont(self._axis_font)
            t.setFlag(QGraphicsItem.ItemIgnoresTransformations)
            r = t.boundingRect()
            x_start = val * scale_x - r.width() / 2
            x_finish = x_start + r.width()
            if x_start > last_text + 10 and x_finish < self.scene_max_x:
                t.setPos(x_start, 8)
                self.box_scene.addItem(t)
                last_text = x_finish
            if val >= top:
                break
            val += step
        self.box_scene.addLine(
            bottom * scale_x - 4, 0, top * scale_x + 4, 0, self._pen_axis)

    def draw_stat(self):
        if self.stat_test:
            label = QGraphicsSimpleTextItem(self.stat_test)
            brect = self.box_scene.sceneRect()
            label.setPos(brect.center().x() - label.boundingRect().width()/2,
                         8 + self._axis_font.pixelSize()*2)
            label.setFlag(QGraphicsItem.ItemIgnoresTransformations)
            self.box_scene.addItem(label)

    def draw_axis_disc(self):
        """
        Draw the horizontal axis and sets self.scale_x for discrete attributes
        """
        assert not self.is_continuous
        if self.show_stretched:
            if not self.attr_labels:
                return
            step = steps = 10
        else:
            if self.group_var:
                max_box = max(float(np.sum(dist))
                              for dist in self.conts.array_with_unknowns)
            else:
                max_box = float(np.sum(self.dist.array_with_unknowns))
            if max_box == 0:
                self.scale_x = 1
                return
            _, step = compute_scale(0, max_box)
            step = int(step) if step > 1 else 1
            steps = int(math.ceil(max_box / step))
        max_box = step * steps

        bv = self.box_view
        viewrect = bv.viewport().rect().adjusted(15, 15, -15, -30)
        self.scene_width = viewrect.width()

        lab_width = max(lab.boundingRect().width() for lab in self.attr_labels)
        lab_width = max(lab_width, 40)
        lab_width = min(lab_width, self.scene_width / 3)
        self.label_width = lab_width

        right_offset = 0  # offset for the right label
        if not self.show_stretched and self.labels:
            if self.group_var:
                rows = list(zip(self.conts.array_with_unknowns, self.labels))
            else:
                rows = [(self.dist, self.labels[0])]
            # available space left of the 'group labels'
            available = self.scene_width - lab_width - 10
            scale_x = (available - right_offset) / max_box
            max_right = max(sum(dist) * scale_x + 10 +
                            lbl.boundingRect().width()
                            for dist, lbl in rows)
            right_offset = max(0, max_right - max_box * scale_x)

        self.scale_x = scale_x = \
            (self.scene_width - lab_width - 10 - right_offset) / max_box

        self.box_scene.addLine(0, 0, max_box * scale_x, 0, self._pen_axis)
        for val in range(0, step * steps + 1, step):
            l = self.box_scene.addLine(val * scale_x, -1, val * scale_x, 1,
                                       self._pen_axis_tick)
            l.setZValue(100)
            t = self.box_scene.addSimpleText(str(val), self._axis_font)
            t.setPos(val * scale_x - t.boundingRect().width() / 2, 8)
        if self.show_stretched:
            self.scale_x *= 100

    def label_group(self, stat, attr, mean_lab):
        def centered_text(val, pos):
            t = QGraphicsSimpleTextItem(attr.str_val(val), labels)
            t.setFont(self._label_font)
            bbox = t.boundingRect()
            t.setPos(pos - bbox.width() / 2, 22)
            return t

        def line(x, down=1):
            QGraphicsLineItem(x, 12 * down, x, 20 * down, labels)

        def move_label(label, frm, to):
            label.setX(to)
            to += t_box.width() / 2
            path = QPainterPath()
            path.lineTo(0, 4)
            path.lineTo(to - frm, 4)
            path.lineTo(to - frm, 8)
            p = QGraphicsPathItem(path)
            p.setPos(frm, 12)
            labels.addToGroup(p)

        labels = QGraphicsItemGroup()

        labels.addToGroup(mean_lab)
        m = stat.mean * self.scale_x
        mean_lab.setPos(m, -22)
        line(m, -1)

        if stat.median is not None:
            msc = stat.median * self.scale_x
            med_t = centered_text(stat.median, msc)
            med_box_width2 = med_t.boundingRect().width() / 2
            line(msc)

        if stat.q25 is not None:
            x = stat.q25 * self.scale_x
            t = centered_text(stat.q25, x)
            t_box = t.boundingRect()
            med_left = msc - med_box_width2
            if x + t_box.width() / 2 >= med_left - 5:
                move_label(t, x, med_left - t_box.width() - 5)
            else:
                line(x)

        if stat.q75 is not None:
            x = stat.q75 * self.scale_x
            t = centered_text(stat.q75, x)
            t_box = t.boundingRect()
            med_right = msc + med_box_width2
            if x - t_box.width() / 2 <= med_right + 5:
                move_label(t, x, med_right + 5)
            else:
                line(x)

        return labels

    def box_group(self, stat, height=20):
        def line(x0, y0, x1, y1, *args):
            return QGraphicsLineItem(x0 * scale_x, y0, x1 * scale_x, y1, *args)

        scale_x = self.scale_x
        box = []
        whisker1 = line(stat.a_min, -1.5, stat.a_min, 1.5)
        whisker2 = line(stat.a_max, -1.5, stat.a_max, 1.5)
        vert_line = line(stat.a_min, 0, stat.a_max, 0)
        mean_line = line(stat.mean, -height / 3, stat.mean, height / 3)
        for it in (whisker1, whisker2, mean_line):
            it.setPen(self._pen_paramet)
        vert_line.setPen(self._pen_dotted)
        var_line = line(stat.mean - stat.dev, 0, stat.mean + stat.dev, 0)
        var_line.setPen(self._pen_paramet)
        box.extend([whisker1, whisker2, vert_line, mean_line, var_line])
        if stat.q25 is not None or stat.q75 is not None:
            # if any of them is None it means that its value is equal to median
            box_from = stat.q25 or stat.median
            box_to = stat.q75 or stat.median
            mbox = FilterGraphicsRectItem(
                stat.conditions, box_from * scale_x, -height / 2,
                (box_to - box_from) * scale_x, height)
            mbox.setBrush(self._box_brush)
            mbox.setPen(QPen(Qt.NoPen))
            mbox.setZValue(-200)
            box.append(mbox)

        if stat.median is not None:
            median_line = line(stat.median, -height / 2,
                               stat.median, height / 2)
            median_line.setPen(self._pen_median)
            median_line.setZValue(-150)
            box.append(median_line)

        return box

    def strudel(self, dist, group_val_index=None):
        attr = self.attribute
        ss = np.sum(dist)
        box = []
        if ss < 1e-6:
            cond = [FilterDiscrete(attr, None)]
            if group_val_index is not None:
                cond.append(FilterDiscrete(self.group_var, [group_val_index]))
            box.append(FilterGraphicsRectItem(cond, 0, -10, 1, 10))
        cum = 0
        values = attr.values + ["Missing values"]
        colors = np.vstack((attr.colors, [128, 128, 128]))
        for i, v in enumerate(dist):
            if v < 1e-6:
                continue
            if self.show_stretched:
                v /= ss
            v *= self.scale_x
            cond = [FilterDiscrete(attr, [i])]
            if group_val_index is not None:
                cond.append(FilterDiscrete(self.group_var, [group_val_index]))
            rect = FilterGraphicsRectItem(cond, cum + 1, -6, v - 2, 12)
            rect.setBrush(QBrush(QColor(*colors[i])))
            rect.setPen(QPen(Qt.NoPen))
            if self.show_stretched:
                tooltip = "{}: {:.2f}%".format(
                    values[i],
                    100 * dist[i] / sum(dist))
            else:
                tooltip = "{}: {}".format(values[i], int(dist[i]))
            rect.setToolTip(tooltip)
            text = QGraphicsTextItem(values[i])
            box.append(rect)
            box.append(text)
            cum += v
        return box

    def commit(self):
        self.conditions = [item.filter for item in
                           self.box_scene.selectedItems() if item.filter]
        selected, selection = None, []
        if self.conditions:
            selected = Values(self.conditions, conjunction=False)(self.dataset)
            selection = np.in1d(
                self.dataset.ids, selected.ids, assume_unique=True).nonzero()[0]
        self.Outputs.selected_data.send(selected)
        self.Outputs.annotated_data.send(
            create_annotated_table(self.dataset, selection))

    def _show_posthoc(self):
        def line(y0, y1):
            it = self.box_scene.addLine(x, y0, x, y1, self._post_line_pen)
            it.setZValue(-100)
            self.posthoc_lines.append(it)

        while self.posthoc_lines:
            self.box_scene.removeItem(self.posthoc_lines.pop())

        if self.compare == OWBoxPlot.CompareNone or len(self.stats) < 2:
            return

        if self.compare == OWBoxPlot.CompareMedians:
            crit_line = "median"
        else:
            crit_line = "mean"

        xs = []

        height = 90 if self.show_annotations else 60

        y_up = -len(self.stats) * height + 10
        for pos, box_index in enumerate(self.order):
            stat = self.stats[box_index]
            x = getattr(stat, crit_line)
            if x is None:
                continue
            x *= self.scale_x
            xs.append(x * self.scale_x)
            by = y_up + pos * height
            line(by + 12, 0)

        used_to = []
        last_to = to = 0
        for frm, frm_x in enumerate(xs[:-1]):
            for to in range(frm + 1, len(xs)):
                if xs[to] - frm_x > 1.5:
                    to -= 1
                    break
            if to in (last_to, frm):
                continue
            for rowi, used in enumerate(used_to):
                if used < frm:
                    used_to[rowi] = to
                    break
            else:
                rowi = len(used_to)
                used_to.append(to)
            y = - 6 - rowi * 6
            it = self.box_scene.addLine(frm_x - 2, y, xs[to] + 2, y,
                                        self._post_grp_pen)
            self.posthoc_lines.append(it)
            last_to = to

    def get_widget_name_extension(self):
        return self.attribute.name if self.attribute else None

    def send_report(self):
        self.report_plot()
        text = ""
        if self.attribute:
            text += "Box plot for attribute '{}' ".format(self.attribute.name)
        if self.group_var:
            text += "grouped by '{}'".format(self.group_var.name)
        if text:
            self.report_caption(text)

    class Label(QGraphicsSimpleTextItem):
        """Boxplot Label with settable maxWidth"""
        # Minimum width to display label text
        MIN_LABEL_WIDTH = 25

        # padding bellow the text
        PADDING = 3

        __max_width = None

        def maxWidth(self):
            return self.__max_width

        def setMaxWidth(self, max_width):
            self.__max_width = max_width

        def paint(self, painter, option, widget):
            """Overrides QGraphicsSimpleTextItem.paint

            If label text is too long, it is elided
            to fit into the allowed region
            """
            if self.__max_width is None:
                width = option.rect.width()
            else:
                width = self.__max_width

            if width < self.MIN_LABEL_WIDTH:
                # if space is too narrow, no label
                return

            fm = painter.fontMetrics()
            text = fm.elidedText(self.text(), Qt.ElideRight, width)
            painter.drawText(
                option.rect.x(),
                option.rect.y() + self.boundingRect().height() - self.PADDING,
                text)


if __name__ == "__main__":  # pragma: no cover
    WidgetPreview(OWBoxPlot).run(Orange.data.Table("heart_disease.tab"))

```
--- END FILE ---

Please first localize the bug based on the issue statement, and then generate *SEARCH/REPLACE* edits to fix the issue.

Every *SEARCH/REPLACE* edit must use this format:
1. The file path
2. The start of search block: <<<<<<< SEARCH
3. A contiguous chunk of lines to search for in the existing source code
4. The dividing line: =======
5. The lines to replace into the source code
6. The end of the replace block: >>>>>>> REPLACE

Here is an example:

```python
### mathweb/flask/app.py
<<<<<<< SEARCH
from flask import Flask
=======
import math
from flask import Flask
>>>>>>> REPLACE
```
</solution>

Please note that the *SEARCH/REPLACE* edit REQUIRES PROPER INDENTATION. If you would like to add the line '        print(x)', you must fully write that out, with all those spaces before the code!
Wrap each *SEARCH/REPLACE* edit in a code block as shown in the example above. If you have multiple *SEARCH/REPLACE* edits, use a separate code block for each one.
[ISSUE]
**Title:** BoxPlot Widget Displays Hidden Attributes Unexpectedly

**Description:**
When certain attributes in the dataset are marked as hidden, the BoxPlot widget still displays them. This results in cluttered visualizations and does not align with the intended behavior of hiding specific attributes.

**Example Buggy Code:**
```python
iris = Table("iris")
iris.domain["iris"].attributes["hidden"] = True
iris.domain["petal length"].attributes["hidden"] = True
self.send_signal(self.widget.Inputs.data, iris)
```
In this example, the attributes "iris" and "petal length" are set to hidden. After sending the data to the widget, these hidden attributes should not appear in the BoxPlot visualization.

**Expected Behavior:**
Only non-hidden attributes should be displayed in the BoxPlot widget. In this case, after hiding two attributes, only three should be visible.

**Actual Behavior:**
All five attributes, including the ones marked as hidden, are displayed in the BoxPlot widget. This causes the test to fail with an assertion error, indicating that the number of displayed attributes does not match the expected count.

[/ISSUE]
{
    "TestOWBoxPlot.test_apply_sorting_group": "PASSED",
    "TestOWBoxPlot.test_apply_sorting_vars": "PASSED",
    "TestOWBoxPlot.test_attribute_combinations": "PASSED",
    "TestOWBoxPlot.test_box_order_when_missing_stats": "PASSED",
    "TestOWBoxPlot.test_continuous_metas": "PASSED",
    "TestOWBoxPlot.test_dont_show_hidden_attrs": "PASSED",
    "TestOWBoxPlot.test_empty_groups": "PASSED",
    "TestOWBoxPlot.test_image_export": "PASSED",
    "TestOWBoxPlot.test_input_data": "PASSED",
    "TestOWBoxPlot.test_input_data_missings_cont_group_var": "PASSED",
    "TestOWBoxPlot.test_input_data_missings_cont_no_group_var": "PASSED",
    "TestOWBoxPlot.test_input_data_missings_disc_group_var": "PASSED",
    "TestOWBoxPlot.test_input_data_missings_disc_no_group_var": "PASSED",
    "TestOWBoxPlot.test_label_overlap": "PASSED",
    "TestOWBoxPlot.test_minimum_size": "PASSED",
    "TestOWBoxPlot.test_msg_base_class": "PASSED",
    "TestOWBoxPlot.test_outputs": "PASSED",
    "TestOWBoxPlot.test_primitive_metas": "PASSED",
    "TestOWBoxPlot.test_saved_selection": "PASSED",
    "TestOWBoxPlot.test_sorting_disc_group_var": "PASSED",
    "TestOWBoxPlot.test_stretching": "PASSED",
    "TestOWBoxPlot.test_unconditional_commit_on_new_signal": "PASSED",
    "TestOWBoxPlot.test_value_all_missing_for_group": "PASSED",
    "TestUtils.test": "PASSED",
    "test_filename": "ERROR"
}
diff --git a/Orange/widgets/visualize/owboxplot.py b/Orange/widgets/visualize/owboxplot.py
index 958cc8894..5811d0244 100644
--- a/Orange/widgets/visualize/owboxplot.py
+++ b/Orange/widgets/visualize/owboxplot.py
@@ -299,14 +299,14 @@ class OWBoxPlot(widget.OWWidget):
         self.attrs[:] = [
             var for var in chain(
                 domain.class_vars, domain.metas, domain.attributes)
-            if var.is_primitive()]
+            if var.is_primitive() and not var.attributes.get("hidden", False)]
 
     def reset_groups(self):
         domain = self.dataset.domain
         self.group_vars[:] = [None] + [
             var for var in chain(
                 domain.class_vars, domain.metas, domain.attributes)
-            if var.is_discrete]
+            if var.is_discrete and not var.attributes.get("hidden", False)]
 
     # noinspection PyTypeChecker
     @Inputs.data

{"Orange/widgets/visualize/owboxplot.py": "import math\nfrom itertools import chain\nimport numpy as np\n\nfrom AnyQt.QtWidgets import (\n    QGraphicsView, QGraphicsScene, QGraphicsItem, QGraphicsSimpleTextItem,\n    QGraphicsTextItem, QGraphicsItemGroup, QGraphicsLineItem,\n    QGraphicsPathItem, QGraphicsRectItem, QSizePolicy\n)\nfrom AnyQt.QtGui import QPen, QColor, QBrush, QPainterPath, QPainter, QFont\nfrom AnyQt.QtCore import Qt, QEvent, QRectF, QSize\n\nimport scipy.special\nfrom scipy.stats import f_oneway, chi2_contingency\n\nimport Orange.data\nfrom Orange.data.filter import FilterDiscrete, FilterContinuous, Values\nfrom Orange.statistics import contingency, distribution\n\nfrom Orange.widgets import widget, gui\nfrom Orange.widgets.settings import (Setting, DomainContextHandler,\n                                     ContextSetting)\nfrom Orange.widgets.utils.itemmodels import VariableListModel\nfrom Orange.widgets.utils.annotated_data import (create_annotated_table,\n                                                 ANNOTATED_DATA_SIGNAL_NAME)\nfrom Orange.widgets.utils.widgetpreview import WidgetPreview\nfrom Orange.widgets.widget import Input, Output\n\n\ndef compute_scale(min_, max_):\n    if min_ == max_:\n        return math.floor(min_), 1\n    magnitude = int(3 * math.log10(abs(max_ - min_)) + 1)\n    if magnitude % 3 == 0:\n        first_place = 1\n    elif magnitude % 3 == 1:\n        first_place = 2\n    else:\n        first_place = 5\n    magnitude = magnitude // 3 - 1\n    step = first_place * pow(10, magnitude)\n    first_val = math.ceil(min_ / step) * step\n    return first_val, step\n\n\ndef _quantiles(a, freq, q, interpolation=\"midpoint\"):\n    \"\"\"\n    Somewhat like np.quantiles, but with explicit sample frequencies.\n\n    * Only 'higher', 'lower' and 'midpoint' interpolation.\n    * `a` MUST be sorted.\n    \"\"\"\n    a = np.asarray(a)\n    freq = np.asarray(freq)\n    assert a.size > 0 and a.size == freq.size\n    cumdist = np.cumsum(freq)\n    cumdist /= cumdist[-1]\n\n    if interpolation == \"midpoint\":  # R quantile(..., type=2)\n        left = np.searchsorted(cumdist, q, side=\"left\")\n        right = np.searchsorted(cumdist, q, side=\"right\")\n        # no mid point for the right most position\n        np.clip(right, 0, a.size - 1, out=right)\n        # right and left will be different only on the `q` boundaries\n        # (excluding the right most sample)\n        return (a[left] + a[right]) / 2\n    elif interpolation == \"higher\":  # R quantile(... type=1)\n        right = np.searchsorted(cumdist, q, side=\"right\")\n        np.clip(right, 0, a.size - 1, out=right)\n        return a[right]\n    elif interpolation == \"lower\":\n        left = np.searchsorted(cumdist, q, side=\"left\")\n        return a[left]\n    else:  # pragma: no cover\n        raise ValueError(\"invalid interpolation: '{}'\".format(interpolation))\n\n\nclass BoxData:\n    def __init__(self, dist, attr, group_val_index=None, group_var=None):\n        self.dist = dist\n        self.n = n = np.sum(dist[1])\n        if n == 0:\n            return\n        self.a_min = float(dist[0, 0])\n        self.a_max = float(dist[0, -1])\n        self.mean = float(np.sum(dist[0] * dist[1]) / n)\n        self.var = float(np.sum(dist[1] * (dist[0] - self.mean) ** 2) / n)\n        self.dev = math.sqrt(self.var)\n        a, freq = np.asarray(dist)\n        q25, median, q75 = _quantiles(a, freq, [0.25, 0.5, 0.75])\n        self.median = median\n        # The code below omits the q25 or q75 in the plot when they are None\n        self.q25 = None if q25 == median else q25\n        self.q75 = None if q75 == median else q75\n        self.conditions = [FilterContinuous(attr, FilterContinuous.Between,\n                                            self.q25, self.q75)]\n        if group_val_index is not None:\n            self.conditions.append(FilterDiscrete(group_var, [group_val_index]))\n\n\nclass FilterGraphicsRectItem(QGraphicsRectItem):\n    def __init__(self, conditions, *args):\n        super().__init__(*args)\n        self.filter = Values(conditions) if conditions else None\n        self.setFlag(QGraphicsItem.ItemIsSelectable)\n\n\nclass OWBoxPlot(widget.OWWidget):\n    \"\"\"\n    Here's how the widget's functions call each other:\n\n    - `set_data` is a signal handler fills the list boxes and calls\n    `grouping_changed`.\n\n    - `grouping_changed` handles changes of grouping attribute: it enables or\n    disables the box for ordering, orders attributes and calls `attr_changed`.\n\n    - `attr_changed` handles changes of attribute. It recomputes box data by\n    calling `compute_box_data`, shows the appropriate display box\n    (discrete/continuous) and then calls`layout_changed`\n\n    - `layout_changed` constructs all the elements for the scene (as lists of\n    QGraphicsItemGroup) and calls `display_changed`. It is called when the\n    attribute or grouping is changed (by attr_changed) and on resize event.\n\n    - `display_changed` puts the elements corresponding to the current display\n    settings on the scene. It is called when the elements are reconstructed\n    (layout is changed due to selection of attributes or resize event), or\n    when the user changes display settings or colors.\n\n    For discrete attributes, the flow is a bit simpler: the elements are not\n    constructed in advance (by layout_changed). Instead, _display_changed_disc\n    draws everything.\n    \"\"\"\n    name = \"Box Plot\"\n    description = \"Visualize the distribution of feature values in a box plot.\"\n    icon = \"icons/BoxPlot.svg\"\n    priority = 100\n    keywords = [\"whisker\"]\n\n    class Inputs:\n        data = Input(\"Data\", Orange.data.Table)\n\n    class Outputs:\n        selected_data = Output(\"Selected Data\", Orange.data.Table, default=True)\n        annotated_data = Output(ANNOTATED_DATA_SIGNAL_NAME, Orange.data.Table)\n\n    #: Comparison types for continuous variables\n    CompareNone, CompareMedians, CompareMeans = 0, 1, 2\n\n    settingsHandler = DomainContextHandler()\n    conditions = ContextSetting([])\n\n    attribute = ContextSetting(None)\n    order_by_importance = Setting(False)\n    order_grouping_by_importance = Setting(False)\n    group_var = ContextSetting(None)\n    show_annotations = Setting(True)\n    compare = Setting(CompareMeans)\n    stattest = Setting(0)\n    sig_threshold = Setting(0.05)\n    stretched = Setting(True)\n    show_labels = Setting(True)\n    sort_freqs = Setting(False)\n\n    _sorting_criteria_attrs = {\n        CompareNone: \"\", CompareMedians: \"median\", CompareMeans: \"mean\"\n    }\n\n    _pen_axis_tick = QPen(Qt.white, 5)\n    _pen_axis = QPen(Qt.darkGray, 3)\n    _pen_median = QPen(QBrush(QColor(0xff, 0xff, 0x00)), 2)\n    _pen_paramet = QPen(QBrush(QColor(0x33, 0x00, 0xff)), 2)\n    _pen_dotted = QPen(QBrush(QColor(0x33, 0x00, 0xff)), 1)\n    _pen_dotted.setStyle(Qt.DotLine)\n    _post_line_pen = QPen(Qt.lightGray, 2)\n    _post_grp_pen = QPen(Qt.lightGray, 4)\n    for pen in (_pen_paramet, _pen_median, _pen_dotted,\n                _pen_axis, _pen_axis_tick, _post_line_pen, _post_grp_pen):\n        pen.setCosmetic(True)\n        pen.setCapStyle(Qt.RoundCap)\n        pen.setJoinStyle(Qt.RoundJoin)\n    _pen_axis_tick.setCapStyle(Qt.FlatCap)\n\n    _box_brush = QBrush(QColor(0x33, 0x88, 0xff, 0xc0))\n\n    _axis_font = QFont()\n    _axis_font.setPixelSize(12)\n    _label_font = QFont()\n    _label_font.setPixelSize(11)\n    _attr_brush = QBrush(QColor(0x33, 0x00, 0xff))\n\n    graph_name = \"box_scene\"\n\n    def __init__(self):\n        super().__init__()\n        self.stats = []\n        self.dataset = None\n        self.posthoc_lines = []\n\n        self.label_txts = self.mean_labels = self.boxes = self.labels = \\\n            self.label_txts_all = self.attr_labels = self.order = []\n        self.scale_x = 1\n        self.scene_min_x = self.scene_max_x = self.scene_width = 0\n        self.label_width = 0\n\n        self.attrs = VariableListModel()\n        view = gui.listView(\n            self.controlArea, self, \"attribute\", box=\"Variable\",\n            model=self.attrs, callback=self.attr_changed)\n        view.setMinimumSize(QSize(30, 30))\n        # Any other policy than Ignored will let the QListBox's scrollbar\n        # set the minimal height (see the penultimate paragraph of\n        # http://doc.qt.io/qt-4.8/qabstractscrollarea.html#addScrollBarWidget)\n        view.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Ignored)\n        gui.checkBox(\n            view.box, self, \"order_by_importance\",\n            \"Order by relevance to subgroups\",\n            tooltip=\"Order by \ud835\udf12\u00b2 or ANOVA over the subgroups\",\n            callback=self.apply_attr_sorting)\n        self.group_vars = VariableListModel(placeholder=\"None\")\n        view = gui.listView(\n            self.controlArea, self, \"group_var\", box=\"Subgroups\",\n            model=self.group_vars, callback=self.grouping_changed)\n        gui.checkBox(\n            view.box, self, \"order_grouping_by_importance\",\n            \"Order by relevance to variable\",\n            tooltip=\"Order by \ud835\udf12\u00b2 or ANOVA over the variable values\",\n            callback=self.on_group_sorting_checkbox)\n        view.setMinimumSize(QSize(30, 30))\n        # See the comment above\n        view.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Ignored)\n\n        # TODO: move Compare median/mean to grouping box\n        # The vertical size policy is needed to let only the list views expand\n        self.display_box = gui.vBox(\n            self.controlArea, \"Display\",\n            sizePolicy=(QSizePolicy.Minimum, QSizePolicy.Maximum),\n            addSpace=False)\n\n        gui.checkBox(self.display_box, self, \"show_annotations\", \"Annotate\",\n                     callback=self.display_changed)\n        self.compare_rb = gui.radioButtonsInBox(\n            self.display_box, self, 'compare',\n            btnLabels=[\"No comparison\", \"Compare medians\", \"Compare means\"],\n            callback=self.layout_changed)\n\n        # The vertical size policy is needed to let only the list views expand\n        self.stretching_box = box = gui.vBox(\n            self.controlArea, box=\"Display\",\n            sizePolicy=(QSizePolicy.Minimum, QSizePolicy.Fixed))\n        self.stretching_box.sizeHint = self.display_box.sizeHint\n        gui.checkBox(\n            box, self, 'stretched', \"Stretch bars\",\n            callback=self.display_changed)\n        gui.checkBox(\n            box, self, 'show_labels', \"Show box labels\",\n            callback=self.display_changed)\n        self.sort_cb = gui.checkBox(\n            box, self, 'sort_freqs', \"Sort by subgroup frequencies\",\n            callback=self.display_changed)\n\n        gui.vBox(self.mainArea, addSpace=True)\n        self.box_scene = QGraphicsScene()\n        self.box_scene.selectionChanged.connect(self.commit)\n        self.box_view = QGraphicsView(self.box_scene)\n        self.box_view.setRenderHints(QPainter.Antialiasing |\n                                     QPainter.TextAntialiasing |\n                                     QPainter.SmoothPixmapTransform)\n        self.box_view.viewport().installEventFilter(self)\n\n        self.mainArea.layout().addWidget(self.box_view)\n\n        gui.hBox(self.mainArea, addSpace=False)\n        self.stat_test = \"\"\n        self.mainArea.setMinimumWidth(300)\n\n        self.stats = self.dist = self.conts = []\n        self.is_continuous = False\n\n        self.update_display_box()\n\n    def sizeHint(self):\n        return QSize(900, 500)\n\n    def eventFilter(self, obj, event):\n        if obj is self.box_view.viewport() and \\\n                event.type() == QEvent.Resize:\n            self.layout_changed()\n\n        return super().eventFilter(obj, event)\n\n    @property\n    def show_stretched(self):\n        return self.stretched and self.group_var is not self.attribute\n\n    def reset_attrs(self):\n        domain = self.dataset.domain\n        self.attrs[:] = [\n            var for var in chain(\n                domain.class_vars, domain.metas, domain.attributes)\n            if var.is_primitive()]\n\n    def reset_groups(self):\n        domain = self.dataset.domain\n        self.group_vars[:] = [None] + [\n            var for var in chain(\n                domain.class_vars, domain.metas, domain.attributes)\n            if var.is_discrete]\n\n    # noinspection PyTypeChecker\n    @Inputs.data\n    def set_data(self, dataset):\n        if dataset is not None and (\n                not bool(dataset) or not len(dataset.domain) and not\n                any(var.is_primitive() for var in dataset.domain.metas)):\n            dataset = None\n        self.closeContext()\n        self.dataset = dataset\n        self.dist = self.stats = self.conts = []\n        self.group_var = None\n        self.attribute = None\n        if dataset:\n            self.reset_attrs()\n            self.reset_groups()\n            self.select_default_variables()\n            self.openContext(self.dataset)\n            self.grouping_changed()\n            self.attr_changed()\n        else:\n            self.reset_all_data()\n        self.commit()\n\n    def select_default_variables(self):\n        # visualize first non-class variable, group by class (if present)\n        domain = self.dataset.domain\n        if len(self.attrs) > len(domain.class_vars):\n            self.attribute = self.attrs[len(domain.class_vars)]\n        elif self.attrs:\n            self.attribute = self.attrs[0]\n\n        if domain.class_var and domain.class_var.is_discrete:\n            self.group_var = domain.class_var\n        else:\n            self.group_var = None  # Reset to trigger selection via callback\n\n    def apply_attr_sorting(self):\n        def compute_score(attr):\n            # This function and the one in apply_group_sorting are similar, but\n            # different in too many details, so they are kept as separate\n            # functions.\n            # If you discover a bug in this function, check the other one, too.\n            if attr is group_var:\n                return 3\n            if attr.is_continuous:\n                # One-way ANOVA\n                col = data.get_column_view(attr)[0].astype(float)\n                groups = (col[group_col == i] for i in range(n_groups))\n                groups = (col[~np.isnan(col)] for col in groups)\n                groups = [group for group in groups if len(group)]\n                p = f_oneway(*groups)[1] if len(groups) > 1 else 2\n            else:\n                p = self._chi_square(group_var, attr)[1]\n            if math.isnan(p):\n                return 2\n            return p\n\n        data = self.dataset\n        if data is None:\n            return\n        domain = data.domain\n        attribute = self.attribute\n        group_var = self.group_var\n        if self.order_by_importance and group_var is not None:\n            n_groups = len(group_var.values)\n            group_col = data.get_column_view(group_var)[0] if \\\n                domain.has_continuous_attributes(\n                    include_class=True, include_metas=True) else None\n            self.attrs.sort(key=compute_score)\n        else:\n            self.reset_attrs()\n        self.attribute = attribute  # reset selection\n        self._ensure_selection_visible(self.controls.attribute)\n\n    def on_group_sorting_checkbox(self):\n        if self.order_grouping_by_importance:\n            self.apply_group_sorting()\n        else:\n            self.reset_groups()\n            self.group_var = self.group_var  # reset selection\n            self._ensure_selection_visible(self.controls.group_var)\n\n    def apply_group_sorting(self):\n        def compute_stat(group):\n            # This function and the one in apply_attr_sorting are similar, but\n            # different in too many details, so they are kept as separate\n            # functions.\n            # If you discover a bug in this function, check the other one, too.\n            if group is attr:\n                return 3\n            if group is None:\n                return -1\n            if attr.is_continuous:\n                group_col = data.get_column_view(group)[0].astype(float)\n                groups = (attr_col[group_col == i]\n                          for i in range(len(group.values)))\n                groups = (col[~np.isnan(col)] for col in groups)\n                groups = [group for group in groups if len(group)]\n                p = f_oneway(*groups)[1] if len(groups) > 1 else 2\n            else:\n                p = self._chi_square(group, attr)[1]\n            if math.isnan(p):\n                return 2\n            return p\n\n        data = self.dataset\n        if data is None or not self.order_grouping_by_importance:\n            return\n        attr = self.attribute\n        group_var = self.group_var\n        if attr.is_continuous:\n            attr_col = data.get_column_view(attr)[0].astype(float)\n        self.group_vars.sort(key=compute_stat)\n        self.group_var = group_var  # reset selection\n        self._ensure_selection_visible(self.controls.group_var)\n\n    @staticmethod\n    def _ensure_selection_visible(view):\n        selection = view.selectedIndexes()\n        if len(selection) == 1:\n            view.scrollTo(selection[0])\n\n    def _chi_square(self, group_var, attr):\n        # Chi-square with the given distribution into groups\n        if not attr.values or not group_var.values:\n            return 0, 2, 0\n        observed = np.array(\n            contingency.get_contingency(self.dataset, group_var, attr))\n        observed = observed[observed.sum(axis=1) != 0, :]\n        observed = observed[:, observed.sum(axis=0) != 0]\n        if min(observed.shape) < 2:\n            return 0, 2, 0\n        return chi2_contingency(observed)[:3]\n\n    def reset_all_data(self):\n        self.clear_scene()\n        self.stat_test = \"\"\n        self.attrs[:] = []\n        self.group_vars[:] = [None]\n        self.is_continuous = False\n        self.update_display_box()\n\n    def grouping_changed(self):\n        self.controls.stretched.setDisabled(self.group_var is self.attribute)\n        self.apply_attr_sorting()\n        self.update_graph()\n\n    def select_box_items(self):\n        temp_cond = self.conditions.copy()\n        for box in self.box_scene.items():\n            if isinstance(box, FilterGraphicsRectItem):\n                box.setSelected(box.filter.conditions in\n                                [c.conditions for c in temp_cond])\n\n    def attr_changed(self):\n        self.controls.stretched.setDisabled(self.group_var is self.attribute)\n        self.apply_group_sorting()\n        self.update_graph()\n\n    def update_graph(self):\n        self.compute_box_data()\n        self.update_display_box()\n        self.layout_changed()\n\n        if self.is_continuous:\n            heights = 90 if self.show_annotations else 60\n            self.box_view.centerOn(self.scene_min_x + self.scene_width / 2,\n                                   -30 - len(self.stats) * heights / 2 + 45)\n        else:\n            self.box_view.centerOn(self.scene_width / 2,\n                                   -30 - len(self.boxes) * 40 / 2 + 45)\n\n    def compute_box_data(self):\n        attr = self.attribute\n        if not attr:\n            return\n        dataset = self.dataset\n        self.is_continuous = attr.is_continuous\n        if dataset is None or not self.is_continuous and not attr.values or \\\n                        self.group_var and not self.group_var.values:\n            self.stats = self.dist = self.conts = []\n            return\n        if self.group_var:\n            self.dist = []\n            self.conts = contingency.get_contingency(\n                dataset, attr, self.group_var)\n            if self.is_continuous:\n                stats, label_texts = [], []\n                for i, cont in enumerate(self.conts):\n                    if np.sum(cont[1]):\n                        stats.append(BoxData(cont, attr, i, self.group_var))\n                        label_texts.append(self.group_var.values[i])\n                self.stats = stats\n                self.label_txts_all = label_texts\n            else:\n                self.label_txts_all = \\\n                    [v for v, c in zip(\n                        self.group_var.values + [\"Missing values\"],\n                        self.conts.array_with_unknowns)\n                     if np.sum(c) > 0]\n        else:\n            self.dist = distribution.get_distribution(dataset, attr)\n            self.conts = []\n            if self.is_continuous:\n                self.stats = [BoxData(self.dist, attr, None)]\n            self.label_txts_all = [\"\"]\n        self.label_txts = [txts for stat, txts in zip(self.stats,\n                                                      self.label_txts_all)\n                           if stat.n > 0]\n        self.stats = [stat for stat in self.stats if stat.n > 0]\n\n    def update_display_box(self):\n        if self.is_continuous:\n            self.stretching_box.hide()\n            self.display_box.show()\n            self.compare_rb.setEnabled(self.group_var is not None)\n        else:\n            self.stretching_box.show()\n            self.display_box.hide()\n            self.sort_cb.setEnabled(self.group_var is not None)\n\n    def clear_scene(self):\n        self.closeContext()\n        self.box_scene.clearSelection()\n        self.box_scene.clear()\n        self.box_view.viewport().update()\n        self.attr_labels = []\n        self.labels = []\n        self.boxes = []\n        self.mean_labels = []\n        self.posthoc_lines = []\n        self.openContext(self.dataset)\n\n    def layout_changed(self):\n        attr = self.attribute\n        if not attr:\n            return\n        self.clear_scene()\n        if self.dataset is None or len(self.conts) == len(self.dist) == 0:\n            return\n\n        if self.is_continuous:\n            self.mean_labels = [self.mean_label(stat, attr, lab)\n                                for stat, lab in zip(self.stats, self.label_txts)]\n            self.draw_axis()\n            self.boxes = [self.box_group(stat) for stat in self.stats]\n            self.labels = [self.label_group(stat, attr, mean_lab)\n                           for stat, mean_lab in zip(self.stats, self.mean_labels)]\n            self.attr_labels = [QGraphicsSimpleTextItem(lab)\n                                for lab in self.label_txts]\n            for it in chain(self.labels, self.attr_labels):\n                self.box_scene.addItem(it)\n\n        self.display_changed()\n\n    def display_changed(self):\n        if self.dataset is None or self.attribute is None:\n            return\n\n        if self.is_continuous:\n            self._display_changed_cont()\n        else:\n            self._display_changed_disc()\n        self.draw_stat()\n        self.select_box_items()\n\n    def _display_changed_cont(self):\n        self.order = list(range(len(self.stats)))\n        criterion = self._sorting_criteria_attrs[self.compare]\n        if criterion:\n            vals = [getattr(stat, criterion) for stat in self.stats]\n            overmax = max((val for val in vals if val is not None), default=0) \\\n                      + 1\n            vals = [val if val is not None else overmax for val in vals]\n            self.order = sorted(self.order, key=vals.__getitem__)\n\n        heights = 90 if self.show_annotations else 60\n\n        for row, box_index in enumerate(self.order):\n            y = (-len(self.stats) + row) * heights + 10\n            for item in self.boxes[box_index]:\n                self.box_scene.addItem(item)\n                item.setY(y)\n            labels = self.labels[box_index]\n\n            if self.show_annotations:\n                labels.show()\n                labels.setY(y)\n            else:\n                labels.hide()\n\n            label = self.attr_labels[box_index]\n            label.setY(y - 15 - label.boundingRect().height())\n            if self.show_annotations:\n                label.hide()\n            else:\n                stat = self.stats[box_index]\n\n                if self.compare == OWBoxPlot.CompareMedians and \\\n                        stat.median is not None:\n                    pos = stat.median + 5 / self.scale_x\n                elif self.compare == OWBoxPlot.CompareMeans or stat.q25 is None:\n                    pos = stat.mean + 5 / self.scale_x\n                else:\n                    pos = stat.q25\n                label.setX(pos * self.scale_x)\n                label.show()\n\n        r = QRectF(self.scene_min_x, -30 - len(self.stats) * heights,\n                   self.scene_width, len(self.stats) * heights + 90)\n        self.box_scene.setSceneRect(r)\n\n        self._compute_tests_cont()\n        self._show_posthoc()\n\n    def _display_changed_disc(self):\n        self.clear_scene()\n        self.attr_labels = [QGraphicsSimpleTextItem(lab)\n                            for lab in self.label_txts_all]\n\n        if not self.show_stretched:\n            if self.group_var:\n                self.labels = [\n                    QGraphicsTextItem(\"{}\".format(int(sum(cont))))\n                    for cont in self.conts.array_with_unknowns\n                    if np.sum(cont) > 0]\n            else:\n                self.labels = [\n                    QGraphicsTextItem(str(int(sum(self.dist))))]\n\n        self.order = list(range(len(self.attr_labels)))\n\n        self.draw_axis_disc()\n        if self.group_var:\n            self.boxes = \\\n                [self.strudel(cont, i)\n                 for i, cont in enumerate(self.conts.array_with_unknowns)\n                 if np.sum(cont) > 0]\n\n            if self.sort_freqs:\n                # pylint: disable=invalid-unary-operand-type\n                self.order = sorted(\n                    self.order, key=(-np.sum(\n                        self.conts.array_with_unknowns, axis=1)).__getitem__)\n        else:\n            self.boxes = [self.strudel(self.dist.array_with_unknowns)]\n\n        for row, box_index in enumerate(self.order):\n            y = (-len(self.boxes) + row) * 40 + 10\n            box = self.boxes[box_index]\n            bars, labels = box[::2], box[1::2]\n\n            self.__draw_group_labels(y, box_index)\n            if not self.show_stretched:\n                self.__draw_row_counts(y, box_index)\n            if self.show_labels and self.attribute is not self.group_var:\n                self.__draw_bar_labels(y, bars, labels)\n            self.__draw_bars(y, bars)\n\n        self.box_scene.setSceneRect(-self.label_width - 5,\n                                    -30 - len(self.boxes) * 40,\n                                    self.scene_width, len(self.boxes * 40) + 90)\n        self._compute_tests_disc()\n\n    def __draw_group_labels(self, y, row):\n        \"\"\"Draw group labels\n\n        Parameters\n        ----------\n        y: int\n            vertical offset of bars\n        row: int\n            row index\n        \"\"\"\n        label = self.attr_labels[row]\n        b = label.boundingRect()\n        label.setPos(-b.width() - 10, y - b.height() / 2)\n        self.box_scene.addItem(label)\n\n    def __draw_row_counts(self, y, row):\n        \"\"\"Draw row counts\n\n        Parameters\n        ----------\n        y: int\n            vertical offset of bars\n        row: int\n            row index\n        \"\"\"\n        assert not self.is_continuous\n        label = self.labels[row]\n        b = label.boundingRect()\n        if self.group_var:\n            right = self.scale_x * sum(self.conts.array_with_unknowns[row])\n        else:\n            right = self.scale_x * sum(self.dist)\n        label.setPos(right + 10, y - b.height() / 2)\n        self.box_scene.addItem(label)\n\n    def __draw_bar_labels(self, y, bars, labels):\n        \"\"\"Draw bar labels\n\n        Parameters\n        ----------\n        y: int\n            vertical offset of bars\n        bars: List[FilterGraphicsRectItem]\n            list of bars being drawn\n        labels: List[QGraphicsTextItem]\n            list of labels for corresponding bars\n        \"\"\"\n        label = bar_part = None\n        for text_item, bar_part in zip(labels, bars):\n            label = self.Label(\n                text_item.toPlainText())\n            label.setPos(bar_part.boundingRect().x(),\n                         y - label.boundingRect().height() - 8)\n            label.setMaxWidth(bar_part.boundingRect().width())\n            self.box_scene.addItem(label)\n\n    def __draw_bars(self, y, bars):\n        \"\"\"Draw bars\n\n        Parameters\n        ----------\n        y: int\n            vertical offset of bars\n\n        bars: List[FilterGraphicsRectItem]\n            list of bars to draw\n        \"\"\"\n        for item in bars:\n            item.setPos(0, y)\n            self.box_scene.addItem(item)\n\n    # noinspection PyPep8Naming\n    def _compute_tests_cont(self):\n        # The t-test and ANOVA are implemented here since they efficiently use\n        # the widget-specific data in self.stats.\n        # The non-parametric tests can't do this, so we use statistics.tests\n\n        # pylint: disable=comparison-with-itself\n        def stat_ttest():\n            d1, d2 = self.stats\n            if d1.n < 2 or d2.n < 2:\n                return np.nan, np.nan\n            pooled_var = d1.var / d1.n + d2.var / d2.n\n            # pylint: disable=comparison-with-itself\n            if pooled_var == 0 or np.isnan(pooled_var):\n                return np.nan, np.nan\n            df = pooled_var ** 2 / \\\n                ((d1.var / d1.n) ** 2 / (d1.n - 1) +\n                 (d2.var / d2.n) ** 2 / (d2.n - 1))\n            t = abs(d1.mean - d2.mean) / math.sqrt(pooled_var)\n            p = 2 * (1 - scipy.special.stdtr(df, t))\n            return t, p\n\n        # TODO: Check this function\n        # noinspection PyPep8Naming\n        def stat_ANOVA():\n            if any(stat.n == 0 for stat in self.stats):\n                return np.nan, np.nan\n            n = sum(stat.n for stat in self.stats)\n            grand_avg = sum(stat.n * stat.mean for stat in self.stats) / n\n            var_between = sum(stat.n * (stat.mean - grand_avg) ** 2\n                              for stat in self.stats)\n            df_between = len(self.stats) - 1\n\n            var_within = sum(stat.n * stat.var for stat in self.stats)\n            df_within = n - len(self.stats)\n            if var_within == 0 or df_within == 0 or df_between == 0:\n                return np.nan, np.nan\n            F = (var_between / df_between) / (var_within / df_within)\n            p = 1 - scipy.special.fdtr(df_between, df_within, F)\n            return F, p\n\n        n = len(self.dataset)\n        if self.compare == OWBoxPlot.CompareNone or len(self.stats) < 2:\n            t = \"\"\n        elif any(s.n <= 1 for s in self.stats):\n            t = \"At least one group has just one instance, \" \\\n                \"cannot compute significance\"\n        elif len(self.stats) == 2:\n            if self.compare == OWBoxPlot.CompareMedians:\n                t = \"\"\n                # z, p = tests.wilcoxon_rank_sum(\n                #    self.stats[0].dist, self.stats[1].dist)\n                # t = \"Mann-Whitney's z: %.1f (p=%.3f)\" % (z, p)\n            else:\n                t, p = stat_ttest()\n                t = \"\" if np.isnan(t) else f\"Student's t: {t:.3f} (p={p:.3f}, N={n})\"\n        else:\n            if self.compare == OWBoxPlot.CompareMedians:\n                t = \"\"\n                # U, p = -1, -1\n                # t = \"Kruskal Wallis's U: %.1f (p=%.3f)\" % (U, p)\n            else:\n                F, p = stat_ANOVA()\n                t = \"\" if np.isnan(F) else f\"ANOVA: {F:.3f} (p={p:.3f}, N={n})\"\n        self.stat_test = t\n\n    def _compute_tests_disc(self):\n        if self.group_var is None or self.attribute is None:\n            self.stat_test = \"\"\n        else:\n            chi, p, dof = self._chi_square(self.group_var, self.attribute)\n            if np.isnan(p):\n                self.stat_test = \"\"\n            else:\n                self.stat_test = f\"\u03c7\u00b2: {chi:.2f} (p={p:.3f}, dof={dof})\"\n\n    def mean_label(self, stat, attr, val_name):\n        label = QGraphicsItemGroup()\n        t = QGraphicsSimpleTextItem(attr.str_val(stat.mean), label)\n        t.setFont(self._label_font)\n        bbox = t.boundingRect()\n        w2, h = bbox.width() / 2, bbox.height()\n        t.setPos(-w2, -h)\n        tpm = QGraphicsSimpleTextItem(\n            \" \\u00b1 \" + \"%.*f\" % (attr.number_of_decimals + 1, stat.dev),\n            label)\n        tpm.setFont(self._label_font)\n        tpm.setPos(w2, -h)\n        if val_name:\n            vnm = QGraphicsSimpleTextItem(val_name + \": \", label)\n            vnm.setFont(self._label_font)\n            vnm.setBrush(self._attr_brush)\n            vb = vnm.boundingRect()\n            label.min_x = -w2 - vb.width()\n            vnm.setPos(label.min_x, -h)\n        else:\n            label.min_x = -w2\n        return label\n\n    def draw_axis(self):\n        \"\"\"Draw the horizontal axis and sets self.scale_x\"\"\"\n        misssing_stats = not self.stats\n        stats = self.stats or [BoxData(np.array([[0.], [1.]]), self.attribute)]\n        mean_labels = self.mean_labels or [self.mean_label(stats[0], self.attribute, \"\")]\n        bottom = min(stat.a_min for stat in stats)\n        top = max(stat.a_max for stat in stats)\n\n        first_val, step = compute_scale(bottom, top)\n        while bottom <= first_val:\n            first_val -= step\n        bottom = first_val\n        no_ticks = math.ceil((top - first_val) / step) + 1\n        top = max(top, first_val + no_ticks * step)\n\n        gbottom = min(bottom, min(stat.mean - stat.dev for stat in stats))\n        gtop = max(top, max(stat.mean + stat.dev for stat in stats))\n\n        bv = self.box_view\n        viewrect = bv.viewport().rect().adjusted(15, 15, -15, -30)\n        self.scale_x = scale_x = viewrect.width() / (gtop - gbottom)\n\n        # In principle we should repeat this until convergence since the new\n        # scaling is too conservative. (No chance am I doing this.)\n        mlb = min(stat.mean + mean_lab.min_x / scale_x\n                  for stat, mean_lab in zip(stats, mean_labels))\n        if mlb < gbottom:\n            gbottom = mlb\n            self.scale_x = scale_x = viewrect.width() / (gtop - gbottom)\n\n        self.scene_min_x = gbottom * scale_x\n        self.scene_max_x = gtop * scale_x\n        self.scene_width = self.scene_max_x - self.scene_min_x\n\n        val = first_val\n        last_text = self.scene_min_x\n        while True:\n            l = self.box_scene.addLine(val * scale_x, -1, val * scale_x, 1,\n                                       self._pen_axis_tick)\n            l.setZValue(100)\n            t = QGraphicsSimpleTextItem(\n                self.attribute.str_val(val) if not misssing_stats else \"?\")\n            t.setFont(self._axis_font)\n            t.setFlag(QGraphicsItem.ItemIgnoresTransformations)\n            r = t.boundingRect()\n            x_start = val * scale_x - r.width() / 2\n            x_finish = x_start + r.width()\n            if x_start > last_text + 10 and x_finish < self.scene_max_x:\n                t.setPos(x_start, 8)\n                self.box_scene.addItem(t)\n                last_text = x_finish\n            if val >= top:\n                break\n            val += step\n        self.box_scene.addLine(\n            bottom * scale_x - 4, 0, top * scale_x + 4, 0, self._pen_axis)\n\n    def draw_stat(self):\n        if self.stat_test:\n            label = QGraphicsSimpleTextItem(self.stat_test)\n            brect = self.box_scene.sceneRect()\n            label.setPos(brect.center().x() - label.boundingRect().width()/2,\n                         8 + self._axis_font.pixelSize()*2)\n            label.setFlag(QGraphicsItem.ItemIgnoresTransformations)\n            self.box_scene.addItem(label)\n\n    def draw_axis_disc(self):\n        \"\"\"\n        Draw the horizontal axis and sets self.scale_x for discrete attributes\n        \"\"\"\n        assert not self.is_continuous\n        if self.show_stretched:\n            if not self.attr_labels:\n                return\n            step = steps = 10\n        else:\n            if self.group_var:\n                max_box = max(float(np.sum(dist))\n                              for dist in self.conts.array_with_unknowns)\n            else:\n                max_box = float(np.sum(self.dist.array_with_unknowns))\n            if max_box == 0:\n                self.scale_x = 1\n                return\n            _, step = compute_scale(0, max_box)\n            step = int(step) if step > 1 else 1\n            steps = int(math.ceil(max_box / step))\n        max_box = step * steps\n\n        bv = self.box_view\n        viewrect = bv.viewport().rect().adjusted(15, 15, -15, -30)\n        self.scene_width = viewrect.width()\n\n        lab_width = max(lab.boundingRect().width() for lab in self.attr_labels)\n        lab_width = max(lab_width, 40)\n        lab_width = min(lab_width, self.scene_width / 3)\n        self.label_width = lab_width\n\n        right_offset = 0  # offset for the right label\n        if not self.show_stretched and self.labels:\n            if self.group_var:\n                rows = list(zip(self.conts.array_with_unknowns, self.labels))\n            else:\n                rows = [(self.dist, self.labels[0])]\n            # available space left of the 'group labels'\n            available = self.scene_width - lab_width - 10\n            scale_x = (available - right_offset) / max_box\n            max_right = max(sum(dist) * scale_x + 10 +\n                            lbl.boundingRect().width()\n                            for dist, lbl in rows)\n            right_offset = max(0, max_right - max_box * scale_x)\n\n        self.scale_x = scale_x = \\\n            (self.scene_width - lab_width - 10 - right_offset) / max_box\n\n        self.box_scene.addLine(0, 0, max_box * scale_x, 0, self._pen_axis)\n        for val in range(0, step * steps + 1, step):\n            l = self.box_scene.addLine(val * scale_x, -1, val * scale_x, 1,\n                                       self._pen_axis_tick)\n            l.setZValue(100)\n            t = self.box_scene.addSimpleText(str(val), self._axis_font)\n            t.setPos(val * scale_x - t.boundingRect().width() / 2, 8)\n        if self.show_stretched:\n            self.scale_x *= 100\n\n    def label_group(self, stat, attr, mean_lab):\n        def centered_text(val, pos):\n            t = QGraphicsSimpleTextItem(attr.str_val(val), labels)\n            t.setFont(self._label_font)\n            bbox = t.boundingRect()\n            t.setPos(pos - bbox.width() / 2, 22)\n            return t\n\n        def line(x, down=1):\n            QGraphicsLineItem(x, 12 * down, x, 20 * down, labels)\n\n        def move_label(label, frm, to):\n            label.setX(to)\n            to += t_box.width() / 2\n            path = QPainterPath()\n            path.lineTo(0, 4)\n            path.lineTo(to - frm, 4)\n            path.lineTo(to - frm, 8)\n            p = QGraphicsPathItem(path)\n            p.setPos(frm, 12)\n            labels.addToGroup(p)\n\n        labels = QGraphicsItemGroup()\n\n        labels.addToGroup(mean_lab)\n        m = stat.mean * self.scale_x\n        mean_lab.setPos(m, -22)\n        line(m, -1)\n\n        if stat.median is not None:\n            msc = stat.median * self.scale_x\n            med_t = centered_text(stat.median, msc)\n            med_box_width2 = med_t.boundingRect().width() / 2\n            line(msc)\n\n        if stat.q25 is not None:\n            x = stat.q25 * self.scale_x\n            t = centered_text(stat.q25, x)\n            t_box = t.boundingRect()\n            med_left = msc - med_box_width2\n            if x + t_box.width() / 2 >= med_left - 5:\n                move_label(t, x, med_left - t_box.width() - 5)\n            else:\n                line(x)\n\n        if stat.q75 is not None:\n            x = stat.q75 * self.scale_x\n            t = centered_text(stat.q75, x)\n            t_box = t.boundingRect()\n            med_right = msc + med_box_width2\n            if x - t_box.width() / 2 <= med_right + 5:\n                move_label(t, x, med_right + 5)\n            else:\n                line(x)\n\n        return labels\n\n    def box_group(self, stat, height=20):\n        def line(x0, y0, x1, y1, *args):\n            return QGraphicsLineItem(x0 * scale_x, y0, x1 * scale_x, y1, *args)\n\n        scale_x = self.scale_x\n        box = []\n        whisker1 = line(stat.a_min, -1.5, stat.a_min, 1.5)\n        whisker2 = line(stat.a_max, -1.5, stat.a_max, 1.5)\n        vert_line = line(stat.a_min, 0, stat.a_max, 0)\n        mean_line = line(stat.mean, -height / 3, stat.mean, height / 3)\n        for it in (whisker1, whisker2, mean_line):\n            it.setPen(self._pen_paramet)\n        vert_line.setPen(self._pen_dotted)\n        var_line = line(stat.mean - stat.dev, 0, stat.mean + stat.dev, 0)\n        var_line.setPen(self._pen_paramet)\n        box.extend([whisker1, whisker2, vert_line, mean_line, var_line])\n        if stat.q25 is not None or stat.q75 is not None:\n            # if any of them is None it means that its value is equal to median\n            box_from = stat.q25 or stat.median\n            box_to = stat.q75 or stat.median\n            mbox = FilterGraphicsRectItem(\n                stat.conditions, box_from * scale_x, -height / 2,\n                (box_to - box_from) * scale_x, height)\n            mbox.setBrush(self._box_brush)\n            mbox.setPen(QPen(Qt.NoPen))\n            mbox.setZValue(-200)\n            box.append(mbox)\n\n        if stat.median is not None:\n            median_line = line(stat.median, -height / 2,\n                               stat.median, height / 2)\n            median_line.setPen(self._pen_median)\n            median_line.setZValue(-150)\n            box.append(median_line)\n\n        return box\n\n    def strudel(self, dist, group_val_index=None):\n        attr = self.attribute\n        ss = np.sum(dist)\n        box = []\n        if ss < 1e-6:\n            cond = [FilterDiscrete(attr, None)]\n            if group_val_index is not None:\n                cond.append(FilterDiscrete(self.group_var, [group_val_index]))\n            box.append(FilterGraphicsRectItem(cond, 0, -10, 1, 10))\n        cum = 0\n        values = attr.values + [\"Missing values\"]\n        colors = np.vstack((attr.colors, [128, 128, 128]))\n        for i, v in enumerate(dist):\n            if v < 1e-6:\n                continue\n            if self.show_stretched:\n                v /= ss\n            v *= self.scale_x\n            cond = [FilterDiscrete(attr, [i])]\n            if group_val_index is not None:\n                cond.append(FilterDiscrete(self.group_var, [group_val_index]))\n            rect = FilterGraphicsRectItem(cond, cum + 1, -6, v - 2, 12)\n            rect.setBrush(QBrush(QColor(*colors[i])))\n            rect.setPen(QPen(Qt.NoPen))\n            if self.show_stretched:\n                tooltip = \"{}: {:.2f}%\".format(\n                    values[i],\n                    100 * dist[i] / sum(dist))\n            else:\n                tooltip = \"{}: {}\".format(values[i], int(dist[i]))\n            rect.setToolTip(tooltip)\n            text = QGraphicsTextItem(values[i])\n            box.append(rect)\n            box.append(text)\n            cum += v\n        return box\n\n    def commit(self):\n        self.conditions = [item.filter for item in\n                           self.box_scene.selectedItems() if item.filter]\n        selected, selection = None, []\n        if self.conditions:\n            selected = Values(self.conditions, conjunction=False)(self.dataset)\n            selection = np.in1d(\n                self.dataset.ids, selected.ids, assume_unique=True).nonzero()[0]\n        self.Outputs.selected_data.send(selected)\n        self.Outputs.annotated_data.send(\n            create_annotated_table(self.dataset, selection))\n\n    def _show_posthoc(self):\n        def line(y0, y1):\n            it = self.box_scene.addLine(x, y0, x, y1, self._post_line_pen)\n            it.setZValue(-100)\n            self.posthoc_lines.append(it)\n\n        while self.posthoc_lines:\n            self.box_scene.removeItem(self.posthoc_lines.pop())\n\n        if self.compare == OWBoxPlot.CompareNone or len(self.stats) < 2:\n            return\n\n        if self.compare == OWBoxPlot.CompareMedians:\n            crit_line = \"median\"\n        else:\n            crit_line = \"mean\"\n\n        xs = []\n\n        height = 90 if self.show_annotations else 60\n\n        y_up = -len(self.stats) * height + 10\n        for pos, box_index in enumerate(self.order):\n            stat = self.stats[box_index]\n            x = getattr(stat, crit_line)\n            if x is None:\n                continue\n            x *= self.scale_x\n            xs.append(x * self.scale_x)\n            by = y_up + pos * height\n            line(by + 12, 0)\n\n        used_to = []\n        last_to = to = 0\n        for frm, frm_x in enumerate(xs[:-1]):\n            for to in range(frm + 1, len(xs)):\n                if xs[to] - frm_x > 1.5:\n                    to -= 1\n                    break\n            if to in (last_to, frm):\n                continue\n            for rowi, used in enumerate(used_to):\n                if used < frm:\n                    used_to[rowi] = to\n                    break\n            else:\n                rowi = len(used_to)\n                used_to.append(to)\n            y = - 6 - rowi * 6\n            it = self.box_scene.addLine(frm_x - 2, y, xs[to] + 2, y,\n                                        self._post_grp_pen)\n            self.posthoc_lines.append(it)\n            last_to = to\n\n    def get_widget_name_extension(self):\n        return self.attribute.name if self.attribute else None\n\n    def send_report(self):\n        self.report_plot()\n        text = \"\"\n        if self.attribute:\n            text += \"Box plot for attribute '{}' \".format(self.attribute.name)\n        if self.group_var:\n            text += \"grouped by '{}'\".format(self.group_var.name)\n        if text:\n            self.report_caption(text)\n\n    class Label(QGraphicsSimpleTextItem):\n        \"\"\"Boxplot Label with settable maxWidth\"\"\"\n        # Minimum width to display label text\n        MIN_LABEL_WIDTH = 25\n\n        # padding bellow the text\n        PADDING = 3\n\n        __max_width = None\n\n        def maxWidth(self):\n            return self.__max_width\n\n        def setMaxWidth(self, max_width):\n            self.__max_width = max_width\n\n        def paint(self, painter, option, widget):\n            \"\"\"Overrides QGraphicsSimpleTextItem.paint\n\n            If label text is too long, it is elided\n            to fit into the allowed region\n            \"\"\"\n            if self.__max_width is None:\n                width = option.rect.width()\n            else:\n                width = self.__max_width\n\n            if width < self.MIN_LABEL_WIDTH:\n                # if space is too narrow, no label\n                return\n\n            fm = painter.fontMetrics()\n            text = fm.elidedText(self.text(), Qt.ElideRight, width)\n            painter.drawText(\n                option.rect.x(),\n                option.rect.y() + self.boundingRect().height() - self.PADDING,\n                text)\n\n\nif __name__ == \"__main__\":  # pragma: no cover\n    WidgetPreview(OWBoxPlot).run(Orange.data.Table(\"heart_disease.tab\"))\n"}
A user will ask you to solve a task. You should first draft your thinking process (inner monologue). Then, generate the solution.

Your response format must follow the template below:
<think>
Your thoughts or/and draft, like working through an exercise on scratch paper. Be as casual and as long as you want until you are confident to generate a correct solution.
</think>
<solution>
Final solution presented to the user.
</solution>
system
We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
[ISSUE]
**Title:** BoxPlot Widget Displays Hidden Attributes Unexpectedly

**Description:**
When certain attributes in the dataset are marked as hidden, the BoxPlot widget still displays them. This results in cluttered visualizations and does not align with the intended behavior of hiding specific attributes.

**Example Buggy Code:**
```python
iris = Table("iris")
iris.domain["iris"].attributes["hidden"] = True
iris.domain["petal length"].attributes["hidden"] = True
self.send_signal(self.widget.Inputs.data, iris)
```
In this example, the attributes "iris" and "petal length" are set to hidden. After sending the data to the widget, these hidden attributes should not appear in the BoxPlot visualization.

**Expected Behavior:**
Only non-hidden attributes should be displayed in the BoxPlot widget. In this case, after hiding two attributes, only three should be visible.

**Actual Behavior:**
All five attributes, including the ones marked as hidden, are displayed in the BoxPlot widget. This causes the test to fail with an assertion error, indicating that the number of displayed attributes does not match the expected count.

[/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/owboxplot.py
import math
from itertools import chain
import numpy as np

from AnyQt.QtWidgets import (
    QGraphicsView, QGraphicsScene, QGraphicsItem, QGraphicsSimpleTextItem,
    QGraphicsTextItem, QGraphicsItemGroup, QGraphicsLineItem,
    QGraphicsPathItem, QGraphicsRectItem, QSizePolicy
)
from AnyQt.QtGui import QPen, QColor, QBrush, QPainterPath, QPainter, QFont
from AnyQt.QtCore import Qt, QEvent, QRectF, QSize

import scipy.special
from scipy.stats import f_oneway, chi2_contingency

import Orange.data
from Orange.data.filter import FilterDiscrete, FilterContinuous, Values
from Orange.statistics import contingency, distribution

from Orange.widgets import widget, gui
from Orange.widgets.settings import (Setting, DomainContextHandler,
                                     ContextSetting)
from Orange.widgets.utils.itemmodels import VariableListModel
from Orange.widgets.utils.annotated_data import (create_annotated_table,
                                                 ANNOTATED_DATA_SIGNAL_NAME)
from Orange.widgets.utils.widgetpreview import WidgetPreview
from Orange.widgets.widget import Input, Output


def compute_scale(min_, max_):
    if min_ == max_:
        return math.floor(min_), 1
    magnitude = int(3 * math.log10(abs(max_ - min_)) + 1)
    if magnitude % 3 == 0:
        first_place = 1
    elif magnitude % 3 == 1:
        first_place = 2
    else:
        first_place = 5
    magnitude = magnitude // 3 - 1
    step = first_place * pow(10, magnitude)
    first_val = math.ceil(min_ / step) * step
    return first_val, step


def _quantiles(a, freq, q, interpolation="midpoint"):
    """
    Somewhat like np.quantiles, but with explicit sample frequencies.

    * Only 'higher', 'lower' and 'midpoint' interpolation.
    * `a` MUST be sorted.
    """
    a = np.asarray(a)
    freq = np.asarray(freq)
    assert a.size > 0 and a.size == freq.size
    cumdist = np.cumsum(freq)
    cumdist /= cumdist[-1]

    if interpolation == "midpoint":  # R quantile(..., type=2)
        left = np.searchsorted(cumdist, q, side="left")
        right = np.searchsorted(cumdist, q, side="right")
        # no mid point for the right most position
        np.clip(right, 0, a.size - 1, out=right)
        # right and left will be different only on the `q` boundaries
        # (excluding the right most sample)
        return (a[left] + a[right]) / 2
    elif interpolation == "higher":  # R quantile(... type=1)
        right = np.searchsorted(cumdist, q, side="right")
        np.clip(right, 0, a.size - 1, out=right)
        return a[right]
    elif interpolation == "lower":
        left = np.searchsorted(cumdist, q, side="left")
        return a[left]
    else:  # pragma: no cover
        raise ValueError("invalid interpolation: '{}'".format(interpolation))


class BoxData:
    def __init__(self, dist, attr, group_val_index=None, group_var=None):
        self.dist = dist
        self.n = n = np.sum(dist[1])
        if n == 0:
            return
        self.a_min = float(dist[0, 0])
        self.a_max = float(dist[0, -1])
        self.mean = float(np.sum(dist[0] * dist[1]) / n)
        self.var = float(np.sum(dist[1] * (dist[0] - self.mean) ** 2) / n)
        self.dev = math.sqrt(self.var)
        a, freq = np.asarray(dist)
        q25, median, q75 = _quantiles(a, freq, [0.25, 0.5, 0.75])
        self.median = median
        # The code below omits the q25 or q75 in the plot when they are None
        self.q25 = None if q25 == median else q25
        self.q75 = None if q75 == median else q75
        self.conditions = [FilterContinuous(attr, FilterContinuous.Between,
                                            self.q25, self.q75)]
        if group_val_index is not None:
            self.conditions.append(FilterDiscrete(group_var, [group_val_index]))


class FilterGraphicsRectItem(QGraphicsRectItem):
    def __init__(self, conditions, *args):
        super().__init__(*args)
        self.filter = Values(conditions) if conditions else None
        self.setFlag(QGraphicsItem.ItemIsSelectable)


class OWBoxPlot(widget.OWWidget):
    """
    Here's how the widget's functions call each other:

    - `set_data` is a signal handler fills the list boxes and calls
    `grouping_changed`.

    - `grouping_changed` handles changes of grouping attribute: it enables or
    disables the box for ordering, orders attributes and calls `attr_changed`.

    - `attr_changed` handles changes of attribute. It recomputes box data by
    calling `compute_box_data`, shows the appropriate display box
    (discrete/continuous) and then calls`layout_changed`

    - `layout_changed` constructs all the elements for the scene (as lists of
    QGraphicsItemGroup) and calls `display_changed`. It is called when the
    attribute or grouping is changed (by attr_changed) and on resize event.

    - `display_changed` puts the elements corresponding to the current display
    settings on the scene. It is called when the elements are reconstructed
    (layout is changed due to selection of attributes or resize event), or
    when the user changes display settings or colors.

    For discrete attributes, the flow is a bit simpler: the elements are not
    constructed in advance (by layout_changed). Instead, _display_changed_disc
    draws everything.
    """
    name = "Box Plot"
    description = "Visualize the distribution of feature values in a box plot."
    icon = "icons/BoxPlot.svg"
    priority = 100
    keywords = ["whisker"]

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

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

    #: Comparison types for continuous variables
    CompareNone, CompareMedians, CompareMeans = 0, 1, 2

    settingsHandler = DomainContextHandler()
    conditions = ContextSetting([])

    attribute = ContextSetting(None)
    order_by_importance = Setting(False)
    order_grouping_by_importance = Setting(False)
    group_var = ContextSetting(None)
    show_annotations = Setting(True)
    compare = Setting(CompareMeans)
    stattest = Setting(0)
    sig_threshold = Setting(0.05)
    stretched = Setting(True)
    show_labels = Setting(True)
    sort_freqs = Setting(False)

    _sorting_criteria_attrs = {
        CompareNone: "", CompareMedians: "median", CompareMeans: "mean"
    }

    _pen_axis_tick = QPen(Qt.white, 5)
    _pen_axis = QPen(Qt.darkGray, 3)
    _pen_median = QPen(QBrush(QColor(0xff, 0xff, 0x00)), 2)
    _pen_paramet = QPen(QBrush(QColor(0x33, 0x00, 0xff)), 2)
    _pen_dotted = QPen(QBrush(QColor(0x33, 0x00, 0xff)), 1)
    _pen_dotted.setStyle(Qt.DotLine)
    _post_line_pen = QPen(Qt.lightGray, 2)
    _post_grp_pen = QPen(Qt.lightGray, 4)
    for pen in (_pen_paramet, _pen_median, _pen_dotted,
                _pen_axis, _pen_axis_tick, _post_line_pen, _post_grp_pen):
        pen.setCosmetic(True)
        pen.setCapStyle(Qt.RoundCap)
        pen.setJoinStyle(Qt.RoundJoin)
    _pen_axis_tick.setCapStyle(Qt.FlatCap)

    _box_brush = QBrush(QColor(0x33, 0x88, 0xff, 0xc0))

    _axis_font = QFont()
    _axis_font.setPixelSize(12)
    _label_font = QFont()
    _label_font.setPixelSize(11)
    _attr_brush = QBrush(QColor(0x33, 0x00, 0xff))

    graph_name = "box_scene"

    def __init__(self):
        super().__init__()
        self.stats = []
        self.dataset = None
        self.posthoc_lines = []

        self.label_txts = self.mean_labels = self.boxes = self.labels = \
            self.label_txts_all = self.attr_labels = self.order = []
        self.scale_x = 1
        self.scene_min_x = self.scene_max_x = self.scene_width = 0
        self.label_width = 0

        self.attrs = VariableListModel()
        view = gui.listView(
            self.controlArea, self, "attribute", box="Variable",
            model=self.attrs, callback=self.attr_changed)
        view.setMinimumSize(QSize(30, 30))
        # Any other policy than Ignored will let the QListBox's scrollbar
        # set the minimal height (see the penultimate paragraph of
        # http://doc.qt.io/qt-4.8/qabstractscrollarea.html#addScrollBarWidget)
        view.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Ignored)
        gui.checkBox(
            view.box, self, "order_by_importance",
            "Order by relevance to subgroups",
            tooltip="Order by 𝜒² or ANOVA over the subgroups",
            callback=self.apply_attr_sorting)
        self.group_vars = VariableListModel(placeholder="None")
        view = gui.listView(
            self.controlArea, self, "group_var", box="Subgroups",
            model=self.group_vars, callback=self.grouping_changed)
        gui.checkBox(
            view.box, self, "order_grouping_by_importance",
            "Order by relevance to variable",
            tooltip="Order by 𝜒² or ANOVA over the variable values",
            callback=self.on_group_sorting_checkbox)
        view.setMinimumSize(QSize(30, 30))
        # See the comment above
        view.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Ignored)

        # TODO: move Compare median/mean to grouping box
        # The vertical size policy is needed to let only the list views expand
        self.display_box = gui.vBox(
            self.controlArea, "Display",
            sizePolicy=(QSizePolicy.Minimum, QSizePolicy.Maximum),
            addSpace=False)

        gui.checkBox(self.display_box, self, "show_annotations", "Annotate",
                     callback=self.display_changed)
        self.compare_rb = gui.radioButtonsInBox(
            self.display_box, self, 'compare',
            btnLabels=["No comparison", "Compare medians", "Compare means"],
            callback=self.layout_changed)

        # The vertical size policy is needed to let only the list views expand
        self.stretching_box = box = gui.vBox(
            self.controlArea, box="Display",
            sizePolicy=(QSizePolicy.Minimum, QSizePolicy.Fixed))
        self.stretching_box.sizeHint = self.display_box.sizeHint
        gui.checkBox(
            box, self, 'stretched', "Stretch bars",
            callback=self.display_changed)
        gui.checkBox(
            box, self, 'show_labels', "Show box labels",
            callback=self.display_changed)
        self.sort_cb = gui.checkBox(
            box, self, 'sort_freqs', "Sort by subgroup frequencies",
            callback=self.display_changed)

        gui.vBox(self.mainArea, addSpace=True)
        self.box_scene = QGraphicsScene()
        self.box_scene.selectionChanged.connect(self.commit)
        self.box_view = QGraphicsView(self.box_scene)
        self.box_view.setRenderHints(QPainter.Antialiasing |
                                     QPainter.TextAntialiasing |
                                     QPainter.SmoothPixmapTransform)
        self.box_view.viewport().installEventFilter(self)

        self.mainArea.layout().addWidget(self.box_view)

        gui.hBox(self.mainArea, addSpace=False)
        self.stat_test = ""
        self.mainArea.setMinimumWidth(300)

        self.stats = self.dist = self.conts = []
        self.is_continuous = False

        self.update_display_box()

    def sizeHint(self):
        return QSize(900, 500)

    def eventFilter(self, obj, event):
        if obj is self.box_view.viewport() and \
                event.type() == QEvent.Resize:
            self.layout_changed()

        return super().eventFilter(obj, event)

    @property
    def show_stretched(self):
        return self.stretched and self.group_var is not self.attribute

    def reset_attrs(self):
        domain = self.dataset.domain
        self.attrs[:] = [
            var for var in chain(
                domain.class_vars, domain.metas, domain.attributes)
            if var.is_primitive()]

    def reset_groups(self):
        domain = self.dataset.domain
        self.group_vars[:] = [None] + [
            var for var in chain(
                domain.class_vars, domain.metas, domain.attributes)
            if var.is_discrete]

    # noinspection PyTypeChecker
    @Inputs.data
    def set_data(self, dataset):
        if dataset is not None and (
                not bool(dataset) or not len(dataset.domain) and not
                any(var.is_primitive() for var in dataset.domain.metas)):
            dataset = None
        self.closeContext()
        self.dataset = dataset
        self.dist = self.stats = self.conts = []
        self.group_var = None
        self.attribute = None
        if dataset:
            self.reset_attrs()
            self.reset_groups()
            self.select_default_variables()
            self.openContext(self.dataset)
            self.grouping_changed()
            self.attr_changed()
        else:
            self.reset_all_data()
        self.commit()

    def select_default_variables(self):
        # visualize first non-class variable, group by class (if present)
        domain = self.dataset.domain
        if len(self.attrs) > len(domain.class_vars):
            self.attribute = self.attrs[len(domain.class_vars)]
        elif self.attrs:
            self.attribute = self.attrs[0]

        if domain.class_var and domain.class_var.is_discrete:
            self.group_var = domain.class_var
        else:
            self.group_var = None  # Reset to trigger selection via callback

    def apply_attr_sorting(self):
        def compute_score(attr):
            # This function and the one in apply_group_sorting are similar, but
            # different in too many details, so they are kept as separate
            # functions.
            # If you discover a bug in this function, check the other one, too.
            if attr is group_var:
                return 3
            if attr.is_continuous:
                # One-way ANOVA
                col = data.get_column_view(attr)[0].astype(float)
                groups = (col[group_col == i] for i in range(n_groups))
                groups = (col[~np.isnan(col)] for col in groups)
                groups = [group for group in groups if len(group)]
                p = f_oneway(*groups)[1] if len(groups) > 1 else 2
            else:
                p = self._chi_square(group_var, attr)[1]
            if math.isnan(p):
                return 2
            return p

        data = self.dataset
        if data is None:
            return
        domain = data.domain
        attribute = self.attribute
        group_var = self.group_var
        if self.order_by_importance and group_var is not None:
            n_groups = len(group_var.values)
            group_col = data.get_column_view(group_var)[0] if \
                domain.has_continuous_attributes(
                    include_class=True, include_metas=True) else None
            self.attrs.sort(key=compute_score)
        else:
            self.reset_attrs()
        self.attribute = attribute  # reset selection
        self._ensure_selection_visible(self.controls.attribute)

    def on_group_sorting_checkbox(self):
        if self.order_grouping_by_importance:
            self.apply_group_sorting()
        else:
            self.reset_groups()
            self.group_var = self.group_var  # reset selection
            self._ensure_selection_visible(self.controls.group_var)

    def apply_group_sorting(self):
        def compute_stat(group):
            # This function and the one in apply_attr_sorting are similar, but
            # different in too many details, so they are kept as separate
            # functions.
            # If you discover a bug in this function, check the other one, too.
            if group is attr:
                return 3
            if group is None:
                return -1
            if attr.is_continuous:
                group_col = data.get_column_view(group)[0].astype(float)
                groups = (attr_col[group_col == i]
                          for i in range(len(group.values)))
                groups = (col[~np.isnan(col)] for col in groups)
                groups = [group for group in groups if len(group)]
                p = f_oneway(*groups)[1] if len(groups) > 1 else 2
            else:
                p = self._chi_square(group, attr)[1]
            if math.isnan(p):
                return 2
            return p

        data = self.dataset
        if data is None or not self.order_grouping_by_importance:
            return
        attr = self.attribute
        group_var = self.group_var
        if attr.is_continuous:
            attr_col = data.get_column_view(attr)[0].astype(float)
        self.group_vars.sort(key=compute_stat)
        self.group_var = group_var  # reset selection
        self._ensure_selection_visible(self.controls.group_var)

    @staticmethod
    def _ensure_selection_visible(view):
        selection = view.selectedIndexes()
        if len(selection) == 1:
            view.scrollTo(selection[0])

    def _chi_square(self, group_var, attr):
        # Chi-square with the given distribution into groups
        if not attr.values or not group_var.values:
            return 0, 2, 0
        observed = np.array(
            contingency.get_contingency(self.dataset, group_var, attr))
        observed = observed[observed.sum(axis=1) != 0, :]
        observed = observed[:, observed.sum(axis=0) != 0]
        if min(observed.shape) < 2:
            return 0, 2, 0
        return chi2_contingency(observed)[:3]

    def reset_all_data(self):
        self.clear_scene()
        self.stat_test = ""
        self.attrs[:] = []
        self.group_vars[:] = [None]
        self.is_continuous = False
        self.update_display_box()

    def grouping_changed(self):
        self.controls.stretched.setDisabled(self.group_var is self.attribute)
        self.apply_attr_sorting()
        self.update_graph()

    def select_box_items(self):
        temp_cond = self.conditions.copy()
        for box in self.box_scene.items():
            if isinstance(box, FilterGraphicsRectItem):
                box.setSelected(box.filter.conditions in
                                [c.conditions for c in temp_cond])

    def attr_changed(self):
        self.controls.stretched.setDisabled(self.group_var is self.attribute)
        self.apply_group_sorting()
        self.update_graph()

    def update_graph(self):
        self.compute_box_data()
        self.update_display_box()
        self.layout_changed()

        if self.is_continuous:
            heights = 90 if self.show_annotations else 60
            self.box_view.centerOn(self.scene_min_x + self.scene_width / 2,
                                   -30 - len(self.stats) * heights / 2 + 45)
        else:
            self.box_view.centerOn(self.scene_width / 2,
                                   -30 - len(self.boxes) * 40 / 2 + 45)

    def compute_box_data(self):
        attr = self.attribute
        if not attr:
            return
        dataset = self.dataset
        self.is_continuous = attr.is_continuous
        if dataset is None or not self.is_continuous and not attr.values or \
                        self.group_var and not self.group_var.values:
            self.stats = self.dist = self.conts = []
            return
        if self.group_var:
            self.dist = []
            self.conts = contingency.get_contingency(
                dataset, attr, self.group_var)
            if self.is_continuous:
                stats, label_texts = [], []
                for i, cont in enumerate(self.conts):
                    if np.sum(cont[1]):
                        stats.append(BoxData(cont, attr, i, self.group_var))
                        label_texts.append(self.group_var.values[i])
                self.stats = stats
                self.label_txts_all = label_texts
            else:
                self.label_txts_all = \
                    [v for v, c in zip(
                        self.group_var.values + ["Missing values"],
                        self.conts.array_with_unknowns)
                     if np.sum(c) > 0]
        else:
            self.dist = distribution.get_distribution(dataset, attr)
            self.conts = []
            if self.is_continuous:
                self.stats = [BoxData(self.dist, attr, None)]
            self.label_txts_all = [""]
        self.label_txts = [txts for stat, txts in zip(self.stats,
                                                      self.label_txts_all)
                           if stat.n > 0]
        self.stats = [stat for stat in self.stats if stat.n > 0]

    def update_display_box(self):
        if self.is_continuous:
            self.stretching_box.hide()
            self.display_box.show()
            self.compare_rb.setEnabled(self.group_var is not None)
        else:
            self.stretching_box.show()
            self.display_box.hide()
            self.sort_cb.setEnabled(self.group_var is not None)

    def clear_scene(self):
        self.closeContext()
        self.box_scene.clearSelection()
        self.box_scene.clear()
        self.box_view.viewport().update()
        self.attr_labels = []
        self.labels = []
        self.boxes = []
        self.mean_labels = []
        self.posthoc_lines = []
        self.openContext(self.dataset)

    def layout_changed(self):
        attr = self.attribute
        if not attr:
            return
        self.clear_scene()
        if self.dataset is None or len(self.conts) == len(self.dist) == 0:
            return

        if self.is_continuous:
            self.mean_labels = [self.mean_label(stat, attr, lab)
                                for stat, lab in zip(self.stats, self.label_txts)]
            self.draw_axis()
            self.boxes = [self.box_group(stat) for stat in self.stats]
            self.labels = [self.label_group(stat, attr, mean_lab)
                           for stat, mean_lab in zip(self.stats, self.mean_labels)]
            self.attr_labels = [QGraphicsSimpleTextItem(lab)
                                for lab in self.label_txts]
            for it in chain(self.labels, self.attr_labels):
                self.box_scene.addItem(it)

        self.display_changed()

    def display_changed(self):
        if self.dataset is None or self.attribute is None:
            return

        if self.is_continuous:
            self._display_changed_cont()
        else:
            self._display_changed_disc()
        self.draw_stat()
        self.select_box_items()

    def _display_changed_cont(self):
        self.order = list(range(len(self.stats)))
        criterion = self._sorting_criteria_attrs[self.compare]
        if criterion:
            vals = [getattr(stat, criterion) for stat in self.stats]
            overmax = max((val for val in vals if val is not None), default=0) \
                      + 1
            vals = [val if val is not None else overmax for val in vals]
            self.order = sorted(self.order, key=vals.__getitem__)

        heights = 90 if self.show_annotations else 60

        for row, box_index in enumerate(self.order):
            y = (-len(self.stats) + row) * heights + 10
            for item in self.boxes[box_index]:
                self.box_scene.addItem(item)
                item.setY(y)
            labels = self.labels[box_index]

            if self.show_annotations:
                labels.show()
                labels.setY(y)
            else:
                labels.hide()

            label = self.attr_labels[box_index]
            label.setY(y - 15 - label.boundingRect().height())
            if self.show_annotations:
                label.hide()
            else:
                stat = self.stats[box_index]

                if self.compare == OWBoxPlot.CompareMedians and \
                        stat.median is not None:
                    pos = stat.median + 5 / self.scale_x
                elif self.compare == OWBoxPlot.CompareMeans or stat.q25 is None:
                    pos = stat.mean + 5 / self.scale_x
                else:
                    pos = stat.q25
                label.setX(pos * self.scale_x)
                label.show()

        r = QRectF(self.scene_min_x, -30 - len(self.stats) * heights,
                   self.scene_width, len(self.stats) * heights + 90)
        self.box_scene.setSceneRect(r)

        self._compute_tests_cont()
        self._show_posthoc()

    def _display_changed_disc(self):
        self.clear_scene()
        self.attr_labels = [QGraphicsSimpleTextItem(lab)
                            for lab in self.label_txts_all]

        if not self.show_stretched:
            if self.group_var:
                self.labels = [
                    QGraphicsTextItem("{}".format(int(sum(cont))))
                    for cont in self.conts.array_with_unknowns
                    if np.sum(cont) > 0]
            else:
                self.labels = [
                    QGraphicsTextItem(str(int(sum(self.dist))))]

        self.order = list(range(len(self.attr_labels)))

        self.draw_axis_disc()
        if self.group_var:
            self.boxes = \
                [self.strudel(cont, i)
                 for i, cont in enumerate(self.conts.array_with_unknowns)
                 if np.sum(cont) > 0]

            if self.sort_freqs:
                # pylint: disable=invalid-unary-operand-type
                self.order = sorted(
                    self.order, key=(-np.sum(
                        self.conts.array_with_unknowns, axis=1)).__getitem__)
        else:
            self.boxes = [self.strudel(self.dist.array_with_unknowns)]

        for row, box_index in enumerate(self.order):
            y = (-len(self.boxes) + row) * 40 + 10
            box = self.boxes[box_index]
            bars, labels = box[::2], box[1::2]

            self.__draw_group_labels(y, box_index)
            if not self.show_stretched:
                self.__draw_row_counts(y, box_index)
            if self.show_labels and self.attribute is not self.group_var:
                self.__draw_bar_labels(y, bars, labels)
            self.__draw_bars(y, bars)

        self.box_scene.setSceneRect(-self.label_width - 5,
                                    -30 - len(self.boxes) * 40,
                                    self.scene_width, len(self.boxes * 40) + 90)
        self._compute_tests_disc()

    def __draw_group_labels(self, y, row):
        """Draw group labels

        Parameters
        ----------
        y: int
            vertical offset of bars
        row: int
            row index
        """
        label = self.attr_labels[row]
        b = label.boundingRect()
        label.setPos(-b.width() - 10, y - b.height() / 2)
        self.box_scene.addItem(label)

    def __draw_row_counts(self, y, row):
        """Draw row counts

        Parameters
        ----------
        y: int
            vertical offset of bars
        row: int
            row index
        """
        assert not self.is_continuous
        label = self.labels[row]
        b = label.boundingRect()
        if self.group_var:
            right = self.scale_x * sum(self.conts.array_with_unknowns[row])
        else:
            right = self.scale_x * sum(self.dist)
        label.setPos(right + 10, y - b.height() / 2)
        self.box_scene.addItem(label)

    def __draw_bar_labels(self, y, bars, labels):
        """Draw bar labels

        Parameters
        ----------
        y: int
            vertical offset of bars
        bars: List[FilterGraphicsRectItem]
            list of bars being drawn
        labels: List[QGraphicsTextItem]
            list of labels for corresponding bars
        """
        label = bar_part = None
        for text_item, bar_part in zip(labels, bars):
            label = self.Label(
                text_item.toPlainText())
            label.setPos(bar_part.boundingRect().x(),
                         y - label.boundingRect().height() - 8)
            label.setMaxWidth(bar_part.boundingRect().width())
            self.box_scene.addItem(label)

    def __draw_bars(self, y, bars):
        """Draw bars

        Parameters
        ----------
        y: int
            vertical offset of bars

        bars: List[FilterGraphicsRectItem]
            list of bars to draw
        """
        for item in bars:
            item.setPos(0, y)
            self.box_scene.addItem(item)

    # noinspection PyPep8Naming
    def _compute_tests_cont(self):
        # The t-test and ANOVA are implemented here since they efficiently use
        # the widget-specific data in self.stats.
        # The non-parametric tests can't do this, so we use statistics.tests

        # pylint: disable=comparison-with-itself
        def stat_ttest():
            d1, d2 = self.stats
            if d1.n < 2 or d2.n < 2:
                return np.nan, np.nan
            pooled_var = d1.var / d1.n + d2.var / d2.n
            # pylint: disable=comparison-with-itself
            if pooled_var == 0 or np.isnan(pooled_var):
                return np.nan, np.nan
            df = pooled_var ** 2 / \
                ((d1.var / d1.n) ** 2 / (d1.n - 1) +
                 (d2.var / d2.n) ** 2 / (d2.n - 1))
            t = abs(d1.mean - d2.mean) / math.sqrt(pooled_var)
            p = 2 * (1 - scipy.special.stdtr(df, t))
            return t, p

        # TODO: Check this function
        # noinspection PyPep8Naming
        def stat_ANOVA():
            if any(stat.n == 0 for stat in self.stats):
                return np.nan, np.nan
            n = sum(stat.n for stat in self.stats)
            grand_avg = sum(stat.n * stat.mean for stat in self.stats) / n
            var_between = sum(stat.n * (stat.mean - grand_avg) ** 2
                              for stat in self.stats)
            df_between = len(self.stats) - 1

            var_within = sum(stat.n * stat.var for stat in self.stats)
            df_within = n - len(self.stats)
            if var_within == 0 or df_within == 0 or df_between == 0:
                return np.nan, np.nan
            F = (var_between / df_between) / (var_within / df_within)
            p = 1 - scipy.special.fdtr(df_between, df_within, F)
            return F, p

        n = len(self.dataset)
        if self.compare == OWBoxPlot.CompareNone or len(self.stats) < 2:
            t = ""
        elif any(s.n <= 1 for s in self.stats):
            t = "At least one group has just one instance, " \
                "cannot compute significance"
        elif len(self.stats) == 2:
            if self.compare == OWBoxPlot.CompareMedians:
                t = ""
                # z, p = tests.wilcoxon_rank_sum(
                #    self.stats[0].dist, self.stats[1].dist)
                # t = "Mann-Whitney's z: %.1f (p=%.3f)" % (z, p)
            else:
                t, p = stat_ttest()
                t = "" if np.isnan(t) else f"Student's t: {t:.3f} (p={p:.3f}, N={n})"
        else:
            if self.compare == OWBoxPlot.CompareMedians:
                t = ""
                # U, p = -1, -1
                # t = "Kruskal Wallis's U: %.1f (p=%.3f)" % (U, p)
            else:
                F, p = stat_ANOVA()
                t = "" if np.isnan(F) else f"ANOVA: {F:.3f} (p={p:.3f}, N={n})"
        self.stat_test = t

    def _compute_tests_disc(self):
        if self.group_var is None or self.attribute is None:
            self.stat_test = ""
        else:
            chi, p, dof = self._chi_square(self.group_var, self.attribute)
            if np.isnan(p):
                self.stat_test = ""
            else:
                self.stat_test = f"χ²: {chi:.2f} (p={p:.3f}, dof={dof})"

    def mean_label(self, stat, attr, val_name):
        label = QGraphicsItemGroup()
        t = QGraphicsSimpleTextItem(attr.str_val(stat.mean), label)
        t.setFont(self._label_font)
        bbox = t.boundingRect()
        w2, h = bbox.width() / 2, bbox.height()
        t.setPos(-w2, -h)
        tpm = QGraphicsSimpleTextItem(
            " \u00b1 " + "%.*f" % (attr.number_of_decimals + 1, stat.dev),
            label)
        tpm.setFont(self._label_font)
        tpm.setPos(w2, -h)
        if val_name:
            vnm = QGraphicsSimpleTextItem(val_name + ": ", label)
            vnm.setFont(self._label_font)
            vnm.setBrush(self._attr_brush)
            vb = vnm.boundingRect()
            label.min_x = -w2 - vb.width()
            vnm.setPos(label.min_x, -h)
        else:
            label.min_x = -w2
        return label

    def draw_axis(self):
        """Draw the horizontal axis and sets self.scale_x"""
        misssing_stats = not self.stats
        stats = self.stats or [BoxData(np.array([[0.], [1.]]), self.attribute)]
        mean_labels = self.mean_labels or [self.mean_label(stats[0], self.attribute, "")]
        bottom = min(stat.a_min for stat in stats)
        top = max(stat.a_max for stat in stats)

        first_val, step = compute_scale(bottom, top)
        while bottom <= first_val:
            first_val -= step
        bottom = first_val
        no_ticks = math.ceil((top - first_val) / step) + 1
        top = max(top, first_val + no_ticks * step)

        gbottom = min(bottom, min(stat.mean - stat.dev for stat in stats))
        gtop = max(top, max(stat.mean + stat.dev for stat in stats))

        bv = self.box_view
        viewrect = bv.viewport().rect().adjusted(15, 15, -15, -30)
        self.scale_x = scale_x = viewrect.width() / (gtop - gbottom)

        # In principle we should repeat this until convergence since the new
        # scaling is too conservative. (No chance am I doing this.)
        mlb = min(stat.mean + mean_lab.min_x / scale_x
                  for stat, mean_lab in zip(stats, mean_labels))
        if mlb < gbottom:
            gbottom = mlb
            self.scale_x = scale_x = viewrect.width() / (gtop - gbottom)

        self.scene_min_x = gbottom * scale_x
        self.scene_max_x = gtop * scale_x
        self.scene_width = self.scene_max_x - self.scene_min_x

        val = first_val
        last_text = self.scene_min_x
        while True:
            l = self.box_scene.addLine(val * scale_x, -1, val * scale_x, 1,
                                       self._pen_axis_tick)
            l.setZValue(100)
            t = QGraphicsSimpleTextItem(
                self.attribute.str_val(val) if not misssing_stats else "?")
            t.setFont(self._axis_font)
            t.setFlag(QGraphicsItem.ItemIgnoresTransformations)
            r = t.boundingRect()
            x_start = val * scale_x - r.width() / 2
            x_finish = x_start + r.width()
            if x_start > last_text + 10 and x_finish < self.scene_max_x:
                t.setPos(x_start, 8)
                self.box_scene.addItem(t)
                last_text = x_finish
            if val >= top:
                break
            val += step
        self.box_scene.addLine(
            bottom * scale_x - 4, 0, top * scale_x + 4, 0, self._pen_axis)

    def draw_stat(self):
        if self.stat_test:
            label = QGraphicsSimpleTextItem(self.stat_test)
            brect = self.box_scene.sceneRect()
            label.setPos(brect.center().x() - label.boundingRect().width()/2,
                         8 + self._axis_font.pixelSize()*2)
            label.setFlag(QGraphicsItem.ItemIgnoresTransformations)
            self.box_scene.addItem(label)

    def draw_axis_disc(self):
        """
        Draw the horizontal axis and sets self.scale_x for discrete attributes
        """
        assert not self.is_continuous
        if self.show_stretched:
            if not self.attr_labels:
                return
            step = steps = 10
        else:
            if self.group_var:
                max_box = max(float(np.sum(dist))
                              for dist in self.conts.array_with_unknowns)
            else:
                max_box = float(np.sum(self.dist.array_with_unknowns))
            if max_box == 0:
                self.scale_x = 1
                return
            _, step = compute_scale(0, max_box)
            step = int(step) if step > 1 else 1
            steps = int(math.ceil(max_box / step))
        max_box = step * steps

        bv = self.box_view
        viewrect = bv.viewport().rect().adjusted(15, 15, -15, -30)
        self.scene_width = viewrect.width()

        lab_width = max(lab.boundingRect().width() for lab in self.attr_labels)
        lab_width = max(lab_width, 40)
        lab_width = min(lab_width, self.scene_width / 3)
        self.label_width = lab_width

        right_offset = 0  # offset for the right label
        if not self.show_stretched and self.labels:
            if self.group_var:
                rows = list(zip(self.conts.array_with_unknowns, self.labels))
            else:
                rows = [(self.dist, self.labels[0])]
            # available space left of the 'group labels'
            available = self.scene_width - lab_width - 10
            scale_x = (available - right_offset) / max_box
            max_right = max(sum(dist) * scale_x + 10 +
                            lbl.boundingRect().width()
                            for dist, lbl in rows)
            right_offset = max(0, max_right - max_box * scale_x)

        self.scale_x = scale_x = \
            (self.scene_width - lab_width - 10 - right_offset) / max_box

        self.box_scene.addLine(0, 0, max_box * scale_x, 0, self._pen_axis)
        for val in range(0, step * steps + 1, step):
            l = self.box_scene.addLine(val * scale_x, -1, val * scale_x, 1,
                                       self._pen_axis_tick)
            l.setZValue(100)
            t = self.box_scene.addSimpleText(str(val), self._axis_font)
            t.setPos(val * scale_x - t.boundingRect().width() / 2, 8)
        if self.show_stretched:
            self.scale_x *= 100

    def label_group(self, stat, attr, mean_lab):
        def centered_text(val, pos):
            t = QGraphicsSimpleTextItem(attr.str_val(val), labels)
            t.setFont(self._label_font)
            bbox = t.boundingRect()
            t.setPos(pos - bbox.width() / 2, 22)
            return t

        def line(x, down=1):
            QGraphicsLineItem(x, 12 * down, x, 20 * down, labels)

        def move_label(label, frm, to):
            label.setX(to)
            to += t_box.width() / 2
            path = QPainterPath()
            path.lineTo(0, 4)
            path.lineTo(to - frm, 4)
            path.lineTo(to - frm, 8)
            p = QGraphicsPathItem(path)
            p.setPos(frm, 12)
            labels.addToGroup(p)

        labels = QGraphicsItemGroup()

        labels.addToGroup(mean_lab)
        m = stat.mean * self.scale_x
        mean_lab.setPos(m, -22)
        line(m, -1)

        if stat.median is not None:
            msc = stat.median * self.scale_x
            med_t = centered_text(stat.median, msc)
            med_box_width2 = med_t.boundingRect().width() / 2
            line(msc)

        if stat.q25 is not None:
            x = stat.q25 * self.scale_x
            t = centered_text(stat.q25, x)
            t_box = t.boundingRect()
            med_left = msc - med_box_width2
            if x + t_box.width() / 2 >= med_left - 5:
                move_label(t, x, med_left - t_box.width() - 5)
            else:
                line(x)

        if stat.q75 is not None:
            x = stat.q75 * self.scale_x
            t = centered_text(stat.q75, x)
            t_box = t.boundingRect()
            med_right = msc + med_box_width2
            if x - t_box.width() / 2 <= med_right + 5:
                move_label(t, x, med_right + 5)
            else:
                line(x)

        return labels

    def box_group(self, stat, height=20):
        def line(x0, y0, x1, y1, *args):
            return QGraphicsLineItem(x0 * scale_x, y0, x1 * scale_x, y1, *args)

        scale_x = self.scale_x
        box = []
        whisker1 = line(stat.a_min, -1.5, stat.a_min, 1.5)
        whisker2 = line(stat.a_max, -1.5, stat.a_max, 1.5)
        vert_line = line(stat.a_min, 0, stat.a_max, 0)
        mean_line = line(stat.mean, -height / 3, stat.mean, height / 3)
        for it in (whisker1, whisker2, mean_line):
            it.setPen(self._pen_paramet)
        vert_line.setPen(self._pen_dotted)
        var_line = line(stat.mean - stat.dev, 0, stat.mean + stat.dev, 0)
        var_line.setPen(self._pen_paramet)
        box.extend([whisker1, whisker2, vert_line, mean_line, var_line])
        if stat.q25 is not None or stat.q75 is not None:
            # if any of them is None it means that its value is equal to median
            box_from = stat.q25 or stat.median
            box_to = stat.q75 or stat.median
            mbox = FilterGraphicsRectItem(
                stat.conditions, box_from * scale_x, -height / 2,
                (box_to - box_from) * scale_x, height)
            mbox.setBrush(self._box_brush)
            mbox.setPen(QPen(Qt.NoPen))
            mbox.setZValue(-200)
            box.append(mbox)

        if stat.median is not None:
            median_line = line(stat.median, -height / 2,
                               stat.median, height / 2)
            median_line.setPen(self._pen_median)
            median_line.setZValue(-150)
            box.append(median_line)

        return box

    def strudel(self, dist, group_val_index=None):
        attr = self.attribute
        ss = np.sum(dist)
        box = []
        if ss < 1e-6:
            cond = [FilterDiscrete(attr, None)]
            if group_val_index is not None:
                cond.append(FilterDiscrete(self.group_var, [group_val_index]))
            box.append(FilterGraphicsRectItem(cond, 0, -10, 1, 10))
        cum = 0
        values = attr.values + ["Missing values"]
        colors = np.vstack((attr.colors, [128, 128, 128]))
        for i, v in enumerate(dist):
            if v < 1e-6:
                continue
            if self.show_stretched:
                v /= ss
            v *= self.scale_x
            cond = [FilterDiscrete(attr, [i])]
            if group_val_index is not None:
                cond.append(FilterDiscrete(self.group_var, [group_val_index]))
            rect = FilterGraphicsRectItem(cond, cum + 1, -6, v - 2, 12)
            rect.setBrush(QBrush(QColor(*colors[i])))
            rect.setPen(QPen(Qt.NoPen))
            if self.show_stretched:
                tooltip = "{}: {:.2f}%".format(
                    values[i],
                    100 * dist[i] / sum(dist))
            else:
                tooltip = "{}: {}".format(values[i], int(dist[i]))
            rect.setToolTip(tooltip)
            text = QGraphicsTextItem(values[i])
            box.append(rect)
            box.append(text)
            cum += v
        return box

    def commit(self):
        self.conditions = [item.filter for item in
                           self.box_scene.selectedItems() if item.filter]
        selected, selection = None, []
        if self.conditions:
            selected = Values(self.conditions, conjunction=False)(self.dataset)
            selection = np.in1d(
                self.dataset.ids, selected.ids, assume_unique=True).nonzero()[0]
        self.Outputs.selected_data.send(selected)
        self.Outputs.annotated_data.send(
            create_annotated_table(self.dataset, selection))

    def _show_posthoc(self):
        def line(y0, y1):
            it = self.box_scene.addLine(x, y0, x, y1, self._post_line_pen)
            it.setZValue(-100)
            self.posthoc_lines.append(it)

        while self.posthoc_lines:
            self.box_scene.removeItem(self.posthoc_lines.pop())

        if self.compare == OWBoxPlot.CompareNone or len(self.stats) < 2:
            return

        if self.compare == OWBoxPlot.CompareMedians:
            crit_line = "median"
        else:
            crit_line = "mean"

        xs = []

        height = 90 if self.show_annotations else 60

        y_up = -len(self.stats) * height + 10
        for pos, box_index in enumerate(self.order):
            stat = self.stats[box_index]
            x = getattr(stat, crit_line)
            if x is None:
                continue
            x *= self.scale_x
            xs.append(x * self.scale_x)
            by = y_up + pos * height
            line(by + 12, 0)

        used_to = []
        last_to = to = 0
        for frm, frm_x in enumerate(xs[:-1]):
            for to in range(frm + 1, len(xs)):
                if xs[to] - frm_x > 1.5:
                    to -= 1
                    break
            if to in (last_to, frm):
                continue
            for rowi, used in enumerate(used_to):
                if used < frm:
                    used_to[rowi] = to
                    break
            else:
                rowi = len(used_to)
                used_to.append(to)
            y = - 6 - rowi * 6
            it = self.box_scene.addLine(frm_x - 2, y, xs[to] + 2, y,
                                        self._post_grp_pen)
            self.posthoc_lines.append(it)
            last_to = to

    def get_widget_name_extension(self):
        return self.attribute.name if self.attribute else None

    def send_report(self):
        self.report_plot()
        text = ""
        if self.attribute:
            text += "Box plot for attribute '{}' ".format(self.attribute.name)
        if self.group_var:
            text += "grouped by '{}'".format(self.group_var.name)
        if text:
            self.report_caption(text)

    class Label(QGraphicsSimpleTextItem):
        """Boxplot Label with settable maxWidth"""
        # Minimum width to display label text
        MIN_LABEL_WIDTH = 25

        # padding bellow the text
        PADDING = 3

        __max_width = None

        def maxWidth(self):
            return self.__max_width

        def setMaxWidth(self, max_width):
            self.__max_width = max_width

        def paint(self, painter, option, widget):
            """Overrides QGraphicsSimpleTextItem.paint

            If label text is too long, it is elided
            to fit into the allowed region
            """
            if self.__max_width is None:
                width = option.rect.width()
            else:
                width = self.__max_width

            if width < self.MIN_LABEL_WIDTH:
                # if space is too narrow, no label
                return

            fm = painter.fontMetrics()
            text = fm.elidedText(self.text(), Qt.ElideRight, width)
            painter.drawText(
                option.rect.x(),
                option.rect.y() + self.boundingRect().height() - self.PADDING,
                text)


if __name__ == "__main__":  # pragma: no cover
    WidgetPreview(OWBoxPlot).run(Orange.data.Table("heart_disease.tab"))

```
--- END FILE ---

Please first localize the bug based on the issue statement, and then generate *SEARCH/REPLACE* edits to fix the issue.

Every *SEARCH/REPLACE* edit must use this format:
1. The file path
2. The start of search block: <<<<<<< SEARCH
3. A contiguous chunk of lines to search for in the existing source code
4. The dividing line: =======
5. The lines to replace into the source code
6. The end of the replace block: >>>>>>> REPLACE

Here is an example:

```python
### mathweb/flask/app.py
<<<<<<< SEARCH
from flask import Flask
=======
import math
from flask import Flask
>>>>>>> REPLACE
```
</solution>

Please note that the *SEARCH/REPLACE* edit REQUIRES PROPER INDENTATION. If you would like to add the line '        print(x)', you must fully write that out, with all those spaces before the code!
Wrap each *SEARCH/REPLACE* edit in a code block as shown in the example above. If you have multiple *SEARCH/REPLACE* edits, use a separate code block for each one.
user