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