Exemplo n.º 1
0
    def __init__(self, source, dest):
        super().__init__()
        self.setAcceptedMouseButtons(Qt.LeftButton)
        self.setCacheMode(
            self.DeviceCoordinateCache)  # Without this, burn thy CPU
        self.setZValue(1)
        pen = QPen(Edge.Color.DEFAULT[0], 1)
        pen.setJoinStyle(Qt.MiterJoin)
        self.setPen(pen)
        self.arrowHead = QPolygonF()
        self._selected = False
        self._weights = []
        self._labels = []
        self.squares = GroupOfSquares(self)

        self.source = source
        self.dest = dest
        if source is dest:
            source.edges.append(self)
        else:
            source.edges.insert(0, self)
            dest.edges.insert(0, self)

        # Add text labels
        label = self.label = TextItem('', self)
        label.setFont(Edge.Font.DEFAULT)
        label.setZValue(3)
        self.adjust()
Exemplo n.º 2
0
    def __init__(self, i, mu1, mu2, sigma1, sigma2, phi, color):
        OWPlotItem.__init__(self)
        self.outer_box = QGraphicsPolygonItem(self)
        self.inner_box = QGraphicsPolygonItem(self)

        self.i = i
        self.mu1 = mu1
        self.mu2 = mu2
        self.sigma1 = sigma1
        self.sigma2 = sigma2
        self.phi = phi

        self.twosigmapolygon = QPolygonF([
            QPointF(i, mu1 - sigma1),
            QPointF(i, mu1 + sigma1),
            QPointF(i + 1, mu2 + sigma2),
            QPointF(i + 1, mu2 - sigma2),
            QPointF(i, mu1 - sigma1)
        ])

        self.sigmapolygon = QPolygonF([
            QPointF(i, mu1 - .5 * sigma1),
            QPointF(i, mu1 + .5 * sigma1),
            QPointF(i + 1, mu2 + .5 * sigma2),
            QPointF(i + 1, mu2 - .5 * sigma2),
            QPointF(i, mu1 - .5 * sigma1)
        ])

        if isinstance(color, tuple):
            color = QColor(*color)
        color.setAlphaF(.3)
        self.outer_box.setBrush(color)
        self.outer_box.setPen(QColor(0, 0, 0, 0))
        self.inner_box.setBrush(color)
        self.inner_box.setPen(color)
Exemplo n.º 3
0
    def paint(self, painter, _options, _widget):
        if self.hidden:
            return

        if self.expanded:
            tot = np.sum(self.freqs)
            if tot == 0:
                return
            freqs = self.freqs / tot
        else:
            freqs = self.freqs

        if not self.padding:
            padding = self.mapRectFromDevice(QRectF(0, 0, 0.5, 0)).width()
        else:
            padding = min(20, self.width * self.padding)
        sx = self.x + padding
        padded_width = self.width - 2 * padding

        if self.stacked:
            painter.setPen(Qt.NoPen)
            y = 0
            for freq, color in zip(freqs, self.colors):
                painter.setBrush(QBrush(color))
                painter.drawRect(QRectF(sx, y, padded_width, freq))
                y += freq
            self.polygon = QPolygonF(QRectF(sx, 0, padded_width, y))
        else:
            polypoints = [QPointF(sx, 0)]
            pen = QPen(QBrush(Qt.white), 0.5)
            pen.setCosmetic(True)
            painter.setPen(pen)
            wsingle = padded_width / len(self.freqs)
            for i, freq, color in zip(count(), freqs, self.colors):
                painter.setBrush(QBrush(color))
                x = sx + wsingle * i
                painter.drawRect(
                    QRectF(x, 0, wsingle, freq))
                polypoints += [QPointF(x, freq),
                               QPointF(x + wsingle, freq)]
            polypoints += [QPointF(polypoints[-1].x(), 0), QPointF(sx, 0)]
            self.polygon = QPolygonF(polypoints)

        if self.hovered:
            pen = QPen(QBrush(Qt.blue), 2, Qt.DashLine)
            pen.setCosmetic(True)
            painter.setPen(pen)
            painter.setBrush(Qt.NoBrush)
            painter.drawPolygon(self.polygon)
