I am no expert in Qt/PyQt, but I managed to write some QtChart code for plotting some data that extends QtGraphicsView
. I would like to reuse this code to be able to plot multiple charts, I thought I would be able to add them to a layout like widgets. However I am getting into an issue in the SystemPlot
class:
<code>TypeError: addItem(self, item: typing.Optional[QGraphicsItem]): argument 1 has unexpected type 'TemperaturePlotItem'
</code>
<code>TypeError: addItem(self, item: typing.Optional[QGraphicsItem]): argument 1 has unexpected type 'TemperaturePlotItem'
</code>
TypeError: addItem(self, item: typing.Optional[QGraphicsItem]): argument 1 has unexpected type 'TemperaturePlotItem'
Here is the code:
<code>import sys
from typing import List
from PyQt5.QtChart import QChart, QSplineSeries, QValueAxis
from PyQt5.QtCore import QPointF, QRectF, QSizeF, Qt
from PyQt5.QtCore import Qt, QEventLoop, QTimer, QThread, pyqtSignal
from PyQt5.QtGui import (
QPainter,
QResizeEvent,
)
from PyQt5.QtWidgets import (
QApplication,
QGraphicsScene,
QGraphicsView,
QWidget,
QVBoxLayout,
)
from PyQt5.QtOpenGL import QGLWidget, QGLFormat, QGL
from callout import Callout
class TemperaturePlotItem(QGraphicsView):
def __init__(self, datanames, *args, moving=True, xres=64, **kwargs):
super().__init__(*args, **kwargs)
self.moving = moving
self.xres = xres
self.callouts: List[Callout] = []
self.setDragMode(QGraphicsView.NoDrag)
self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
# chart
self.chart = QChart(self.parent())
self.chart.setMinimumSize(640, 480)
self.chart.setTitle("Temperature")
axis_y = QValueAxis()
axis_y.setRange(0, 35)
axis_y.setLabelFormat("%d")
axis_y.setTitleText("Temperature (°C)")
axis_y.applyNiceNumbers()
self.chart.addAxis(axis_y, Qt.AlignLeft)
self.axis_y = axis_y
axis_x = QValueAxis()
axis_x.setRange(0, 10)
axis_x.setLabelFormat("%d")
axis_x.setTitleText("Time")
axis_x.setGridLineVisible(False)
axis_x.setLabelsVisible(False)
axis_x.setTitleVisible(False)
self.chart.addAxis(axis_x, Qt.AlignBottom)
self.axis_x = axis_x
# series
all_series = []
self.series = all_series
for name in datanames:
# series = QLineSeries(name=name)
series = QSplineSeries(name=name)
series.setUseOpenGL(True)
self.chart.addSeries(series)
series.attachAxis(axis_x)
series.attachAxis(axis_y)
series.clicked.connect(self.keep_callout)
series.hovered.connect(self.tooltip_event)
all_series.append(series)
self.chart.setAcceptHoverEvents(True)
self.setRenderHint(QPainter.HighQualityAntialiasing, True)
self.setViewport(QGLWidget(QGLFormat(QGL.SampleBuffers)))
self.setScene(QGraphicsScene())
self.scene().addItem(self.chart)
self.tooltip = Callout(self.chart)
self.tooltip.hide()
self.scene().addItem(self.tooltip)
self.setMouseTracking(True)
# internal
self.last_mouse_pos = None
self.dragging = False
self.x = 0
def append_data(self, data):
for series, d in zip(self.series, data):
series.append(self.x, d)
# move horizontally when not dragging
if self.moving and not self.dragging:
start = self.x - self.xres
if start < 0:
self.axis_x.setRange(0, self.xres)
else:
self.axis_x.setRange(start, self.x)
self.x += 1
self.repaint()
def mouseDoubleClickEvent(self, event):
self.moving = not self.moving
super().mouseDoubleClickEvent(event)
def mouseMoveEvent(self, event):
if event.buttons() == Qt.LeftButton:
self.dragging = True
delta = event.pos() - self.last_mouse_pos
scroll = min(delta.x(), self.chart.axisX().min())
self.chart.scroll(-scroll, 0)
else:
self.dragging = False
self.last_mouse_pos = event.pos()
super().mouseMoveEvent(event)
def resizeEvent(self, event: QResizeEvent):
if scene := self.scene():
scene.setSceneRect(QRectF(QPointF(0, 0), QSizeF(event.size())))
self.chart.resize(QSizeF(event.size()))
for callout in self.callouts:
callout.updateGeometry()
super().resizeEvent(event)
def keep_callout(self):
tooltip = self.tooltip
self.callouts.append(self.tooltip)
self.tooltip = Callout(self.chart)
self.scene().addItem(self.tooltip)
def tooltip_event(self, point: QPointF, state: bool):
if not self.tooltip:
self.tooltip = Callout(self.chart)
if state:
pos = self.chart.mapToValue(point)
self.tooltip.setText(f"{point.y():.2f}°C")
self.tooltip.m_anchor = point
self.tooltip.setZValue(11)
self.tooltip.updateGeometry()
self.tooltip.show()
else:
self.tooltip.hide()
class SystemPlot(QWidget):
def __init__(
self,
):
super(QWidget, self).__init__()
layout = QVBoxLayout(self)
self.setLayout(layout)
self.scene = QGraphicsScene()
self.view = QGraphicsView(self.scene)
layout.addWidget(self.view)
self.controlplot = TemperaturePlotItem(["Control"])
self.polplot = TemperaturePlotItem(
["Temperature 1", "Temperature 2", "Temperature 3", "Temperature 4"]
)
self.scene.addItem(self.adcplot)
self.scene.addItem(self.polplot)
if __name__ == "__main__":
app = QApplication(sys.argv)
window = SystemPolariserPlot()
window.resize(640, 480)
window.show()
sys.exit(app.exec_())
</code>
<code>import sys
from typing import List
from PyQt5.QtChart import QChart, QSplineSeries, QValueAxis
from PyQt5.QtCore import QPointF, QRectF, QSizeF, Qt
from PyQt5.QtCore import Qt, QEventLoop, QTimer, QThread, pyqtSignal
from PyQt5.QtGui import (
QPainter,
QResizeEvent,
)
from PyQt5.QtWidgets import (
QApplication,
QGraphicsScene,
QGraphicsView,
QWidget,
QVBoxLayout,
)
from PyQt5.QtOpenGL import QGLWidget, QGLFormat, QGL
from callout import Callout
class TemperaturePlotItem(QGraphicsView):
def __init__(self, datanames, *args, moving=True, xres=64, **kwargs):
super().__init__(*args, **kwargs)
self.moving = moving
self.xres = xres
self.callouts: List[Callout] = []
self.setDragMode(QGraphicsView.NoDrag)
self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
# chart
self.chart = QChart(self.parent())
self.chart.setMinimumSize(640, 480)
self.chart.setTitle("Temperature")
axis_y = QValueAxis()
axis_y.setRange(0, 35)
axis_y.setLabelFormat("%d")
axis_y.setTitleText("Temperature (°C)")
axis_y.applyNiceNumbers()
self.chart.addAxis(axis_y, Qt.AlignLeft)
self.axis_y = axis_y
axis_x = QValueAxis()
axis_x.setRange(0, 10)
axis_x.setLabelFormat("%d")
axis_x.setTitleText("Time")
axis_x.setGridLineVisible(False)
axis_x.setLabelsVisible(False)
axis_x.setTitleVisible(False)
self.chart.addAxis(axis_x, Qt.AlignBottom)
self.axis_x = axis_x
# series
all_series = []
self.series = all_series
for name in datanames:
# series = QLineSeries(name=name)
series = QSplineSeries(name=name)
series.setUseOpenGL(True)
self.chart.addSeries(series)
series.attachAxis(axis_x)
series.attachAxis(axis_y)
series.clicked.connect(self.keep_callout)
series.hovered.connect(self.tooltip_event)
all_series.append(series)
self.chart.setAcceptHoverEvents(True)
self.setRenderHint(QPainter.HighQualityAntialiasing, True)
self.setViewport(QGLWidget(QGLFormat(QGL.SampleBuffers)))
self.setScene(QGraphicsScene())
self.scene().addItem(self.chart)
self.tooltip = Callout(self.chart)
self.tooltip.hide()
self.scene().addItem(self.tooltip)
self.setMouseTracking(True)
# internal
self.last_mouse_pos = None
self.dragging = False
self.x = 0
def append_data(self, data):
for series, d in zip(self.series, data):
series.append(self.x, d)
# move horizontally when not dragging
if self.moving and not self.dragging:
start = self.x - self.xres
if start < 0:
self.axis_x.setRange(0, self.xres)
else:
self.axis_x.setRange(start, self.x)
self.x += 1
self.repaint()
def mouseDoubleClickEvent(self, event):
self.moving = not self.moving
super().mouseDoubleClickEvent(event)
def mouseMoveEvent(self, event):
if event.buttons() == Qt.LeftButton:
self.dragging = True
delta = event.pos() - self.last_mouse_pos
scroll = min(delta.x(), self.chart.axisX().min())
self.chart.scroll(-scroll, 0)
else:
self.dragging = False
self.last_mouse_pos = event.pos()
super().mouseMoveEvent(event)
def resizeEvent(self, event: QResizeEvent):
if scene := self.scene():
scene.setSceneRect(QRectF(QPointF(0, 0), QSizeF(event.size())))
self.chart.resize(QSizeF(event.size()))
for callout in self.callouts:
callout.updateGeometry()
super().resizeEvent(event)
def keep_callout(self):
tooltip = self.tooltip
self.callouts.append(self.tooltip)
self.tooltip = Callout(self.chart)
self.scene().addItem(self.tooltip)
def tooltip_event(self, point: QPointF, state: bool):
if not self.tooltip:
self.tooltip = Callout(self.chart)
if state:
pos = self.chart.mapToValue(point)
self.tooltip.setText(f"{point.y():.2f}°C")
self.tooltip.m_anchor = point
self.tooltip.setZValue(11)
self.tooltip.updateGeometry()
self.tooltip.show()
else:
self.tooltip.hide()
class SystemPlot(QWidget):
def __init__(
self,
):
super(QWidget, self).__init__()
layout = QVBoxLayout(self)
self.setLayout(layout)
self.scene = QGraphicsScene()
self.view = QGraphicsView(self.scene)
layout.addWidget(self.view)
self.controlplot = TemperaturePlotItem(["Control"])
self.polplot = TemperaturePlotItem(
["Temperature 1", "Temperature 2", "Temperature 3", "Temperature 4"]
)
self.scene.addItem(self.adcplot)
self.scene.addItem(self.polplot)
if __name__ == "__main__":
app = QApplication(sys.argv)
window = SystemPolariserPlot()
window.resize(640, 480)
window.show()
sys.exit(app.exec_())
</code>
import sys
from typing import List
from PyQt5.QtChart import QChart, QSplineSeries, QValueAxis
from PyQt5.QtCore import QPointF, QRectF, QSizeF, Qt
from PyQt5.QtCore import Qt, QEventLoop, QTimer, QThread, pyqtSignal
from PyQt5.QtGui import (
QPainter,
QResizeEvent,
)
from PyQt5.QtWidgets import (
QApplication,
QGraphicsScene,
QGraphicsView,
QWidget,
QVBoxLayout,
)
from PyQt5.QtOpenGL import QGLWidget, QGLFormat, QGL
from callout import Callout
class TemperaturePlotItem(QGraphicsView):
def __init__(self, datanames, *args, moving=True, xres=64, **kwargs):
super().__init__(*args, **kwargs)
self.moving = moving
self.xres = xres
self.callouts: List[Callout] = []
self.setDragMode(QGraphicsView.NoDrag)
self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
# chart
self.chart = QChart(self.parent())
self.chart.setMinimumSize(640, 480)
self.chart.setTitle("Temperature")
axis_y = QValueAxis()
axis_y.setRange(0, 35)
axis_y.setLabelFormat("%d")
axis_y.setTitleText("Temperature (°C)")
axis_y.applyNiceNumbers()
self.chart.addAxis(axis_y, Qt.AlignLeft)
self.axis_y = axis_y
axis_x = QValueAxis()
axis_x.setRange(0, 10)
axis_x.setLabelFormat("%d")
axis_x.setTitleText("Time")
axis_x.setGridLineVisible(False)
axis_x.setLabelsVisible(False)
axis_x.setTitleVisible(False)
self.chart.addAxis(axis_x, Qt.AlignBottom)
self.axis_x = axis_x
# series
all_series = []
self.series = all_series
for name in datanames:
# series = QLineSeries(name=name)
series = QSplineSeries(name=name)
series.setUseOpenGL(True)
self.chart.addSeries(series)
series.attachAxis(axis_x)
series.attachAxis(axis_y)
series.clicked.connect(self.keep_callout)
series.hovered.connect(self.tooltip_event)
all_series.append(series)
self.chart.setAcceptHoverEvents(True)
self.setRenderHint(QPainter.HighQualityAntialiasing, True)
self.setViewport(QGLWidget(QGLFormat(QGL.SampleBuffers)))
self.setScene(QGraphicsScene())
self.scene().addItem(self.chart)
self.tooltip = Callout(self.chart)
self.tooltip.hide()
self.scene().addItem(self.tooltip)
self.setMouseTracking(True)
# internal
self.last_mouse_pos = None
self.dragging = False
self.x = 0
def append_data(self, data):
for series, d in zip(self.series, data):
series.append(self.x, d)
# move horizontally when not dragging
if self.moving and not self.dragging:
start = self.x - self.xres
if start < 0:
self.axis_x.setRange(0, self.xres)
else:
self.axis_x.setRange(start, self.x)
self.x += 1
self.repaint()
def mouseDoubleClickEvent(self, event):
self.moving = not self.moving
super().mouseDoubleClickEvent(event)
def mouseMoveEvent(self, event):
if event.buttons() == Qt.LeftButton:
self.dragging = True
delta = event.pos() - self.last_mouse_pos
scroll = min(delta.x(), self.chart.axisX().min())
self.chart.scroll(-scroll, 0)
else:
self.dragging = False
self.last_mouse_pos = event.pos()
super().mouseMoveEvent(event)
def resizeEvent(self, event: QResizeEvent):
if scene := self.scene():
scene.setSceneRect(QRectF(QPointF(0, 0), QSizeF(event.size())))
self.chart.resize(QSizeF(event.size()))
for callout in self.callouts:
callout.updateGeometry()
super().resizeEvent(event)
def keep_callout(self):
tooltip = self.tooltip
self.callouts.append(self.tooltip)
self.tooltip = Callout(self.chart)
self.scene().addItem(self.tooltip)
def tooltip_event(self, point: QPointF, state: bool):
if not self.tooltip:
self.tooltip = Callout(self.chart)
if state:
pos = self.chart.mapToValue(point)
self.tooltip.setText(f"{point.y():.2f}°C")
self.tooltip.m_anchor = point
self.tooltip.setZValue(11)
self.tooltip.updateGeometry()
self.tooltip.show()
else:
self.tooltip.hide()
class SystemPlot(QWidget):
def __init__(
self,
):
super(QWidget, self).__init__()
layout = QVBoxLayout(self)
self.setLayout(layout)
self.scene = QGraphicsScene()
self.view = QGraphicsView(self.scene)
layout.addWidget(self.view)
self.controlplot = TemperaturePlotItem(["Control"])
self.polplot = TemperaturePlotItem(
["Temperature 1", "Temperature 2", "Temperature 3", "Temperature 4"]
)
self.scene.addItem(self.adcplot)
self.scene.addItem(self.polplot)
if __name__ == "__main__":
app = QApplication(sys.argv)
window = SystemPolariserPlot()
window.resize(640, 480)
window.show()
sys.exit(app.exec_())
The callout code (for your testing, the code is valid):
<code>from PyQt5.QtChart import QChart
from PyQt5.QtCore import QPointF, QRect, QRectF, Qt, pyqtSignal
from PyQt5.QtGui import QColor, QFont, QFontMetrics, QPainter, QPainterPath
from PyQt5.QtWidgets import (
QGraphicsItem, QGraphicsSceneMouseEvent, QStyleOptionGraphicsItem, QWidget,
)
class Callout(QGraphicsItem):
def __init__(self, parent: QChart):
super().__init__()
self.m_chart: QChart = parent
self.m_text: str = ''
self.m_anchor: QPointF = QPointF()
self.m_font: QFont = QFont()
self.m_textRect: QRectF = QRectF()
self.m_rect: QRectF = QRectF()
def setText(self, text: str):
self.m_text = text
metrics = QFontMetrics(self.m_font)
self.m_textRect = QRectF(metrics.boundingRect(QRect(0, 0, 150, 150), Qt.AlignLeft, self.m_text))
self.m_textRect.translate(5, 5)
self.prepareGeometryChange()
self.m_rect = QRectF(self.m_textRect.adjusted(-5, -5, 5, 5))
self.updateGeometry()
def updateGeometry(self):
self.prepareGeometryChange()
self.setPos(self.m_chart.mapToPosition(self.m_anchor) + QPointF(10, -50))
def boundingRect(self) -> QRectF:
from_parent = self.mapFromParent(self.m_chart.mapToPosition(self.m_anchor))
anchor = QPointF(from_parent)
rect = QRectF()
rect.setLeft(min(self.m_rect.left(), anchor.x()))
rect.setRight(max(self.m_rect.right(), anchor.x()))
rect.setTop(min(self.m_rect.top(), anchor.y()))
rect.setBottom(max(self.m_rect.bottom(), anchor.y()))
return rect
def paint(self, painter: QPainter, option: QStyleOptionGraphicsItem, widget: QWidget):
path = QPainterPath()
mr = self.m_rect
path.addRoundedRect(mr, 5, 5)
anchor = QPointF(self.mapFromParent(self.m_chart.mapToPosition(self.m_anchor)))
if not mr.contains(anchor):
point1 = QPointF()
point2 = QPointF()
# establish the position of the anchor point in relation to self.m_rect
above = anchor.y() <= mr.top()
above_center = mr.top() < anchor.y() <= mr.center().y()
below_center = mr.center().y() < anchor.y() <= mr.bottom()
below = anchor.y() > mr.bottom()
on_left = anchor.x() <= mr.left()
left_of_center = mr.left() < anchor.x() <= mr.center().x()
right_of_center = mr.center().x() < anchor.x() <= mr.right()
on_right = anchor.x() > mr.right()
# get the nearest self.m_rect corner.
x = (on_right + right_of_center) * mr.width()
y = (below + below_center) * mr.height()
corner_case = (above and on_left) or (above and on_right) or (below and on_left) or (below and on_right)
vertical = abs(anchor.x() - x) > abs(anchor.y() - y)
horizontal = bool(not vertical)
x1 = x + left_of_center * 10 - right_of_center * 20 + corner_case * horizontal * (
on_left * 10 - on_right * 20)
y1 = y + above_center * 10 - below_center * 20 + corner_case * vertical * (above * 10 - below * 20)
point1.setX(x1)
point1.setY(y1)
x2 = x + left_of_center * 20 - right_of_center * 10 + corner_case * horizontal * (
on_left * 20 - on_right * 10)
y2 = y + above_center * 20 - below_center * 10 + corner_case * vertical * (above * 20 - below * 10)
point2.setX(x2)
point2.setY(y2)
path.moveTo(point1)
path.lineTo(anchor)
path.lineTo(point2)
path = path.simplified()
painter.setPen(QColor(30, 30, 30))
painter.setBrush(QColor(255, 255, 255))
painter.drawPath(path)
painter.drawText(self.m_textRect, self.m_text)
def mousePressEvent(self, event: QGraphicsSceneMouseEvent):
self.scene().removeItem(self)
event.setAccepted(True)
def mouseMoveEvent(self, event: QGraphicsSceneMouseEvent):
if event.buttons() & Qt.LeftButton:
self.setPos(self.mapToParent(event.pos() - event.buttonDownPos(Qt.LeftButton)))
event.setAccepted(True)
else:
event.setAccepted(False)
</code>
<code>from PyQt5.QtChart import QChart
from PyQt5.QtCore import QPointF, QRect, QRectF, Qt, pyqtSignal
from PyQt5.QtGui import QColor, QFont, QFontMetrics, QPainter, QPainterPath
from PyQt5.QtWidgets import (
QGraphicsItem, QGraphicsSceneMouseEvent, QStyleOptionGraphicsItem, QWidget,
)
class Callout(QGraphicsItem):
def __init__(self, parent: QChart):
super().__init__()
self.m_chart: QChart = parent
self.m_text: str = ''
self.m_anchor: QPointF = QPointF()
self.m_font: QFont = QFont()
self.m_textRect: QRectF = QRectF()
self.m_rect: QRectF = QRectF()
def setText(self, text: str):
self.m_text = text
metrics = QFontMetrics(self.m_font)
self.m_textRect = QRectF(metrics.boundingRect(QRect(0, 0, 150, 150), Qt.AlignLeft, self.m_text))
self.m_textRect.translate(5, 5)
self.prepareGeometryChange()
self.m_rect = QRectF(self.m_textRect.adjusted(-5, -5, 5, 5))
self.updateGeometry()
def updateGeometry(self):
self.prepareGeometryChange()
self.setPos(self.m_chart.mapToPosition(self.m_anchor) + QPointF(10, -50))
def boundingRect(self) -> QRectF:
from_parent = self.mapFromParent(self.m_chart.mapToPosition(self.m_anchor))
anchor = QPointF(from_parent)
rect = QRectF()
rect.setLeft(min(self.m_rect.left(), anchor.x()))
rect.setRight(max(self.m_rect.right(), anchor.x()))
rect.setTop(min(self.m_rect.top(), anchor.y()))
rect.setBottom(max(self.m_rect.bottom(), anchor.y()))
return rect
def paint(self, painter: QPainter, option: QStyleOptionGraphicsItem, widget: QWidget):
path = QPainterPath()
mr = self.m_rect
path.addRoundedRect(mr, 5, 5)
anchor = QPointF(self.mapFromParent(self.m_chart.mapToPosition(self.m_anchor)))
if not mr.contains(anchor):
point1 = QPointF()
point2 = QPointF()
# establish the position of the anchor point in relation to self.m_rect
above = anchor.y() <= mr.top()
above_center = mr.top() < anchor.y() <= mr.center().y()
below_center = mr.center().y() < anchor.y() <= mr.bottom()
below = anchor.y() > mr.bottom()
on_left = anchor.x() <= mr.left()
left_of_center = mr.left() < anchor.x() <= mr.center().x()
right_of_center = mr.center().x() < anchor.x() <= mr.right()
on_right = anchor.x() > mr.right()
# get the nearest self.m_rect corner.
x = (on_right + right_of_center) * mr.width()
y = (below + below_center) * mr.height()
corner_case = (above and on_left) or (above and on_right) or (below and on_left) or (below and on_right)
vertical = abs(anchor.x() - x) > abs(anchor.y() - y)
horizontal = bool(not vertical)
x1 = x + left_of_center * 10 - right_of_center * 20 + corner_case * horizontal * (
on_left * 10 - on_right * 20)
y1 = y + above_center * 10 - below_center * 20 + corner_case * vertical * (above * 10 - below * 20)
point1.setX(x1)
point1.setY(y1)
x2 = x + left_of_center * 20 - right_of_center * 10 + corner_case * horizontal * (
on_left * 20 - on_right * 10)
y2 = y + above_center * 20 - below_center * 10 + corner_case * vertical * (above * 20 - below * 10)
point2.setX(x2)
point2.setY(y2)
path.moveTo(point1)
path.lineTo(anchor)
path.lineTo(point2)
path = path.simplified()
painter.setPen(QColor(30, 30, 30))
painter.setBrush(QColor(255, 255, 255))
painter.drawPath(path)
painter.drawText(self.m_textRect, self.m_text)
def mousePressEvent(self, event: QGraphicsSceneMouseEvent):
self.scene().removeItem(self)
event.setAccepted(True)
def mouseMoveEvent(self, event: QGraphicsSceneMouseEvent):
if event.buttons() & Qt.LeftButton:
self.setPos(self.mapToParent(event.pos() - event.buttonDownPos(Qt.LeftButton)))
event.setAccepted(True)
else:
event.setAccepted(False)
</code>
from PyQt5.QtChart import QChart
from PyQt5.QtCore import QPointF, QRect, QRectF, Qt, pyqtSignal
from PyQt5.QtGui import QColor, QFont, QFontMetrics, QPainter, QPainterPath
from PyQt5.QtWidgets import (
QGraphicsItem, QGraphicsSceneMouseEvent, QStyleOptionGraphicsItem, QWidget,
)
class Callout(QGraphicsItem):
def __init__(self, parent: QChart):
super().__init__()
self.m_chart: QChart = parent
self.m_text: str = ''
self.m_anchor: QPointF = QPointF()
self.m_font: QFont = QFont()
self.m_textRect: QRectF = QRectF()
self.m_rect: QRectF = QRectF()
def setText(self, text: str):
self.m_text = text
metrics = QFontMetrics(self.m_font)
self.m_textRect = QRectF(metrics.boundingRect(QRect(0, 0, 150, 150), Qt.AlignLeft, self.m_text))
self.m_textRect.translate(5, 5)
self.prepareGeometryChange()
self.m_rect = QRectF(self.m_textRect.adjusted(-5, -5, 5, 5))
self.updateGeometry()
def updateGeometry(self):
self.prepareGeometryChange()
self.setPos(self.m_chart.mapToPosition(self.m_anchor) + QPointF(10, -50))
def boundingRect(self) -> QRectF:
from_parent = self.mapFromParent(self.m_chart.mapToPosition(self.m_anchor))
anchor = QPointF(from_parent)
rect = QRectF()
rect.setLeft(min(self.m_rect.left(), anchor.x()))
rect.setRight(max(self.m_rect.right(), anchor.x()))
rect.setTop(min(self.m_rect.top(), anchor.y()))
rect.setBottom(max(self.m_rect.bottom(), anchor.y()))
return rect
def paint(self, painter: QPainter, option: QStyleOptionGraphicsItem, widget: QWidget):
path = QPainterPath()
mr = self.m_rect
path.addRoundedRect(mr, 5, 5)
anchor = QPointF(self.mapFromParent(self.m_chart.mapToPosition(self.m_anchor)))
if not mr.contains(anchor):
point1 = QPointF()
point2 = QPointF()
# establish the position of the anchor point in relation to self.m_rect
above = anchor.y() <= mr.top()
above_center = mr.top() < anchor.y() <= mr.center().y()
below_center = mr.center().y() < anchor.y() <= mr.bottom()
below = anchor.y() > mr.bottom()
on_left = anchor.x() <= mr.left()
left_of_center = mr.left() < anchor.x() <= mr.center().x()
right_of_center = mr.center().x() < anchor.x() <= mr.right()
on_right = anchor.x() > mr.right()
# get the nearest self.m_rect corner.
x = (on_right + right_of_center) * mr.width()
y = (below + below_center) * mr.height()
corner_case = (above and on_left) or (above and on_right) or (below and on_left) or (below and on_right)
vertical = abs(anchor.x() - x) > abs(anchor.y() - y)
horizontal = bool(not vertical)
x1 = x + left_of_center * 10 - right_of_center * 20 + corner_case * horizontal * (
on_left * 10 - on_right * 20)
y1 = y + above_center * 10 - below_center * 20 + corner_case * vertical * (above * 10 - below * 20)
point1.setX(x1)
point1.setY(y1)
x2 = x + left_of_center * 20 - right_of_center * 10 + corner_case * horizontal * (
on_left * 20 - on_right * 10)
y2 = y + above_center * 20 - below_center * 10 + corner_case * vertical * (above * 20 - below * 10)
point2.setX(x2)
point2.setY(y2)
path.moveTo(point1)
path.lineTo(anchor)
path.lineTo(point2)
path = path.simplified()
painter.setPen(QColor(30, 30, 30))
painter.setBrush(QColor(255, 255, 255))
painter.drawPath(path)
painter.drawText(self.m_textRect, self.m_text)
def mousePressEvent(self, event: QGraphicsSceneMouseEvent):
self.scene().removeItem(self)
event.setAccepted(True)
def mouseMoveEvent(self, event: QGraphicsSceneMouseEvent):
if event.buttons() & Qt.LeftButton:
self.setPos(self.mapToParent(event.pos() - event.buttonDownPos(Qt.LeftButton)))
event.setAccepted(True)
else:
event.setAccepted(False)