-
-
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 takenwhen 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 oneright 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.