Exemplo n.º 4
0
    def __init__(self, source, dest):
        super().__init__()
        self.setAcceptedMouseButtons(Qt.LeftButton)
        self.setCacheMode(self.DeviceCoordinateCache)  # Without this, burn thy CPU
        self.setZValue(1)
        pen = QPen(Edge.Color.DEFAULT[0], 1)
        pen.setJoinStyle(Qt.MiterJoin)
        self.setPen(pen)
        self.arrowHead = QPolygonF()
        self._selected = False
        self._weights = []
        self._labels = []
        self.squares = GroupOfSquares(self)

        self.source = source
        self.dest = dest
        if source is dest:
            source.edges.append(self)
        else:
            source.edges.insert(0, self)
            dest.edges.insert(0, self)

        # Add text labels
        label = self.label = TextItem('', self)
        label.setFont(Edge.Font.DEFAULT)
        label.setZValue(3)
        self.adjust()
Exemplo n.º 5
0
    def polygon_from_data(xData, yData):
        """
            Creates a polygon from a list of x and y coordinates.

            :returns: A polygon with point corresponding to ``xData`` and ``yData``.
            :rtype: QPolygonF
        """
        if xData and yData:
            n = min(len(xData), len(yData))
            p = QPolygonF(n+1)
            for i in range(n):
                p[i] = QPointF(xData[i], yData[i])
            p[n] = QPointF(xData[0], yData[0])
            return p
        else:
            return QPolygonF()
Exemplo n.º 6
0
def violin_shape(x, p):
    # type: (Sequence[float], Sequence[float]) -> QPainterPath
    points = [QPointF(pi, xi) for xi, pi in zip(x, p)]
    points += [QPointF(-pi, xi) for xi, pi in reversed(list(zip(x, p)))]
    poly = QPolygonF(points)
    path = QPainterPath()
    path.addPolygon(poly)
    return path
Exemplo n.º 7
0
def qgraphicsview_map_rect_from_scene(view: QGraphicsView,
                                      rect: QRectF) -> QPolygonF:
    """Like QGraphicsView.mapFromScene(QRectF) but returning a QPolygonF
    (without rounding).
    """
    tr = view.viewportTransform()
    p1 = tr.map(rect.topLeft())
    p2 = tr.map(rect.topRight())
    p3 = tr.map(rect.bottomRight())
    p4 = tr.map(rect.bottomLeft())
    return QPolygonF([p1, p2, p3, p4])
Exemplo n.º 8
0
 def select_by_rectangle(self, rect: QRectF):
     """
     Find regions that intersect with selected rectangle.
     """
     poly_rect = QPolygonF(rect)
     indices = set()
     for ci in self.choropleth_items:
         if ci.intersects(poly_rect):
             indices.add(np.where(self.master.region_ids == ci.region.id)[0][0])
     if indices:
         self.select_by_indices(np.array(list(indices)))
 def _contains_point(item: pg.FillBetweenItem, point: QPointF) -> bool:
     curve1, curve2 = item.curves
     x_data_lower, y_data_lower = curve1.curve.getData()
     x_data_upper, y_data_upper = curve2.curve.getData()
     pts = [QPointF(x, y) for x, y in zip(x_data_lower, y_data_lower)]
     pts += [QPointF(x, y) for x, y in
             reversed(list(zip(x_data_upper, y_data_upper)))]
     pts += pts[:1]
     path = QPainterPath()
     path.addPolygon(QPolygonF(pts))
     return path.contains(point)
