Issue with pd.Timestamp as x in custom QGraphicsObject impl

30 views
Skip to first unread message

mks

unread,
May 23, 2020, 4:44:55 PM5/23/20
to pyqtgraph
Hello,

First of all, let me thank you for sharing PyQtGraph. It's amazing. I'm very grateful.

I am still learning it, I am especially interested in writing my own GraphicsItems. I was following the "Custom Graphics" example, which plots Candlesticks. In the example, the input data's x is a float. That works fine for the sake of example, but in real world it would almost always be a Timestamp/Datetime/equivalent. I was working on adopting this example to pandas Timestamp and I hit a roadblock. Upon closer look it seems that pandas Timestamp's nanosecond-precision is an issue, i.e. if I use the float value of the timestamp, the picture's boundingRect is calculated incorrectly. However, if I reduce precision to seconds by dividing the float by 10**9, it works fine. That leads me to believe there is some kind of overflow error somewhere along the way. I am not sure how to approach this. Simply dividing by 10**9 is an ok workaround for now, but it seems hacky to me. Below please find example code with two classes: "CandlestickItemWorking" and "CandlestickNotWorking". You can switch between them by commenting/uncommenting lines 77/78 to reproduce.

Happy to hear your thoughts, perhaps it's a beginner's mistake on my part.

Kind Regards,
mks

"""
Demonstrate creation of a custom graphic (a candlestick plot)

"""
import initExample ## Add path to library (just for examples; you do not need this)

import pyqtgraph as pg
from pyqtgraph import QtCore, QtGui
import pandas as pd


class CandlestickItemWorking(pg.GraphicsObject):
def __init__(self, data):
pg.GraphicsObject.__init__(self)
self.data = data ## data must have fields: time, open, close, min, max
self.generatePicture()

def generatePicture(self):
self.picture = QtGui.QPicture()
p = QtGui.QPainter(self.picture)
p.setPen(pg.mkPen('w'))
w = (self.data[1][0] - self.data[0][0]).value / (3. * 10**9)

for (t, open, close, min, max) in self.data:
p.drawLine(QtCore.QPointF(t.value / 10**9, min), QtCore.QPointF(t.value / 10**9, max))
if open > close:
p.setBrush(pg.mkBrush('r'))
else:
p.setBrush(pg.mkBrush('g'))
p.drawRect(QtCore.QRectF(t.value/10**9 - w, open, w * 2, close - open))
p.end()

def paint(self, p, *args):
p.drawPicture(0, 0, self.picture)

def boundingRect(self):
return QtCore.QRectF(self.picture.boundingRect())


class CandlestickItemNotWorking(pg.GraphicsObject):
def __init__(self, data):
pg.GraphicsObject.__init__(self)
self.data = data
self.generatePicture()

def generatePicture(self):
self.picture = QtGui.QPicture()
p = QtGui.QPainter(self.picture)
p.setPen(pg.mkPen('w'))
w = (self.data[1][0] - self.data[0][0]).value / 3.

for (t, open, close, min, max) in self.data:
p.drawLine(QtCore.QPointF(t.value, min), QtCore.QPointF(t.value, max))
if open > close:
p.setBrush(pg.mkBrush('r'))
else:
p.setBrush(pg.mkBrush('g'))
p.drawRect(QtCore.QRectF(t.value - w, open, w * 2, close - open))
p.end()

def paint(self, p, *args):
p.drawPicture(0, 0, self.picture)

def boundingRect(self):
return QtCore.QRectF(self.picture.boundingRect())


data = [ ## fields are (time, open, close, min, max).
(pd.Timestamp('2020-03-01 00:00:00'), 10, 13, 5, 15),
(pd.Timestamp('2020-03-01 00:01:00'), 13, 17, 9, 20),
(pd.Timestamp('2020-03-01 00:02:00'), 17, 14, 11, 23),
(pd.Timestamp('2020-03-01 00:03:00'), 14, 15, 5, 19),
(pd.Timestamp('2020-03-01 00:04:00'), 15, 9, 8, 22),
(pd.Timestamp('2020-03-01 00:05:00'), 9, 15, 8, 16),
]

item = CandlestickItemWorking(data)
# item = CandlestickItemNotWorking(data)
plt = pg.plot()
plt.addItem(item)
plt.setWindowTitle('pyqtgraph example: customGraphicsItem')

## Start Qt event loop unless running in interactive mode or using pyside.
if __name__ == '__main__':
import sys

if (sys.flags.interactive != 1) or not hasattr(QtCore, 'PYQT_VERSION'):
QtGui.QApplication.instance().exec_()

mks

unread,
Jun 12, 2020, 7:56:18 AM6/12/20
to pyqtgraph
To answer my own question - this is a rather well known issue, as it turns out. Datetime axes use a UNIX Timestamp, which is a number of seconds since the beginning of an epoch. This can be a float, where the fractional part would represent fractions of a second. Pandas uses nanosecond precision by design, so the decimal point is shifted to the left by 9 positions. I ended up using Numpy's datetime64[s] instead.
Reply all
Reply to author
Forward
0 new messages