'draw cursor on a QChartView object
I need to draw a cursor on the QChartView object. something like this:
Cursor on the Chart
Whenever a user clicks on the chart the cursor should be moved there. I have no idea how it is possible. As I searched It seems that this is not a built-in feature of QChartView. So How can I do it? BTW, I'm newbie to the QT.
Solution 1:[1]
I am facing the same problem. Drawing a cursor at the mouse position seems to be trivial by referring to this anwser. Note that I modify the codes from this anwser in x.setter from self.update() to self.scene().update(). This is important for updating the cursor. To be honest, I don't know why. You can leave a comment if you know the difference.
# refer to https://stackoverflow.com/a/67596291/9758790
import sys
import time
from PyQt5.QtCore import Qt, QPointF
from PyQt5.QtGui import QColor, QPainter, QPen
from PyQt5.QtWidgets import QApplication, QMainWindow
from PyQt5.QtChart import (
QChart,
QChartView,
QLineSeries,
)
from PyQt5.Qt import *
class ChartView(QChartView):
_x = None
@property
def x(self):
return self._x
@x.setter
def x(self, x):
self._x = x
# self.update()
self.scene().update()
def drawForeground(self, painter, rect):
if self.x is None:
return
painter.save()
pen = QPen(QColor("indigo"))
pen.setWidth(3)
painter.setPen(pen)
# p = self.chart().mapToPosition(QPointF(self.x, 0))
p = QPointF(self.x, 0)
r = self.chart().plotArea()
p1 = QPointF(p.x(), r.top())
p2 = QPointF(p.x(), r.bottom())
painter.drawLine(p1, p2)
painter.restore()
def mousePressEvent(self, env):
# refer to https://stackoverflow.com/a/44078533/9758790
scene_position = self.mapToScene(env.pos())
chart_position = self.chart().mapFromScene(scene_position)
value_at_position = self.chart().mapToValue(chart_position)
if self.chart().axisX().min() < value_at_position.x() < self.chart().axisX().max():
self.x = scene_position.x()
def main():
app = QApplication(sys.argv)
series1 = QLineSeries() << QPointF(0, 3) << QPointF(1, 1) << QPointF(3, 9) << QPointF(4, 1)
series2 = QLineSeries() << QPointF(0, 9) << QPointF(1, 8) << QPointF(3, 6) << QPointF(4, 6)
series3 = QLineSeries() << QPointF(0, 4) << QPointF(1, 2) << QPointF(3, 2) << QPointF(4, 3)
chart = QChart()
chart.addSeries(series1)
chart.addSeries(series2)
chart.addSeries(series3)
chart.createDefaultAxes()
chart.legend().setVisible(True)
chart.legend().setAlignment(Qt.AlignBottom)
chartView = ChartView(chart)
chartView.setRenderHint(QPainter.Antialiasing)
chartView.x = None
window = QMainWindow()
window.setCentralWidget(chartView)
window.resize(420, 300)
window.show()
app.exec()
if __name__ == "__main__":
main()
However, I find it difficult to draw the intersection point of the cursor and the line chart. As mentioned in this forum and this forum, if the cursor points at the interpolated part between two data point, it is difficult to decide the y coordinates of the intersection points, because Qt doesn't provide a build-in function to get the interpolated function value at value_at_position.x(). This question is also related to find the interpolated value. So I tried to do interpolation by my self.
The following version of codes will also draw the intersection point.
# refer to https://stackoverflow.com/a/67596291/9758790
import sys
import time
from PyQt5.QtCore import Qt, QPointF
from PyQt5.QtGui import QColor, QPainter, QPen
from PyQt5.QtWidgets import QApplication, QMainWindow
from PyQt5.QtChart import (
QChart,
QChartView,
QLineSeries,
)
from PyQt5.Qt import *
import math
class ChartView(QChartView):
_cursor = None
_y = []
@property
def cursor(self):
return self._cursor
@cursor.setter
def cursor(self, point):
# refer to https://stackoverflow.com/a/44078533/9758790
scene_position = self.mapToScene(point)
chart_position = self.chart().mapFromScene(scene_position)
value_at_position = self.chart().mapToValue(chart_position)
if self.chart().axisX().min() < value_at_position.x() < self.chart().axisX().max():
self._cursor = scene_position
# self.update()
self.scene().update()
def drawForeground(self, painter, rect):
if self.cursor is None:
return
painter.save()
pen = QPen(QColor("indigo"))
pen.setWidth(1)
painter.setPen(pen)
p = self.cursor
r = self.chart().plotArea()
p1 = QPointF(p.x(), r.top())
p2 = QPointF(p.x(), r.bottom())
painter.drawLine(p1, p2)
chart_position = self.chart().mapFromScene(self.cursor)
value_at_position = self.chart().mapToValue(chart_position)
for series_i in self.chart().series():
pen2 = QPen(series_i.color())
pen2.setWidth(10)
painter.setPen(pen2)
# find the nearest points
min_distance_left = math.inf
min_distance_right = math.inf
nearest_point_left = None
nearest_point_right = None
exact_point = None
for p_i in series_i.pointsVector():
if p_i.x() > value_at_position.x():
if p_i.x() - value_at_position.x() < min_distance_right:
min_distance_right = p_i.x() - value_at_position.x()
nearest_point_right = p_i
elif p_i.x() < value_at_position.x():
if value_at_position.x() - p_i.x() < min_distance_right:
min_distance_left = value_at_position.x() - p_i.x()
nearest_point_left = p_i
else:
exact_point = p_i
nearest_point_left = None
nearest_point_right = None
break
if nearest_point_right is not None and nearest_point_left is not None:
# do linear interpolated by my self
k = ((nearest_point_right.y() - nearest_point_left.y()) / (nearest_point_right.x() - nearest_point_left.x()))
point_interpolated_y = nearest_point_left.y() + k * (value_at_position.x() - nearest_point_left.x())
point_interpolated_x = value_at_position.x()
point_interpolated = QPointF(point_interpolated_x, point_interpolated_y)
painter.drawPoint(self.chart().mapToScene(self.chart().mapToPosition(point_interpolated)))
if exact_point is not None:
painter.drawPoint(self.chart().mapToScene(self.chart().mapToPosition(exact_point)))
painter.restore()
def mousePressEvent(self, env):
self.cursor = env.pos()
def main():
app = QApplication(sys.argv)
series1 = QLineSeries() << QPointF(0, 3) << QPointF(1, 1) << QPointF(3, 9) << QPointF(4, 1)
series2 = QLineSeries() << QPointF(0, 9) << QPointF(1, 8) << QPointF(3, 6) << QPointF(4, 6)
series3 = QLineSeries() << QPointF(0, 4) << QPointF(1, 2) << QPointF(3, 2) << QPointF(4, 3)
chart = QChart()
chart.addSeries(series1)
chart.addSeries(series2)
chart.addSeries(series3)
chart.createDefaultAxes()
chart.legend().setVisible(True)
chart.legend().setAlignment(Qt.AlignBottom)
chartView = ChartView(chart)
chartView.setRenderHint(QPainter.Antialiasing)
chartView.x = None
window = QMainWindow()
window.setCentralWidget(chartView)
window.resize(420, 300)
window.show()
app.exec()
if __name__ == "__main__":
main()
This link Track Line with Data Labels provide a solution to find a nearest data point to the mouse position and draw a line there, which avoids dealing with interpolation. My codes can also be modified to draw in this way.
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
Solution | Source |
---|---|
Solution 1 | hellohawaii |