Exemplo n.º 10
0
    def _selection_poly(self, item):
        # type: (Tree) -> QPolygonF
        """
        Return an selection geometry covering item and all its children.
        """
        def left(item):
            return [self._items[ch] for ch in item.node.branches[:1]]

        def right(item):
            return [self._items[ch] for ch in item.node.branches[-1:]]

        itemsleft = list(preorder(item, left))[::-1]
        itemsright = list(preorder(item, right))
        # itemsleft + itemsright walks from the leftmost leaf up to the root
        # and down to the rightmost leaf
        assert itemsleft[0].node.is_leaf
        assert itemsright[-1].node.is_leaf

        if item.node.is_leaf:
            # a single anchor point
            vert = [itemsleft[0].element.anchor]
        else:
            vert = []
            for it in itemsleft[1:]:
                vert.extend([
                    it.element.path[0], it.element.path[1], it.element.anchor
                ])
            for it in itemsright[:-1]:
                vert.extend([
                    it.element.anchor, it.element.path[-2], it.element.path[-1]
                ])
            # close the polygon
            vert.append(vert[0])

            def isclose(a, b, rel_tol=1e-6):
                return abs(a - b) < rel_tol * max(abs(a), abs(b))

            def isclose_p(p1, p2, rel_tol=1e-6):
                return isclose(p1.x, p2.x, rel_tol) and \
                       isclose(p1.y, p2.y, rel_tol)

            # merge consecutive vertices that are (too) close
            acc = [vert[0]]
            for v in vert[1:]:
                if not isclose_p(v, acc[-1]):
                    acc.append(v)
            vert = acc

        return QPolygonF([QPointF(*p) for p in vert])
Exemplo n.º 11
0
def arrow_path_concave(line, width):
    # type: (QLineF, float) -> QPainterPath
    """
    Return a :class:`QPainterPath` of a pretty looking arrow.
    """
    path = QPainterPath()
    p1, p2 = line.p1(), line.p2()

    if p1 == p2:
        return path

    baseline = QLineF(line)
    # Require some minimum length.
    baseline.setLength(max(line.length() - width * 3, width * 3))

    start, end = baseline.p1(), baseline.p2()
    mid = (start + end) / 2.0
    normal = QLineF.fromPolar(1.0, baseline.angle() + 90).p2()

    path.moveTo(start)
    path.lineTo(start + (normal * width / 4.0))

    path.quadTo(mid + (normal * width / 4.0),
                end + (normal * width / 1.5))

    path.lineTo(end - (normal * width / 1.5))
    path.quadTo(mid - (normal * width / 4.0),
                start - (normal * width / 4.0))
    path.closeSubpath()

    arrow_head_len = width * 4
    arrow_head_angle = 50
    line_angle = line.angle() - 180

    angle_1 = line_angle - arrow_head_angle / 2.0
    angle_2 = line_angle + arrow_head_angle / 2.0

    points = [p2,
              p2 + QLineF.fromPolar(arrow_head_len, angle_1).p2(),
              baseline.p2(),
              p2 + QLineF.fromPolar(arrow_head_len, angle_2).p2(),
              p2]

    poly = QPolygonF(points)
    path_head = QPainterPath()
    path_head.addPolygon(poly)
    path = path.united(path_head)
    return path
Exemplo n.º 12
0
def arrow_path_plain(line, width):
    """
    Return an :class:`QPainterPath` of a plain looking arrow.
    """
    path = QPainterPath()
    p1, p2 = line.p1(), line.p2()

    if p1 == p2:
        return path

    baseline = QLineF(line)
    # Require some minimum length.
    baseline.setLength(max(line.length() - width * 3, width * 3))
    path.moveTo(baseline.p1())
    path.lineTo(baseline.p2())

    stroker = QPainterPathStroker()
    stroker.setWidth(width)
    path = stroker.createStroke(path)

    arrow_head_len = width * 4
    arrow_head_angle = 50
    line_angle = line.angle() - 180

    angle_1 = line_angle - arrow_head_angle / 2.0
    angle_2 = line_angle + arrow_head_angle / 2.0

    points = [
        p2,
        p2 + QLineF.fromPolar(arrow_head_len, angle_1).p2(),
        p2 + QLineF.fromPolar(arrow_head_len, angle_2).p2(),
        p2,
    ]

    poly = QPolygonF(points)
    path_head = QPainterPath()
    path_head.addPolygon(poly)
    path = path.united(path_head)
    return path
