Beispiel #1
0
 def strudel(self, dist):
     attr = self.attribute
     ss = np.sum(dist)
     box = QGraphicsItemGroup()
     if ss < 1e-6:
         QGraphicsRectItem(0, -10, 1, 10, box)
     cum = 0
     for i, v in enumerate(dist):
         if v < 1e-6:
             continue
         if self.stretched:
             v /= ss
         v *= self.scale_x
         rect = QGraphicsRectItem(cum + 1, -6, v - 2, 12, box)
         rect.setBrush(QBrush(QColor(*attr.colors[i])))
         rect.setPen(QPen(Qt.NoPen))
         if self.stretched:
             tooltip = "{}: {:.2f}%".format(attr.values[i],
                                            100 * dist[i] / sum(dist))
         else:
             tooltip = "{}: {}".format(attr.values[i], int(dist[i]))
         rect.setToolTip(tooltip)
         text = QGraphicsTextItem(attr.values[i])
         box.addToGroup(text)
         cum += v
     return box
Beispiel #2
0
 def strudel(self, dist, group_val_index=None):
     attr = self.attribute
     ss = np.sum(dist)
     box = QGraphicsItemGroup()
     if ss < 1e-6:
         cond = [FilterDiscrete(attr, None)]
         if group_val_index is not None:
             cond.append(FilterDiscrete(self.group_var, [group_val_index]))
         FilterGraphicsRectItem(cond, 0, -10, 1, 10, box)
     cum = 0
     for i, v in enumerate(dist):
         if v < 1e-6:
             continue
         if self.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, box)
         rect.setBrush(QBrush(QColor(*attr.colors[i])))
         rect.setPen(QPen(Qt.NoPen))
         if self.stretched:
             tooltip = "{}: {:.2f}%".format(attr.values[i],
                                            100 * dist[i] / sum(dist))
         else:
             tooltip = "{}: {}".format(attr.values[i], int(dist[i]))
         rect.setToolTip(tooltip)
         text = QGraphicsTextItem(attr.values[i])
         box.addToGroup(text)
         cum += v
     return box
Beispiel #3
0
 def strudel(self, dist):
     attr = self.attribute
     ss = np.sum(dist)
     box = QGraphicsItemGroup()
     if ss < 1e-6:
         QGraphicsRectItem(0, -10, 1, 10, box)
     cum = 0
     for i, v in enumerate(dist):
         if v < 1e-6:
             continue
         if self.stretched:
             v /= ss
         v *= self.scale_x
         rect = QGraphicsRectItem(cum + 1, -6, v - 2, 12, box)
         rect.setBrush(QBrush(QColor(*attr.colors[i])))
         rect.setPen(QPen(Qt.NoPen))
         if self.stretched:
             tooltip = "{}: {:.2f}%".format(attr.values[i],
                                            100 * dist[i] / sum(dist))
         else:
             tooltip = "{}: {}".format(attr.values[i], int(dist[i]))
         rect.setToolTip(tooltip)
         text = QGraphicsTextItem(attr.values[i])
         box.addToGroup(text)
         cum += v
     return box
