-
-
Save cpascual/cdcead6c166e63de2981bc23f5840a98 to your computer and use it in GitHub Desktop.
#!/usr/bin/env python | |
############################################################################# | |
# | |
# This file was adapted from Taurus TEP17, but all taurus dependencies were | |
# removed so that it works with just pyqtgraph | |
# | |
# Just run it and play with the zoom to see how the labels and tick positions | |
# automatically adapt to the shown range | |
# | |
############################################################################# | |
# http://taurus-scada.org | |
# | |
# Copyright 2011 CELLS / ALBA Synchrotron, Bellaterra, Spain | |
# | |
# Taurus is free software: you can redistribute it and/or modify | |
# it under the terms of the GNU Lesser General Public License as published by | |
# the Free Software Foundation, either version 3 of the License, or | |
# (at your option) any later version. | |
# | |
# Taurus is distributed in the hope that it will be useful, | |
# but WITHOUT ANY WARRANTY; without even the implied warranty of | |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
# GNU Lesser General Public License for more details. | |
# | |
# You should have received a copy of the GNU Lesser General Public License | |
# along with Taurus. If not, see <http://www.gnu.org/licenses/>. | |
# | |
############################################################################# | |
""" | |
This module provides date-time aware axis | |
""" | |
__all__ = ["DateAxisItem"] | |
import numpy | |
from pyqtgraph import AxisItem | |
from datetime import datetime, timedelta | |
from time import mktime | |
class DateAxisItem(AxisItem): | |
""" | |
A tool that provides a date-time aware axis. It is implemented as an | |
AxisItem that interpretes positions as unix timestamps (i.e. seconds | |
since 1970). | |
The labels and the tick positions are dynamically adjusted depending | |
on the range. | |
It provides a :meth:`attachToPlotItem` method to add it to a given | |
PlotItem | |
""" | |
# Max width in pixels reserved for each label in axis | |
_pxLabelWidth = 80 | |
def __init__(self, *args, **kwargs): | |
AxisItem.__init__(self, *args, **kwargs) | |
self._oldAxis = None | |
def tickValues(self, minVal, maxVal, size): | |
""" | |
Reimplemented from PlotItem to adjust to the range and to force | |
the ticks at "round" positions in the context of time units instead of | |
rounding in a decimal base | |
""" | |
maxMajSteps = int(size/self._pxLabelWidth) | |
dt1 = datetime.fromtimestamp(minVal) | |
dt2 = datetime.fromtimestamp(maxVal) | |
dx = maxVal - minVal | |
majticks = [] | |
if dx > 63072001: # 3600s*24*(365+366) = 2 years (count leap year) | |
d = timedelta(days=366) | |
for y in range(dt1.year + 1, dt2.year): | |
dt = datetime(year=y, month=1, day=1) | |
majticks.append(mktime(dt.timetuple())) | |
elif dx > 5270400: # 3600s*24*61 = 61 days | |
d = timedelta(days=31) | |
dt = dt1.replace(day=1, hour=0, minute=0, | |
second=0, microsecond=0) + d | |
while dt < dt2: | |
# make sure that we are on day 1 (even if always sum 31 days) | |
dt = dt.replace(day=1) | |
majticks.append(mktime(dt.timetuple())) | |
dt += d | |
elif dx > 172800: # 3600s24*2 = 2 days | |
d = timedelta(days=1) | |
dt = dt1.replace(hour=0, minute=0, second=0, microsecond=0) + d | |
while dt < dt2: | |
majticks.append(mktime(dt.timetuple())) | |
dt += d | |
elif dx > 7200: # 3600s*2 = 2hours | |
d = timedelta(hours=1) | |
dt = dt1.replace(minute=0, second=0, microsecond=0) + d | |
while dt < dt2: | |
majticks.append(mktime(dt.timetuple())) | |
dt += d | |
elif dx > 1200: # 60s*20 = 20 minutes | |
d = timedelta(minutes=10) | |
dt = dt1.replace(minute=(dt1.minute // 10) * 10, | |
second=0, microsecond=0) + d | |
while dt < dt2: | |
majticks.append(mktime(dt.timetuple())) | |
dt += d | |
elif dx > 120: # 60s*2 = 2 minutes | |
d = timedelta(minutes=1) | |
dt = dt1.replace(second=0, microsecond=0) + d | |
while dt < dt2: | |
majticks.append(mktime(dt.timetuple())) | |
dt += d | |
elif dx > 20: # 20s | |
d = timedelta(seconds=10) | |
dt = dt1.replace(second=(dt1.second // 10) * 10, microsecond=0) + d | |
while dt < dt2: | |
majticks.append(mktime(dt.timetuple())) | |
dt += d | |
elif dx > 2: # 2s | |
d = timedelta(seconds=1) | |
majticks = range(int(minVal), int(maxVal)) | |
else: # <2s , use standard implementation from parent | |
return AxisItem.tickValues(self, minVal, maxVal, size) | |
L = len(majticks) | |
if L > maxMajSteps: | |
majticks = majticks[::int(numpy.ceil(float(L) / maxMajSteps))] | |
return [(d.total_seconds(), majticks)] | |
def tickStrings(self, values, scale, spacing): | |
"""Reimplemented from PlotItem to adjust to the range""" | |
ret = [] | |
if not values: | |
return [] | |
if spacing >= 31622400: # 366 days | |
fmt = "%Y" | |
elif spacing >= 2678400: # 31 days | |
fmt = "%Y %b" | |
elif spacing >= 86400: # = 1 day | |
fmt = "%b/%d" | |
elif spacing >= 3600: # 1 h | |
fmt = "%b/%d-%Hh" | |
elif spacing >= 60: # 1 m | |
fmt = "%H:%M" | |
elif spacing >= 1: # 1s | |
fmt = "%H:%M:%S" | |
else: | |
# less than 2s (show microseconds) | |
# fmt = '%S.%f"' | |
fmt = '[+%fms]' # explicitly relative to last second | |
for x in values: | |
try: | |
t = datetime.fromtimestamp(x) | |
ret.append(t.strftime(fmt)) | |
except ValueError: # Windows can't handle dates before 1970 | |
ret.append('') | |
return ret | |
def attachToPlotItem(self, plotItem): | |
"""Add this axis to the given PlotItem | |
:param plotItem: (PlotItem) | |
""" | |
self.setParentItem(plotItem) | |
viewBox = plotItem.getViewBox() | |
self.linkToView(viewBox) | |
self._oldAxis = plotItem.axes[self.orientation]['item'] | |
self._oldAxis.hide() | |
plotItem.axes[self.orientation]['item'] = self | |
pos = plotItem.axes[self.orientation]['pos'] | |
plotItem.layout.addItem(self, *pos) | |
self.setZValue(-1000) | |
def detachFromPlotItem(self): | |
"""Remove this axis from its attached PlotItem | |
(not yet implemented) | |
""" | |
raise NotImplementedError() # TODO | |
if __name__ == '__main__': | |
import time | |
import sys | |
import pyqtgraph as pg | |
from PyQt4 import QtGui | |
app = QtGui.QApplication([]) | |
w = pg.PlotWidget() | |
# Add the Date-time axis | |
axis = DateAxisItem(orientation='bottom') | |
axis.attachToPlotItem(w.getPlotItem()) | |
# plot some random data with timestamps in the last hour | |
now = time.time() | |
timestamps = numpy.linspace(now - 3600, now, 100) | |
w.plot(x=timestamps, y=numpy.random.rand(100), symbol='o') | |
w.show() | |
sys.exit(app.exec_()) |
Although the X axis is gone if you zoom out such as min value of X axis reaches UNIX epoch start
Hi, I could not reproduce it (at least using PyQt5)
Although the X axis is gone if you zoom out such as min value of X axis reaches UNIX epoch start
Hi, I could not reproduce it (at least using PyQt5)
I tried with timestamps = numpy.linspace(now - 3600*24*365*10, now, 100)
to have bigger time period and scrolled to zoom out, when the left-hand side reaches below 1970, axis is gone. When zooming in, in appears again.
PyQt5 is from fresh conda environment.
i so like this! thanks!!
...for me it throws an error though
QGridLayoutEngine::addItem: Cell (3, 1) already taken
when using. so i added two lines to attachToPlotItem()
:
# remove the old item to not get message from QGridLayoutEngine
old_item = plotItem.layout.itemAt(*pos)
plotItem.layout.removeItem(old_item)
# add new one
right before addItem
is called.
maybe this can be of use.. 😃
thank you you saved ma time
Awesome! Although the X axis is gone if you zoom out such as min value of X axis reaches UNIX epoch start. Then you would need to reset the view to get X axis back.