Exemplo n.º 13
0
    def _create_violin(self, data: np.ndarray) -> Tuple[QPainterPath, float]:
        if self.__kde is None:
            x, p, max_density = np.zeros(1), np.zeros(1), 0
        else:
            x = np.linspace(data.min() - self.__bandwidth * 2,
                            data.max() + self.__bandwidth * 2, 1000)
            p = np.exp(self.__kde.score_samples(x.reshape(-1, 1)))
            max_density = p.max()
            p = scale_density(self.__scale, p, len(data), max_density)

        if self.__orientation == Qt.Vertical:
            pts = [QPointF(pi, xi) for xi, pi in zip(x, p)]
            pts += [QPointF(-pi, xi) for xi, pi in reversed(list(zip(x, p)))]
        else:
            pts = [QPointF(xi, pi) for xi, pi in zip(x, p)]
            pts += [QPointF(xi, -pi) for xi, pi in reversed(list(zip(x, p)))]
        pts += pts[:1]

        polygon = QPolygonF(pts)
        path = QPainterPath()
        path.addPolygon(polygon)
        return path, max_density
Exemplo n.º 14
0
 def poly2qpoly(poly: Polygon) -> QPolygonF:
     return QPolygonF([QPointF(x, y) for x, y in poly.exterior.coords])