Beispiel #4
0
    def label_group(self, stat, attr, mean_lab):
        def centered_text(val, pos):
            t = QGraphicsSimpleTextItem(
                "%.*f" % (attr.number_of_decimals + 1, 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()
            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
Beispiel #5
0
    def label_group(self, stat, attr, mean_lab):
        def centered_text(val, pos):
            t = QGraphicsSimpleTextItem(
                "%.*f" % (attr.number_of_decimals + 1, 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
Beispiel #6
0
 def __init__(self, parent):
     super().__init__(parent)
     self.__offset = 2
     self.__group = QGraphicsItemGroup(self)
     self.__bar_height = ViolinItem.HEIGHT * 3
     self._add_bar()
     self._add_high_label()
     self._add_low_label()
     self._add_feature_label()
 def __init__(self, parent):
     super().__init__(parent)
     self.__offset = 2
     self.__group = QGraphicsItemGroup(self)
     self._add_bar()
     self._add_high_label()
     self._add_low_label()
     self._add_feature_label()
     font = self.font()
     font.setPointSize(self.FONT_SIZE)
     self.set_font(font)
Beispiel #8
0
 def __setup(self) -> None:
     self.__clear()
     font = self.__effectiveFont if self.__autoScale else self.font()
     assert self.__group is None
     group = QGraphicsItemGroup()
     for text in self.__items:
         t = QGraphicsSimpleTextItem(group)
         t.setFont(font)
         t.setText(text)
         t.setData(0, text)
         self.__textitems.append(t)
     group.setParentItem(self)
     self.__group = group
Beispiel #9
0
    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 = QGraphicsItemGroup()
        whisker1 = line(stat.a_min, -1.5, stat.a_min, 1.5, box)
        whisker2 = line(stat.a_max, -1.5, stat.a_max, 1.5, box)
        vert_line = line(stat.a_min, 0, stat.a_max, 0, box)
        mean_line = line(stat.mean, -height / 3, stat.mean, height / 3, box)
        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, box)
        var_line.setPen(self._pen_paramet)

        if stat.q25 is not None and stat.q75 is not None:
            mbox = FilterGraphicsRectItem(stat.conditions, stat.q25 * scale_x,
                                          -height / 2,
                                          (stat.q75 - stat.q25) * scale_x,
                                          height, box)
            mbox.setBrush(self._box_brush)
            mbox.setPen(QPen(Qt.NoPen))
            mbox.setZValue(-200)

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

        return box
Beispiel #10
0
 def redraw_grid(self):
     if self.grid is not None:
         self.scene.removeItem(self.grid)
     self.grid = QGraphicsItemGroup()
     self.grid.setZValue(-200)
     self.grid_cells = np.full((self.size_y, self.size_x), None)
     for y in range(self.size_y):
         for x in range(self.size_x - (y % 2) * self.hexagonal):
             if self.hexagonal:
                 cell = QGraphicsPathItem(_hexagon_path)
                 cell.setPos(x + (y % 2) / 2, y * sqrt3_2)
             else:
                 cell = QGraphicsRectItem(x - 0.5, y - 0.5, 1, 1)
             self.grid_cells[y, x] = cell
             cell.setPen(self._grid_pen)
             self.grid.addToGroup(cell)
     self.scene.addItem(self.grid)
Beispiel #11
0
 def __init__(self, parent, attr_name: str, x_range: Tuple[float],
              width: int):
     super().__init__(parent)
     self._attr_name = attr_name
     self._width = width
     self._height = self.HEIGHT
     self._range = x_range
     self._x_data: Optional[Union[np.ndarray, float]] = None
     self._group = QGraphicsItemGroup(self)
    def __init__(self, parent):
        super().__init__(parent)
        self.__range = None  # type: Tuple[float]
        self.__value_range = None  # type: Tuple[float]
        self.__model_output = None  # type: float
        self.__base_value = None  # type: float

        self.__group = QGraphicsItemGroup(self)
        low_color, high_color = QColor(*RGB_LOW), QColor(*RGB_HIGH)

        self.__low_item = QGraphicsRectItem()
        self.__low_item.setPen(QPen(low_color))
        self.__low_item.setBrush(QBrush(low_color))

        self.__high_item = QGraphicsRectItem()
        self.__high_item.setPen(QPen(high_color))
        self.__high_item.setBrush(QBrush(high_color))

        self.__low_cover_item = LowCoverItem()
        self.__high_cover_item = HighCoverItem()

        pen = QPen(IndicatorItem.COLOR)
        pen.setStyle(Qt.DashLine)
        pen.setWidth(1)
        self.__model_output_line = QGraphicsLineItem()
        self.__model_output_line.setPen(pen)
        self.__base_value_line = QGraphicsLineItem()
        self.__base_value_line.setPen(pen)

        self.__model_output_ind = IndicatorItem("Model prediction: {}")
        self.__base_value_ind = IndicatorItem("Base value: {}\nThe average "
                                              "prediction for selected class.")

        self.__group.addToGroup(self.__low_item)
        self.__group.addToGroup(self.__high_item)
        self.__group.addToGroup(self.__low_cover_item)
        self.__group.addToGroup(self.__high_cover_item)
        self.__group.addToGroup(self.__model_output_line)
        self.__group.addToGroup(self.__base_value_line)
        self.__group.addToGroup(self.__model_output_ind)
        self.__group.addToGroup(self.__base_value_ind)

        self.__low_parts = []  # type: List[LowPartItem]
        self.__high_parts = []  # type: List[HighPartItem]
Beispiel #13
0
class Legend(QGraphicsWidget):
    WIDTH = 30
    BAR_WIDTH = 7

    def __init__(self, parent):
        super().__init__(parent)
        self.__offset = 2
        self.__group = QGraphicsItemGroup(self)
        self.__bar_height = ViolinItem.HEIGHT * 3
        self._add_bar()
        self._add_high_label()
        self._add_low_label()
        self._add_feature_label()

    def _add_bar(self):
        item = QGraphicsRectItem(0, 0, self.BAR_WIDTH, self.__bar_height)
        gradient = QLinearGradient(0, 0, 0, self.__bar_height)
        gradient.setColorAt(0, QColor(*RGB_HIGH))
        gradient.setColorAt(1, QColor(*RGB_LOW))
        item.setPen(QPen(Qt.NoPen))
        item.setBrush(gradient)
        self.__group.addToGroup(item)

    def _add_high_label(self):
        font = self.font()
        font.setPixelSize(9)
        item = QGraphicsSimpleTextItem("High")
        item.setFont(font)
        item.setX(self.BAR_WIDTH + self.__offset)
        item.setY(0)
        self.__group.addToGroup(item)

    def _add_low_label(self):
        font = self.font()
        font.setPixelSize(9)
        item = QGraphicsSimpleTextItem("Low")
        item.setFont(font)
        item.setX(self.BAR_WIDTH + self.__offset)
        item.setY(self.__bar_height - item.boundingRect().height())
        self.__group.addToGroup(item)

    def _add_feature_label(self):
        font = self.font()
        font.setPixelSize(11)
        item = QGraphicsSimpleTextItem("Feature value")
        item.setRotation(-90)
        item.setFont(font)
        item.setX(self.BAR_WIDTH + self.__offset * 2)
        item.setY(self.__bar_height / 2 + item.boundingRect().width() / 2)
        self.__group.addToGroup(item)

    def sizeHint(self, *_):
        return QSizeF(self.WIDTH, ViolinItem.HEIGHT)
Beispiel #14
0
    def __setup(self):
        self.__clear()
        font = self.font()
        group = QGraphicsItemGroup(self)

        for text in self.__items:
            t = QGraphicsSimpleTextItem(text, group)
            t.setData(0, text)
            t.setFont(font)
            t.setToolTip(text)
            self.__textitems.append(t)
    def set_data(self, x_data: np.ndarray, color_data: np.ndarray):
        def put_point(_x, _y):
            item = QGraphicsEllipseItem()
            item.setX(_x)
            item.setY(_y)
            item.setRect(0, 0, self.POINT_R, self.POINT_R)
            color = QColor(*colors.pop().astype(int))
            item.setPen(QPen(color))
            item.setBrush(QBrush(color))
            self.__group.addToGroup(item)

        self.x_data = x_data
        self.__group = QGraphicsItemGroup(self)

        x_data_unique, dist, x_data = self.prepare_data()
        for x, d in zip(x_data_unique, dist):
            colors = color_data[x_data == np.round(x, 3)]
            colors = list(colors[np.random.choice(len(colors), 11)])
            y = self.HEIGHT / 2 - self.POINT_R / 2
            self.plot_data(put_point, x, y, d)
Beispiel #16
0
 def mean_label(self, stat, attr, val_name):
     label = QGraphicsItemGroup()
     t = QGraphicsSimpleTextItem("%.*f" % (attr.number_of_decimals + 1, 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
Beispiel #17
0
    def create_legend(self):
        if self.legend is not None:
            self.scene.removeItem(self.legend)
            self.legend = None
        if self.attr_color is None:
            return

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

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

        self.legend = wrap_legend_items(items,
                                        hspacing=20,
                                        vspacing=16 + size,
                                        max_width=self.view.width() - 25)
        self.legend.setFlags(self.legend.ItemIgnoresTransformations)
        self.legend.setTransform(
            QTransform.fromTranslate(-self.legend.boundingRect().width() / 2,
                                     0))
        self.scene.addItem(self.legend)
        self.set_legend_pos()
Beispiel #18
0
        def create_legend():
            if self.variable_color is None:
                names = [
                    "<-8", "-8:-4", "-4:-2", "-2:2", "2:4", "4:8", ">8",
                    "Residuals:"
                ]
                colors = self.RED_COLORS[::-1] + self.BLUE_COLORS[1:]
                edges = repeat(Qt.black)
            else:
                names = get_variable_values_sorted(class_var)
                edges = colors = [QColor(*col) for col in class_var.colors]

            items = []
            size = 8
            for name, color, edgecolor in zip(names, colors, edges):
                item = QGraphicsItemGroup()
                item.addToGroup(
                    CanvasRectangle(None, -size / 2, -size / 2, size, size,
                                    edgecolor, color))
                item.addToGroup(
                    CanvasText(None, name, size, 0, Qt.AlignVCenter))
                items.append(item)
            return wrap_legend_items(items,
                                     hspacing=20,
                                     vspacing=16 + size,
                                     max_width=self.canvas_view.width() - xoff)
Beispiel #19
0
 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
Beispiel #20
0
    def _redraw(self):
        self.Warning.missing_colors.clear()
        if self.elements:
            self.scene.removeItem(self.elements)
            self.elements = None
        self.view.set_dimensions(self.size_x, self.size_y, self.hexagonal)

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

        self.elements = QGraphicsItemGroup()
        self.scene.addItem(self.elements)
        if self.attr_color is None:
            self._draw_same_color(sizes)
        elif self.pie_charts:
            self._draw_pie_charts(sizes)
        else:
            self._draw_colored_circles(sizes)
Beispiel #21
0
    def updateSelectionArea(self):
        mask = self.selectionMask()
        brush = self._stylebrush[mask.astype(int)]
        self._item.setBrush(brush)

        if self._selitem is not None:
            self.removeItem(self._selitem)
            self._selitem = None

        if self.__selectiondelegate is not None:
            self._selitem = QGraphicsItemGroup()
            self._selitem.dataBounds = \
                lambda axis, frac=1.0, orthoRange=None: None

            for p1, p2 in self.__selectiondelegate.selection:
                r = QRectF(p1, p2).normalized()
                ritem = QGraphicsRectItem(r, self._selitem)
                ritem.setBrush(QBrush(Qt.NoBrush))
                ritem.setPen(QPen(Qt.red, 0))

            self.addItem(self._selitem)
Beispiel #22
0
def wrap_legend_items(items, max_width, hspacing, vspacing):
    def line_width(line):
        return sum(item.boundingRect().width() for item in line) \
            + hspacing * (len(line) - 1)

    def create_line(line, yi, fixed_width=None):
        x = 0
        for item in line:
            item.setPos(x, yi * vspacing)
            paragraph.addToGroup(item)
            if fixed_width:
                x += fixed_width
            else:
                x += item.boundingRect().width() + hspacing

    max_item = max(item.boundingRect().width() + hspacing for item in items)
    in_line = int(max_width // max_item)
    if line_width(items) < max_width:  # single line
        lines = [items]
        fixed_width = None
    elif in_line < 2:
        lines = [[]]
        for i, item in enumerate(items):  # just a single column - free wrap
            lines[-1].append(item)
            if line_width(lines[-1]) > max_width and len(lines[-1]) > 1:
                lines.append([lines[-1].pop()])
        fixed_width = None
    else:  # arrange into grid
        lines = [
            items[i:i + in_line]
            for i in range(0,
                           len(items) + in_line - 1, in_line)
        ]
        fixed_width = max_item

    paragraph = QGraphicsItemGroup()
    for yi, line in enumerate(lines):
        create_line(line, yi, fixed_width=fixed_width)
    return paragraph
class StripeItem(QGraphicsWidget):
    WIDTH = 70

    def __init__(self, parent):
        super().__init__(parent)
        self.__range = None  # type: Tuple[float]
        self.__value_range = None  # type: Tuple[float]
        self.__model_output = None  # type: float
        self.__base_value = None  # type: float

        self.__group = QGraphicsItemGroup(self)
        low_color, high_color = QColor(*RGB_LOW), QColor(*RGB_HIGH)

        self.__low_item = QGraphicsRectItem()
        self.__low_item.setPen(QPen(low_color))
        self.__low_item.setBrush(QBrush(low_color))

        self.__high_item = QGraphicsRectItem()
        self.__high_item.setPen(QPen(high_color))
        self.__high_item.setBrush(QBrush(high_color))

        self.__low_cover_item = LowCoverItem()
        self.__high_cover_item = HighCoverItem()

        pen = QPen(IndicatorItem.COLOR)
        pen.setStyle(Qt.DashLine)
        pen.setWidth(1)
        self.__model_output_line = QGraphicsLineItem()
        self.__model_output_line.setPen(pen)
        self.__base_value_line = QGraphicsLineItem()
        self.__base_value_line.setPen(pen)

        self.__model_output_ind = IndicatorItem("Model prediction: {}")
        self.__base_value_ind = IndicatorItem("Base value: {}\nThe average "
                                              "prediction for selected class.")

        self.__group.addToGroup(self.__low_item)
        self.__group.addToGroup(self.__high_item)
        self.__group.addToGroup(self.__low_cover_item)
        self.__group.addToGroup(self.__high_cover_item)
        self.__group.addToGroup(self.__model_output_line)
        self.__group.addToGroup(self.__base_value_line)
        self.__group.addToGroup(self.__model_output_ind)
        self.__group.addToGroup(self.__base_value_ind)

        self.__low_parts = []  # type: List[LowPartItem]
        self.__high_parts = []  # type: List[HighPartItem]

    @property
    def total_width(self):
        widths = [
            part.label_item.boundingRect().width()
            for part in self.__low_parts + self.__high_parts
        ] + [0]
        return self.WIDTH + StripePlot.SPACING + max(widths)

    @property
    def model_output_ind(self) -> IndicatorItem:
        return self.__model_output_ind

    @property
    def base_value_ind(self) -> IndicatorItem:
        return self.__base_value_ind

    def set_data(self, data: PlotData, y_range: Tuple[float, float],
                 height: float):
        self.__range = y_range
        self.__value_range = data.value_range
        self.__model_output = data.model_output
        self.__base_value = data.base_value

        r = self.__range[1] - self.__range[0]
        self.__model_output_ind.set_text(self.__model_output, r)
        self.__base_value_ind.set_text(self.__base_value, r)

        for value, label in zip(data.low_values, data.low_labels):
            self.__add_part(value, label, value / sum(data.low_values),
                            self.__low_parts, LowPartItem)
        for value, label in zip(data.high_values, data.high_labels):
            self.__add_part(value, label, value / sum(data.high_values),
                            self.__high_parts, HighPartItem)
        if self.__low_parts:
            self.__low_parts[-1].setVisible(False)
        else:
            self.__low_item.setVisible(False)
            self.__low_cover_item.setVisible(False)
        if self.__high_parts:
            self.__high_parts[-1].setVisible(False)
        else:
            self.__high_item.setVisible(False)
            self.__high_cover_item.setVisible(False)

        self.set_z_values()
        self.set_height(height)

    def __add_part(self, value: float, label: Tuple[str, str], norm_val: float,
                   list_: List[PartItem], cls_: Type[PartItem]):
        item = cls_(value, label, norm_val)
        list_.append(item)
        self.__group.addToGroup(item)
        self.__group.addToGroup(item.value_item)
        self.__group.addToGroup(item.label_item)

    def set_z_values(self):
        if len(self.__high_parts) < len(self.__low_parts):
            self.__high_cover_item.setZValue(-2)
            self.__high_item.setZValue(-3)
            for i, item in enumerate(self.__high_parts):
                item.setZValue(-1)
        else:
            self.__low_cover_item.setZValue(-2)
            self.__low_item.setZValue(-3)
            for i, item in enumerate(self.__low_parts):
                item.setZValue(-1)

    def set_height(self, height: float):
        if self.__range[1] == self.__range[0]:
            return
        height = height / (self.__range[1] - self.__range[0])

        y_top = height * (self.__range[1] - self.__value_range[1])
        h_top = height * (self.__value_range[1] - self.__model_output)
        y_bot = height * (self.__range[1] - self.__model_output)
        h_bot = height * (self.__model_output - self.__value_range[0])

        self.__low_item.setRect(QRectF(0, y_top, self.WIDTH, h_top))
        self.__high_item.setRect(QRectF(0, y_bot, self.WIDTH, h_bot))
        self.__low_cover_item.setY(y_top)
        self.__high_cover_item.setY(y_bot + h_bot)

        self._set_indicators_pos(height)

        def adjust_y_text_low(i):
            # adjust label y according to TIP_LEN
            # adjust for 0.8 * TIP_LEN, because label is not 1 pixel wide
            k = 0.4 if i == len(self.__low_parts) - 1 else 0.8
            return PartItem.TIP_LEN * k

        def adjust_y_text_high(i):
            k = 0.4 if i == 0 else 0.8
            return -PartItem.TIP_LEN * k

        self._set_parts_pos(height, y_top, h_top / height, self.__low_parts,
                            adjust_y_text_low)
        self._set_parts_pos(height, y_bot, h_bot / height, self.__high_parts,
                            adjust_y_text_high)

    def _set_parts_pos(self, height: float, y: float, diff: float,
                       parts: List[PartItem], adjust_y: Callable):
        for i, item in enumerate(parts):
            y_delta = height * item.norm_value * diff

            y_text = y + y_delta / 2 - item.value_height / 2
            visible = y_delta > item.value_height + 8

            y_test_adj = y_text + adjust_y(i)
            y_mid = height * (self.__range[1] - self.__model_output)
            collides = _collides(
                y_mid, y_mid, y_test_adj,
                y_test_adj + item.value_item.boundingRect().height())
            item.value_item.setVisible(visible and not collides)
            item.value_item.setY(y_test_adj)

            item.label_item.setVisible(visible)
            item.label_item.setY(y_text)

            y = y + y_delta
            item.setY(y)

    def _set_indicators_pos(self, height: float):
        mo_y = height * (self.__range[1] - self.__model_output)
        mo_h = self.__model_output_ind.boundingRect().height()
        self.__model_output_ind.setY(mo_y - mo_h / 2)
        self.__model_output_line.setLine(
            0, mo_y, -StripePlot.SPACING - IndicatorItem.MARGIN, mo_y)

        bv_y = height * (self.__range[1] - self.__base_value)
        bv_h = self.__base_value_ind.boundingRect().height()
        self.__base_value_ind.setY(bv_y - bv_h / 2)
        self.__base_value_line.setLine(
            -StripePlot.SPACING, bv_y,
            -StripePlot.SPACING - IndicatorItem.MARGIN, bv_y)
        collides = _collides(mo_y, mo_y + mo_h, bv_y, bv_y + bv_h, d=6)
        self.__base_value_ind.setVisible(not collides)
Beispiel #24
0
class OWSOM(OWWidget):
    name = "Self-Organizing Map"
    description = "Computation of self-organizing map."
    icon = "icons/SOM.svg"
    keywords = ["SOM"]

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

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

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

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

    graph_name = "view"

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

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

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

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

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

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

        self._set_input_summary(None)
        self._set_output_summary(None)

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

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

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

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

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

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

        gui.rubber(self.controlArea)

        self.scene = QGraphicsScene(self)

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

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

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

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

        self.stop_optimization_and_wait()

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

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

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

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

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

    def _set_output_summary(self, output):
        summary = len(output) if output else self.info.NoOutput
        details = format_summary_details(output) if output else ""
        self.info.set_output_summary(summary, details)

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

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

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

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

    def on_attr_size_change(self):
        self._redraw()

    def on_pie_chart_change(self):
        self._redraw()

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

        self.progressBarInit()

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    def send_report(self):
        self.report_plot()
        if self.attr_color:
            self.report_caption(
                f"Self-organizing map colored by '{self.attr_color.name}'")
class ViolinItem(QGraphicsWidget):
    HEIGHT = 50
    POINT_R = 6
    SCALE_FACTOR = 0.5
    selection_changed = Signal(float, float, str)

    class SelectionRect(QGraphicsRectItem):
        COLOR = [255, 255, 0]

        def __init__(self, parent, width: int):
            super().__init__(parent)
            self.parent_width = width

            color = QColor(*self.COLOR)
            color.setAlpha(100)
            self.setBrush(color)

            color = QColor(*self.COLOR)
            self.setPen(color)

    def __init__(self, parent, attr_name: str, x_range: Tuple[float],
                 width: int):
        super().__init__(parent)
        assert x_range[0] == -x_range[1]
        self.__attr_name = attr_name
        self.__width = width
        self.__range = x_range[1] if x_range[1] else 1
        self.__group = None  # type: Optional[QGraphicsItemGroup]
        self.__selection_rect = None  # type: Optional[QGraphicsRectItem]
        self.x_data = None  # type: Optional[np.ndarray]
        parent.selection_cleared.connect(self.__remove_selection_rect)

    @property
    def attr_name(self):
        return self.__attr_name

    def set_data(self, x_data: np.ndarray, color_data: np.ndarray):
        def put_point(_x, _y):
            item = QGraphicsEllipseItem()
            item.setX(_x)
            item.setY(_y)
            item.setRect(0, 0, self.POINT_R, self.POINT_R)
            color = QColor(*colors.pop().astype(int))
            item.setPen(QPen(color))
            item.setBrush(QBrush(color))
            self.__group.addToGroup(item)

        self.x_data = x_data
        self.__group = QGraphicsItemGroup(self)

        x_data_unique, dist, x_data = self.prepare_data()
        for x, d in zip(x_data_unique, dist):
            colors = color_data[x_data == np.round(x, 3)]
            colors = list(colors[np.random.choice(len(colors), 11)])
            y = self.HEIGHT / 2 - self.POINT_R / 2
            self.plot_data(put_point, x, y, d)

    def prepare_data(self):
        x_data = self._values_to_pixels(self.x_data)
        x_data = x_data[~np.isnan(x_data)]
        if len(x_data) == 0:
            return

        x_data = np.round(x_data - self.POINT_R / 2, 3)

        # remove duplicates and get counts (distribution) to set y
        x_data_unique, counts = np.unique(x_data, return_counts=True)
        min_count, max_count = np.min(counts), np.max(counts)
        dist = (counts - min_count) / (max_count - min_count)
        if min_count == max_count:
            dist[:] = 1
        dist = dist ** 0.7

        # plot rarest values first
        indices = np.argsort(counts)
        return x_data_unique[indices], dist[indices], x_data

    @staticmethod
    def plot_data(func: Callable, x: float, y: float, d: float):
        func(x, y)  # y = 0
        if d > 0:
            offset = d * 10
            func(x, y + offset)  # y = (0, 10]
            func(x, y - offset)  # y = (-10, 0]
            for i in range(2, int(offset), 2):
                func(x, y + i)  # y = [2, 8]
                func(x, y - i)  # y = [-8, -2]

    def _values_to_pixels(self, x: np.ndarray) -> np.ndarray:
        # scale data to [-0.5, 0.5]
        x = x / self.__range * self.SCALE_FACTOR
        # round data to 3. decimal for sampling
        x = np.round(x, 3)
        # convert to pixels
        return x * self.__width + self.__width / 2

    def _values_from_pixels(self, p: np.ndarray) -> np.ndarray:
        # convert from pixels
        x = (p - self.__width / 2) / self.__width
        # rescale data from [-0.5, 0.5]
        return np.round(x * self.__range / self.SCALE_FACTOR, 3)

    def rescale(self, width):
        def move_point(_x, *_):
            item = next(points)
            item.setX(_x)

        self.__width = width
        self.updateGeometry()
        points = (item for item in self.__group.childItems())

        x_data_unique, dist, x_data = self.prepare_data()
        for x, d in zip(x_data_unique, dist):
            self.plot_data(move_point, x, 0, d)

        if self.__selection_rect is not None:
            old_width = self.__selection_rect.parent_width
            rect = self.__selection_rect.rect()
            x1 = self.__width * rect.x() / old_width
            x2 = self.__width * (rect.x() + rect.width()) / old_width
            rect = QRectF(x1, rect.y(), x2 - x1, rect.height())
            self.__selection_rect.setRect(rect)
            self.__selection_rect.parent_width = self.__width

    def sizeHint(self, *_):
        return QSizeF(self.__width, self.HEIGHT)

    def __remove_selection_rect(self):
        if self.__selection_rect is not None:
            self.__selection_rect.setParentItem(None)
            if self.scene() is not None:
                self.scene().removeItem(self.__selection_rect)
            self.__selection_rect = None

    def add_selection_rect(self, x1, x2):
        x1, x2 = self._values_to_pixels(np.array([x1, x2]))
        rect = QRectF(x1, 0, x2 - x1, self.HEIGHT)
        self.__selection_rect = ViolinItem.SelectionRect(self, self.__width)
        self.__selection_rect.setRect(rect)

    def mousePressEvent(self, event: QGraphicsSceneMouseEvent):
        event.accept()

    def mouseMoveEvent(self, event: QGraphicsSceneMouseEvent):
        if event.buttons() & Qt.LeftButton:
            if self.__selection_rect is None:
                self.__selection_rect = ViolinItem.SelectionRect(
                    self, self.__width)
            x = event.buttonDownPos(Qt.LeftButton).x()
            rect = QRectF(x, 0, event.pos().x() - x, self.HEIGHT).normalized()
            rect = rect.intersected(self.contentsRect())
            self.__selection_rect.setRect(rect)
            event.accept()

    def mouseReleaseEvent(self, event: QGraphicsSceneMouseEvent):
        x1 = event.buttonDownPos(Qt.LeftButton).x()
        x2 = event.pos().x()
        if x1 > x2:
            x2, x1 = x1, x2
        x1, x2 = self._values_from_pixels(np.array([x1, x2]))
        self.selection_changed.emit(x1, x2, self.__attr_name)
        event.accept()
class Legend(QGraphicsWidget):
    BAR_WIDTH = 7
    BAR_HEIGHT = 150
    FONT_SIZE = 11

    def __init__(self, parent):
        super().__init__(parent)
        self.__offset = 2
        self.__group = QGraphicsItemGroup(self)
        self._add_bar()
        self._add_high_label()
        self._add_low_label()
        self._add_feature_label()
        font = self.font()
        font.setPointSize(self.FONT_SIZE)
        self.set_font(font)

    def _add_bar(self):
        self._bar_item = QGraphicsRectItem()
        self._bar_item.setPen(QPen(Qt.NoPen))
        self._set_bar(self.BAR_HEIGHT)
        self.__group.addToGroup(self._bar_item)

    def _add_high_label(self):
        self.__high_label = item = QGraphicsSimpleTextItem("High")
        item.setX(self.BAR_WIDTH + self.__offset)
        item.setY(0)
        self.__group.addToGroup(item)

    def _add_low_label(self):
        self.__low_label = item = QGraphicsSimpleTextItem("Low")
        item.setX(self.BAR_WIDTH + self.__offset)
        self.__group.addToGroup(item)

    def _add_feature_label(self):
        self.__feature_label = item = QGraphicsSimpleTextItem("Feature value")
        item.setRotation(-90)
        item.setX(self.BAR_WIDTH + self.__offset * 2)
        self.__group.addToGroup(item)

    def _set_font(self):
        for label in (self.__high_label, self.__low_label,
                      self.__feature_label):
            label.setFont(self.font())

        bar_height = self._bar_item.boundingRect().height()

        height = self.__low_label.boundingRect().height()
        self.__low_label.setY(bar_height - height)

        width = self.__feature_label.boundingRect().width()
        self.__feature_label.setY(bar_height / 2 + width / 2)

    def set_font(self, font: QFont):
        self.setFont(font)
        self._set_font()

    def _set_bar(self, height: int):
        self._bar_item.setRect(0, 0, self.BAR_WIDTH, height)
        gradient = QLinearGradient(0, 0, 0, height)
        gradient.setColorAt(0, QColor(*RGB_HIGH))
        gradient.setColorAt(1, QColor(*RGB_LOW))
        self._bar_item.setBrush(gradient)

    def set_bar(self, height: int):
        self._set_bar(height)
        self._set_font()

    def sizeHint(self, *_):
        width = self.__high_label.boundingRect().width()
        width += self._bar_item.boundingRect().width() + self.__offset
        return QSizeF(width, ViolinItem.HEIGHT)