Exemplo n.º 15
0
class Edge(_SelectableItem, QGraphicsLineItem):

    ARROW_SIZE = 16

    class Font:
        DEFAULT = QFont('Sans', 12, QFont.Normal)
        SELECTED = QFont('Sans', 12, QFont.DemiBold)

    class Color:
        # = pen color, font brush
        DEFAULT = Qt.gray, QBrush(Qt.black)
        SELECTED = QColor('#dd4455'), QBrush(QColor('#770000'))

    def __init__(self, source, dest):
        super().__init__()
        self.setAcceptedMouseButtons(Qt.LeftButton)
        self.setCacheMode(self.DeviceCoordinateCache)  # Without this, burn thy CPU
        self.setZValue(1)
        pen = QPen(Edge.Color.DEFAULT[0], 1)
        pen.setJoinStyle(Qt.MiterJoin)
        self.setPen(pen)
        self.arrowHead = QPolygonF()
        self._selected = False
        self._weights = []
        self._labels = []
        self.squares = GroupOfSquares(self)

        self.source = source
        self.dest = dest
        if source is dest:
            source.edges.append(self)
        else:
            source.edges.insert(0, self)
            dest.edges.insert(0, self)

        # Add text labels
        label = self.label = TextItem('', self)
        label.setFont(Edge.Font.DEFAULT)
        label.setZValue(3)
        self.adjust()

    def addRelation(self, name, shape, is_constraint):
        if not is_constraint:
            self.squares.addSquare(np.multiply(*shape))
        self._labels.append((name, shape))

        tooltip = '\n'.join('{} ({}×{})'.format(i[0], *i[1])
                            for i in self._labels)
        self.setToolTip(tooltip)
        self.label.setToolTip(tooltip)
        self.squares.setToolTip(tooltip)

        text = ', '.join(i[0] for i in self._labels)
        text = text[:15] + ('…' if len(text) > 15 else '')
        self.label.setText(text)

    def __contains__(self, node):
        return node == self.source or node == self.dest

    def weight(self): return sum(self._weights)
    def addWeight(self, weight): self._weights.append(weight)

    @_SelectableItem.selected.setter
    def selected(self, value):
        self._selected = value
        pencolor, fontbrush = Edge.Color.SELECTED if value else Edge.Color.DEFAULT
        pen = self.pen()
        pen.setColor(QColor(pencolor))
        self.setPen(pen)
        self.label.setBrush(fontbrush)
        self.label.setFont(Edge.Font.SELECTED if value else Edge.Font.DEFAULT)
        self.squares.selected(value)

    def adjust(self):
        line = QLineF(self.source.pos(), self.dest.pos())
        self.setLine(line)
        self.label.setPos(line.pointAt(.5) - self.label.boundingRect().center())
        self.squares.placeBelow(self.label)

    def boundingRect(self):
        extra = (self.pen().width() + Edge.ARROW_SIZE) / 2
        p1, p2 = self.line().p1(), self.line().p2()
        return QRectF(p1, QSizeF(p2.x() - p1.x(), p2.y() - p1.y())).normalized().adjusted(-extra, -extra, extra, extra)
    def shape(self):
        path = super().shape()
        path.addPolygon(self.arrowHead)
        return path

    def _arrowhead_points(self, line):
        ARROW_WIDTH = 2.5
        angle = acos(line.dx() / line.length())
        if line.dy() >= 0: angle = 2*PI - angle
        p1 = line.p2() - QPointF(sin(angle + PI / ARROW_WIDTH) * Edge.ARROW_SIZE,
                                 cos(angle + PI / ARROW_WIDTH) * Edge.ARROW_SIZE)
        p2 = line.p2() - QPointF(sin(angle + PI - PI / ARROW_WIDTH) * Edge.ARROW_SIZE,
                                 cos(angle + PI - PI / ARROW_WIDTH) * Edge.ARROW_SIZE)
        return line.p2(), p1, p2

    def paintArc(self, painter, option, widget):
        assert self.source is self.dest
        node = self.source
        def best_angle():
            """...is the one furthest away from all other angles"""
            angles = [QLineF(node.pos(), other.pos()).angle()
                      for other in chain((edge.source for edge in node.edges
                                          if edge.dest == node and edge.source != node),
                                         (edge.dest for edge in node.edges
                                          if edge.dest != node and edge.source == node))]
            angles.sort()
            if not angles:  # If this self-constraint is the only edge
                return 225
            deltas = np.array(angles[1:] + [360 + angles[0]]) - angles
            return (angles[deltas.argmax()] + deltas.max()/2) % 360

        angle = best_angle()
        inf = QPointF(-1e20, -1e20)  # Doesn't work with real -np.inf!
        line0 = QLineF(node.pos(), inf)
        line1 = QLineF(node.pos(), inf)
        line2 = QLineF(node.pos(), inf)
        line0.setAngle(angle)
        line1.setAngle(angle - 13)
        line2.setAngle(angle + 13)

        p0 = shape_line_intersection(node.shape(), node.pos(), line0)
        p1 = shape_line_intersection(node.shape(), node.pos(), line1)
        p2 = shape_line_intersection(node.shape(), node.pos(), line2)
        path = QPainterPath()
        path.moveTo(p1)
        line = QLineF(node.pos(), p0)
        line.setLength(3*line.length())
        pt = line.p2()
        path.quadTo(pt, p2)

        line = QLineF(node.pos(), pt)
        self.setLine(line)  # This invalidates DeviceCoordinateCache
        painter.drawPath(path)

        # Draw arrow head
        line = QLineF(pt, p2)
        self.arrowHead.clear()
        for point in self._arrowhead_points(line):
            self.arrowHead.append(point)
        painter.setBrush(self.pen().color())
        painter.drawPolygon(self.arrowHead)

        # Update label position
        self.label.setPos(path.pointAtPercent(.5))
        if 90 < angle < 270:  # Right-align the label
            pos = self.label.pos()
            x, y = pos.x(), pos.y()
            self.label.setPos(x - self.label.boundingRect().width(), y)
        self.squares.placeBelow(self.label)
    def paint(self, painter, option, widget=None):
        color, _ = Edge.Color.SELECTED if self.selected else Edge.Color.DEFAULT
        pen = self.pen()
        pen.setColor(color)
        pen.setBrush(QBrush(color))
        pen.setWidth(np.clip(2 * self.weight(), .5, 4))
        painter.setPen(pen)
        self.setPen(pen)

        if self.source == self.dest:
            return self.paintArc(painter, option, widget)
        if self.source.collidesWithItem(self.dest):
            return

        have_two_edges = len([edge for edge in self.source.edges
                              if self.source in edge and self.dest in edge and edge is not self])

        source_pos = self.source.pos()
        dest_pos = self.dest.pos()

        color = self.pen().color()
        painter.setBrush(color)

        point = shape_line_intersection(self.dest.shape(), dest_pos,
                                        QLineF(source_pos, dest_pos))
        line = QLineF(source_pos, point)
        if have_two_edges:
            normal = line.normalVector()
            normal.setLength(15)
            line = QLineF(normal.p2(), point)
            self.label.setPos(line.pointAt(.5))
            self.squares.placeBelow(self.label)

        self.setLine(line)
        painter.drawLine(line)

        # Draw arrow head
        self.arrowHead.clear()
        for point in self._arrowhead_points(line):
            self.arrowHead.append(point)
        painter.drawPolygon(self.arrowHead)
Exemplo n.º 16
0
 def _get_polygon(self) -> QPolygonF:
     return QPolygonF([QPointF(x, y) for x, y in self.get_points()])
Exemplo n.º 17
0
class Edge(_SelectableItem, QGraphicsLineItem):

    ARROW_SIZE = 16

    class Font:
        DEFAULT = QFont('Sans', 12, QFont.Normal)
        SELECTED = QFont('Sans', 12, QFont.DemiBold)

    class Color:
        # = pen color, font brush
        DEFAULT = Qt.gray, QBrush(Qt.black)
        SELECTED = QColor('#dd4455'), QBrush(QColor('#770000'))

    def __init__(self, source, dest):
        super().__init__()
        self.setAcceptedMouseButtons(Qt.LeftButton)
        self.setCacheMode(
            self.DeviceCoordinateCache)  # Without this, burn thy CPU
        self.setZValue(1)
        pen = QPen(Edge.Color.DEFAULT[0], 1)
        pen.setJoinStyle(Qt.MiterJoin)
        self.setPen(pen)
        self.arrowHead = QPolygonF()
        self._selected = False
        self._weights = []
        self._labels = []
        self.squares = GroupOfSquares(self)

        self.source = source
        self.dest = dest
        if source is dest:
            source.edges.append(self)
        else:
            source.edges.insert(0, self)
            dest.edges.insert(0, self)

        # Add text labels
        label = self.label = TextItem('', self)
        label.setFont(Edge.Font.DEFAULT)
        label.setZValue(3)
        self.adjust()

    def addRelation(self, name, shape, is_constraint):
        if not is_constraint:
            self.squares.addSquare(np.multiply(*shape))
        self._labels.append((name, shape))

        tooltip = '\n'.join('{} ({}×{})'.format(i[0], *i[1])
                            for i in self._labels)
        self.setToolTip(tooltip)
        self.label.setToolTip(tooltip)
        self.squares.setToolTip(tooltip)

        text = ', '.join(i[0] for i in self._labels)
        text = text[:15] + ('…' if len(text) > 15 else '')
        self.label.setText(text)

    def __contains__(self, node):
        return node == self.source or node == self.dest

    def weight(self):
        return sum(self._weights)

    def addWeight(self, weight):
        self._weights.append(weight)

    @_SelectableItem.selected.setter
    def selected(self, value):
        self._selected = value
        pencolor, fontbrush = Edge.Color.SELECTED if value else Edge.Color.DEFAULT
        pen = self.pen()
        pen.setColor(QColor(pencolor))
        self.setPen(pen)
        self.label.setBrush(fontbrush)
        self.label.setFont(Edge.Font.SELECTED if value else Edge.Font.DEFAULT)
        self.squares.selected(value)

    def adjust(self):
        line = QLineF(self.source.pos(), self.dest.pos())
        self.setLine(line)
        self.label.setPos(
            line.pointAt(.5) - self.label.boundingRect().center())
        self.squares.placeBelow(self.label)

    def boundingRect(self):
        extra = (self.pen().width() + Edge.ARROW_SIZE) / 2
        p1, p2 = self.line().p1(), self.line().p2()
        return QRectF(p1, QSizeF(p2.x() - p1.x(),
                                 p2.y() - p1.y())).normalized().adjusted(
                                     -extra, -extra, extra, extra)

    def shape(self):
        path = super().shape()
        path.addPolygon(self.arrowHead)
        return path

    def _arrowhead_points(self, line):
        ARROW_WIDTH = 2.5
        angle = acos(line.dx() / line.length())
        if line.dy() >= 0: angle = 2 * PI - angle
        p1 = line.p2() - QPointF(
            sin(angle + PI / ARROW_WIDTH) * Edge.ARROW_SIZE,
            cos(angle + PI / ARROW_WIDTH) * Edge.ARROW_SIZE)
        p2 = line.p2() - QPointF(
            sin(angle + PI - PI / ARROW_WIDTH) * Edge.ARROW_SIZE,
            cos(angle + PI - PI / ARROW_WIDTH) * Edge.ARROW_SIZE)
        return line.p2(), p1, p2

    def paintArc(self, painter, option, widget):
        assert self.source is self.dest
        node = self.source

        def best_angle():
            """...is the one furthest away from all other angles"""
            angles = [
                QLineF(node.pos(), other.pos()).angle() for other in chain((
                    edge.source for edge in node.edges
                    if edge.dest == node and edge.source != node), (
                        edge.dest for edge in node.edges
                        if edge.dest != node and edge.source == node))
            ]
            angles.sort()
            if not angles:  # If this self-constraint is the only edge
                return 225
            deltas = np.array(angles[1:] + [360 + angles[0]]) - angles
            return (angles[deltas.argmax()] + deltas.max() / 2) % 360

        angle = best_angle()
        inf = QPointF(-1e20, -1e20)  # Doesn't work with real -np.inf!
        line0 = QLineF(node.pos(), inf)
        line1 = QLineF(node.pos(), inf)
        line2 = QLineF(node.pos(), inf)
        line0.setAngle(angle)
        line1.setAngle(angle - 13)
        line2.setAngle(angle + 13)

        p0 = shape_line_intersection(node.shape(), node.pos(), line0)
        p1 = shape_line_intersection(node.shape(), node.pos(), line1)
        p2 = shape_line_intersection(node.shape(), node.pos(), line2)
        path = QPainterPath()
        path.moveTo(p1)
        line = QLineF(node.pos(), p0)
        line.setLength(3 * line.length())
        pt = line.p2()
        path.quadTo(pt, p2)

        line = QLineF(node.pos(), pt)
        self.setLine(line)  # This invalidates DeviceCoordinateCache
        painter.drawPath(path)

        # Draw arrow head
        line = QLineF(pt, p2)
        self.arrowHead.clear()
        for point in self._arrowhead_points(line):
            self.arrowHead.append(point)
        painter.setBrush(self.pen().color())
        painter.drawPolygon(self.arrowHead)

        # Update label position
        self.label.setPos(path.pointAtPercent(.5))
        if 90 < angle < 270:  # Right-align the label
            pos = self.label.pos()
            x, y = pos.x(), pos.y()
            self.label.setPos(x - self.label.boundingRect().width(), y)
        self.squares.placeBelow(self.label)

    def paint(self, painter, option, widget=None):
        color, _ = Edge.Color.SELECTED if self.selected else Edge.Color.DEFAULT
        pen = self.pen()
        pen.setColor(color)
        pen.setBrush(QBrush(color))
        pen.setWidth(np.clip(2 * self.weight(), .5, 4))
        painter.setPen(pen)
        self.setPen(pen)

        if self.source == self.dest:
            return self.paintArc(painter, option, widget)
        if self.source.collidesWithItem(self.dest):
            return

        have_two_edges = len([
            edge for edge in self.source.edges
            if self.source in edge and self.dest in edge and edge is not self
        ])

        source_pos = self.source.pos()
        dest_pos = self.dest.pos()

        color = self.pen().color()
        painter.setBrush(color)

        point = shape_line_intersection(self.dest.shape(), dest_pos,
                                        QLineF(source_pos, dest_pos))
        line = QLineF(source_pos, point)
        if have_two_edges:
            normal = line.normalVector()
            normal.setLength(15)
            line = QLineF(normal.p2(), point)
            self.label.setPos(line.pointAt(.5))
            self.squares.placeBelow(self.label)

        self.setLine(line)
        painter.drawLine(line)

        # Draw arrow head
        self.arrowHead.clear()
        for point in self._arrowhead_points(line):
            self.arrowHead.append(point)
        painter.drawPolygon(self.arrowHead)