Date axis item (#1154)

* Add DateAxisItem

* Change style to camelCase

* Fix missing first tick for negative timestamps

* Add ms precision, auto skipping

Auto skipping allows a zoom level to skip ticks automatically if the
maximum number of ticks/pt is exceeded

* fixes suggested by @goetzc

* workaround for negative argument to utcfromtimestamp on windows

* attachToPlotItem method

* default date axis orientation

* Use new DateAxisItem in Plot Customization example

* attachToPlotItem bugfix

* examples of DateAxisItem

* modified description of customPlot example

* added descriptions to the new examples, reformatted their code, included the first one into utils.py

* typo

* Refactored code for setting axis items into new function

Replaces "DateAxisItem.attachToPlotItem"

* Fix string comparison with ==

* Doc: Slightly more text for DateAxisItem, small improvement for PlotItem

* Make PlotWidget.setAxisItems official

* Fix typo in docstring

* renamed an example

* merge bug fix

* Revert "merge bug fix"

This reverts commit 876b5a7cdb.

* Real bug fix

* support for dates upto -1e13..1e13

* Automatically limit DateAxisItem to a range from -1e12 to 1e12 years

Very large years (|y|>1e13) cause infinite loop, and since nobody
needs time 100 times larger than the age of the universe anyways,
this constrains it to 1e12.

Following suggestion by @axil:
https://github.com/pyqtgraph/pyqtgraph/pull/1154#issuecomment-612662168

* Also catch ValueErrors occuring on Linux before OverfloeErrors

While zooming out, before hitting OverflowErrors, utctimestamp
produces ValueErrors (at least on my Linux machine), so they
are also catched.

* Fix: Timestamp 0 corresponds to year 1970

For large years, x axis labels jump by 1970 years if it is not
accounted for timestamp 0 to be equal to year 1970.

* Fix: When zooming into extreme dates, OSError occurs

This commit catches the OSError like the other observed errors

* Disable stepping below years for dates outside *_REGULAR_TIMESTAMP

2 reasons: First: At least on my Linux machine, zooming into
those dates creates infinite loops. Second: Nobody needs
sub-year-precision for those extreme years anyways.

* Adapt zoom level sizes based on current font size and screen resolution

This is somewhat experimental. With this commit, no longer 60 px are
assumed as width for all zoom levels, but the current font and
display resolution are considered to calculate the width of ticks in
each zoom level. See the new function `updateZoomLevels` for
details.
Before calling this function, overridden functions `paint` and
`generateDrawSpecs` provide information over the current display
and font via `self.fontScaleFactor` and `self.fontMetrics`.

* Meaningful error meassage when adding axis to multiple PlotItems

As @axil noted in the DateAxisItem PR, currently users get a
segmentation fault when one tries to add an axis to multiple
PlotItems. This commit adds a meaningful RuntimeError message
for that case.

* setZoomLevelForDensity: Refactoring and calculating optimal spacing on the fly

* DateTimeAxis Fix: 1970 shows when zooming far out

* Refactoring: Make zoomLevels a customizable dict again

* updated the dateaxisitem example

* Fix: Get screen resolution in a way that also works for Qt 4

This is both a simplification in code and an improvement in backwards compatibility with Qt 4.

* DateAxisItem Fix: Also resolve time below 0.5 seconds

* unix line endings in examples

* DateTimeAxis Fix: For years < 1 and > 9999, stepping broke

Stepping was off by 1970 years for years < 1 and > 9999,
resulting in a gap in ticks visible when zooming out. Fixed by
subtracting the usual 1970 years.

* DateTimeAxis Fix: Zooming out too far causes infinite loop

Fixed by setting default limits to +/- 1e10 years. Should still
be enough.

* improved second dateaxisitem example

* 1..9999 years limit

* DateTimeAxis: Use OrderedDict to stay compatible with Python < 3-6

* DateAxisItem: Use font height to determine spacing for vertical axes

* window title

* added dateaxisitem.rst

* updated index.rst

Co-authored-by: Lukas Heiniger <lukas.heiniger@sed.ethz.ch>
Co-authored-by: Lev Maximov <lev.maximov@gmail.com>
Co-authored-by: 2xB <2xB@users.noreply.github.com>
This commit is contained in:
Lev Maximov 2020-04-28 01:43:22 +07:00 committed by GitHub
parent a2053b13d0
commit a76d9daec2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 531 additions and 63 deletions

View File

@ -0,0 +1,8 @@
DateAxisItem
============
.. autoclass:: pyqtgraph.DateAxisItem
:members:
.. automethod:: pyqtgraph.DateAxisItem.__init__

View File

@ -43,3 +43,4 @@ Contents:
graphicsitem
uigraphicsitem
graphicswidgetanchor
dateaxisitem

View File

@ -2,6 +2,7 @@ files = """ArrowItem
AxisItem
ButtonItem
CurvePoint
DateAxisItem
GradientEditorItem
GradientLegend
GraphicsLayout

33
examples/DateAxisItem.py Normal file
View File

@ -0,0 +1,33 @@
"""
Demonstrates the usage of DateAxisItem to display properly-formatted
timestamps on x-axis which automatically adapt to current zoom level.
"""
import initExample ## Add path to library (just for examples; you do not need this)
import time
from datetime import datetime, timedelta
import numpy as np
import pyqtgraph as pg
from pyqtgraph.Qt import QtGui
app = QtGui.QApplication([])
# Create a plot with a date-time axis
w = pg.PlotWidget(axisItems = {'bottom': pg.DateAxisItem()})
w.showGrid(x=True, y=True)
# Plot sin(1/x^2) with timestamps in the last 100 years
now = time.time()
x = np.linspace(2*np.pi, 1000*2*np.pi, 8301)
w.plot(now-(2*np.pi/x)**2*100*np.pi*1e7, np.sin(x), symbol='o')
w.setWindowTitle('pyqtgraph example: DateAxisItem')
w.show()
## 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'):
app.exec_()

View File

@ -0,0 +1,48 @@
"""
Demonstrates the usage of DateAxisItem in a layout created with Qt Designer.
The spotlight here is on the 'setAxisItems' method, without which
one would have to subclass plotWidget in order to attach a dateaxis to it.
"""
import initExample ## Add path to library (just for examples; you do not need this)
import sys
import time
import numpy as np
from PyQt5 import QtWidgets, QtCore, uic
import pyqtgraph as pg
pg.setConfigOption('background', 'w')
pg.setConfigOption('foreground', 'k')
BLUE = pg.mkPen('#1f77b4')
Design, _ = uic.loadUiType('DateAxisItem_QtDesigner.ui')
class ExampleApp(QtWidgets.QMainWindow, Design):
def __init__(self):
super().__init__()
self.setupUi(self)
now = time.time()
# Plot random values with timestamps in the last 6 months
timestamps = np.linspace(now - 6*30*24*3600, now, 100)
self.curve = self.plotWidget.plot(x=timestamps, y=np.random.rand(100),
symbol='o', symbolSize=5, pen=BLUE)
# 'o' circle 't' triangle 'd' diamond '+' plus 's' square
self.plotWidget.setAxisItems({'bottom': pg.DateAxisItem()})
self.plotWidget.showGrid(x=True, y=True)
app = QtWidgets.QApplication(sys.argv)
app.setStyle(QtWidgets.QStyleFactory.create('Fusion'))
app.setPalette(QtWidgets.QApplication.style().standardPalette())
window = ExampleApp()
window.setWindowTitle('pyqtgraph example: DateAxisItem_QtDesigner')
window.show()
## 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'):
app.exec_()

View File

@ -0,0 +1,44 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>MainWindow</class>
<widget class="QMainWindow" name="MainWindow">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>536</width>
<height>381</height>
</rect>
</property>
<property name="windowTitle">
<string>MainWindow</string>
</property>
<widget class="QWidget" name="centralwidget">
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="PlotWidget" name="plotWidget"/>
</item>
</layout>
</widget>
<widget class="QMenuBar" name="menubar">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>536</width>
<height>18</height>
</rect>
</property>
</widget>
<widget class="QStatusBar" name="statusbar"/>
</widget>
<customwidgets>
<customwidget>
<class>PlotWidget</class>
<extends>QGraphicsView</extends>
<header>pyqtgraph</header>
</customwidget>
</customwidgets>
<resources/>
<connections/>
</ui>

View File

@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
"""
This example demonstrates the creation of a plot with a customized
AxisItem and ViewBox.
This example demonstrates the creation of a plot with
DateAxisItem and a customized ViewBox.
"""
@ -12,40 +12,6 @@ from pyqtgraph.Qt import QtCore, QtGui
import numpy as np
import time
class DateAxis(pg.AxisItem):
def tickStrings(self, values, scale, spacing):
strns = []
rng = max(values)-min(values)
#if rng < 120:
# return pg.AxisItem.tickStrings(self, values, scale, spacing)
if rng < 3600*24:
string = '%H:%M:%S'
label1 = '%b %d -'
label2 = ' %b %d, %Y'
elif rng >= 3600*24 and rng < 3600*24*30:
string = '%d'
label1 = '%b - '
label2 = '%b, %Y'
elif rng >= 3600*24*30 and rng < 3600*24*30*24:
string = '%b'
label1 = '%Y -'
label2 = ' %Y'
elif rng >=3600*24*30*24:
string = '%Y'
label1 = ''
label2 = ''
for x in values:
try:
strns.append(time.strftime(string, time.localtime(x)))
except ValueError: ## Windows can't handle dates before 1970
strns.append('')
try:
label = time.strftime(label1, time.localtime(min(values)))+time.strftime(label2, time.localtime(max(values)))
except ValueError:
label = ''
#self.setLabel(text=label)
return strns
class CustomViewBox(pg.ViewBox):
def __init__(self, *args, **kwds):
pg.ViewBox.__init__(self, *args, **kwds)
@ -65,10 +31,10 @@ class CustomViewBox(pg.ViewBox):
app = pg.mkQApp()
axis = DateAxis(orientation='bottom')
axis = pg.DateAxisItem(orientation='bottom')
vb = CustomViewBox()
pw = pg.PlotWidget(viewBox=vb, axisItems={'bottom': axis}, enableMenu=False, title="PlotItem with custom axis and ViewBox<br>Menu disabled, mouse behavior changed: left-drag to zoom, right-click to reset zoom")
pw = pg.PlotWidget(viewBox=vb, axisItems={'bottom': axis}, enableMenu=False, title="PlotItem with DateAxisItem and custom ViewBox<br>Menu disabled, mouse behavior changed: left-drag to zoom, right-click to reset zoom")
dates = np.arange(8) * (3600*24*356)
pw.plot(x=dates, y=[1,6,2,4,3,5,6,8], symbol='o')
pw.show()

View File

@ -14,6 +14,7 @@ examples = OrderedDict([
('Crosshair / Mouse interaction', 'crosshair.py'),
('Data Slicing', 'DataSlicing.py'),
('Plot Customization', 'customPlot.py'),
('Timestamps on x axis', 'DateAxisItem.py'),
('Image Analysis', 'imageAnalysis.py'),
('ViewBox Features', 'ViewBoxFeatures.py'),
('Dock widgets', 'dockarea.py'),

View File

@ -219,6 +219,7 @@ from .graphicsItems.ViewBox import *
from .graphicsItems.ArrowItem import *
from .graphicsItems.ImageItem import *
from .graphicsItems.AxisItem import *
from .graphicsItems.DateAxisItem import *
from .graphicsItems.LabelItem import *
from .graphicsItems.CurvePoint import *
from .graphicsItems.GraphicsWidgetAnchor import *

View File

@ -507,20 +507,29 @@ class AxisItem(GraphicsWidget):
def linkToView(self, view):
"""Link this axis to a ViewBox, causing its displayed range to match the visible range of the view."""
oldView = self.linkedView()
self.unlinkFromView()
self._linkedView = weakref.ref(view)
if self.orientation in ['right', 'left']:
view.sigYRangeChanged.connect(self.linkedViewChanged)
else:
view.sigXRangeChanged.connect(self.linkedViewChanged)
view.sigResized.connect(self.linkedViewChanged)
def unlinkFromView(self):
"""Unlink this axis from a ViewBox."""
oldView = self.linkedView()
self._linkedView = None
if self.orientation in ['right', 'left']:
if oldView is not None:
oldView.sigYRangeChanged.disconnect(self.linkedViewChanged)
view.sigYRangeChanged.connect(self.linkedViewChanged)
else:
if oldView is not None:
oldView.sigXRangeChanged.disconnect(self.linkedViewChanged)
view.sigXRangeChanged.connect(self.linkedViewChanged)
if oldView is not None:
oldView.sigResized.disconnect(self.linkedViewChanged)
view.sigResized.connect(self.linkedViewChanged)
def linkedViewChanged(self, view, newRange=None):
if self.orientation in ['right', 'left']:

View File

@ -0,0 +1,319 @@
import sys
import numpy as np
import time
from datetime import datetime, timedelta
from .AxisItem import AxisItem
from ..pgcollections import OrderedDict
__all__ = ['DateAxisItem', 'ZoomLevel']
MS_SPACING = 1/1000.0
SECOND_SPACING = 1
MINUTE_SPACING = 60
HOUR_SPACING = 3600
DAY_SPACING = 24 * HOUR_SPACING
WEEK_SPACING = 7 * DAY_SPACING
MONTH_SPACING = 30 * DAY_SPACING
YEAR_SPACING = 365 * DAY_SPACING
if sys.platform == 'win32':
_epoch = datetime.utcfromtimestamp(0)
def utcfromtimestamp(timestamp):
return _epoch + timedelta(seconds=timestamp)
else:
utcfromtimestamp = datetime.utcfromtimestamp
MIN_REGULAR_TIMESTAMP = (datetime(1, 1, 1) - datetime(1970,1,1)).total_seconds()
MAX_REGULAR_TIMESTAMP = (datetime(9999, 1, 1) - datetime(1970,1,1)).total_seconds()
SEC_PER_YEAR = 365.25*24*3600
def makeMSStepper(stepSize):
def stepper(val, n):
if val < MIN_REGULAR_TIMESTAMP or val > MAX_REGULAR_TIMESTAMP:
return np.inf
val *= 1000
f = stepSize * 1000
return (val // (n*f) + 1) * (n*f) / 1000.0
return stepper
def makeSStepper(stepSize):
def stepper(val, n):
if val < MIN_REGULAR_TIMESTAMP or val > MAX_REGULAR_TIMESTAMP:
return np.inf
return (val // (n*stepSize) + 1) * (n*stepSize)
return stepper
def makeMStepper(stepSize):
def stepper(val, n):
if val < MIN_REGULAR_TIMESTAMP or val > MAX_REGULAR_TIMESTAMP:
return np.inf
d = utcfromtimestamp(val)
base0m = (d.month + n*stepSize - 1)
d = datetime(d.year + base0m // 12, base0m % 12 + 1, 1)
return (d - datetime(1970, 1, 1)).total_seconds()
return stepper
def makeYStepper(stepSize):
def stepper(val, n):
if val < MIN_REGULAR_TIMESTAMP or val > MAX_REGULAR_TIMESTAMP:
return np.inf
d = utcfromtimestamp(val)
next_year = (d.year // (n*stepSize) + 1) * (n*stepSize)
if next_year > 9999:
return np.inf
next_date = datetime(next_year, 1, 1)
return (next_date - datetime(1970, 1, 1)).total_seconds()
return stepper
class TickSpec:
""" Specifies the properties for a set of date ticks and computes ticks
within a given utc timestamp range """
def __init__(self, spacing, stepper, format, autoSkip=None):
"""
============= ==========================================================
Arguments
spacing approximate (average) tick spacing
stepper a stepper function that takes a utc time stamp and a step
steps number n to compute the start of the next unit. You
can use the make_X_stepper functions to create common
steppers.
format a strftime compatible format string which will be used to
convert tick locations to date/time strings
autoSkip list of step size multipliers to be applied when the tick
density becomes too high. The tick spec automatically
applies additional powers of 10 (10, 100, ...) to the list
if necessary. Set to None to switch autoSkip off
============= ==========================================================
"""
self.spacing = spacing
self.step = stepper
self.format = format
self.autoSkip = autoSkip
def makeTicks(self, minVal, maxVal, minSpc):
ticks = []
n = self.skipFactor(minSpc)
x = self.step(minVal, n)
while x <= maxVal:
ticks.append(x)
x = self.step(x, n)
return (np.array(ticks), n)
def skipFactor(self, minSpc):
if self.autoSkip is None or minSpc < self.spacing:
return 1
factors = np.array(self.autoSkip, dtype=np.float)
while True:
for f in factors:
spc = self.spacing * f
if spc > minSpc:
return int(f)
factors *= 10
class ZoomLevel:
""" Generates the ticks which appear in a specific zoom level """
def __init__(self, tickSpecs, exampleText):
"""
============= ==========================================================
tickSpecs a list of one or more TickSpec objects with decreasing
coarseness
============= ==========================================================
"""
self.tickSpecs = tickSpecs
self.utcOffset = 0
self.exampleText = exampleText
def tickValues(self, minVal, maxVal, minSpc):
# return tick values for this format in the range minVal, maxVal
# the return value is a list of tuples (<avg spacing>, [tick positions])
# minSpc indicates the minimum spacing (in seconds) between two ticks
# to fullfill the maxTicksPerPt constraint of the DateAxisItem at the
# current zoom level. This is used for auto skipping ticks.
allTicks = []
valueSpecs = []
# back-project (minVal maxVal) to UTC, compute ticks then offset to
# back to local time again
utcMin = minVal - self.utcOffset
utcMax = maxVal - self.utcOffset
for spec in self.tickSpecs:
ticks, skipFactor = spec.makeTicks(utcMin, utcMax, minSpc)
# reposition tick labels to local time coordinates
ticks += self.utcOffset
# remove any ticks that were present in higher levels
tick_list = [x for x in ticks.tolist() if x not in allTicks]
allTicks.extend(tick_list)
valueSpecs.append((spec.spacing, tick_list))
# if we're skipping ticks on the current level there's no point in
# producing lower level ticks
if skipFactor > 1:
break
return valueSpecs
YEAR_MONTH_ZOOM_LEVEL = ZoomLevel([
TickSpec(YEAR_SPACING, makeYStepper(1), '%Y', autoSkip=[1, 5, 10, 25]),
TickSpec(MONTH_SPACING, makeMStepper(1), '%b')
], "YYYY")
MONTH_DAY_ZOOM_LEVEL = ZoomLevel([
TickSpec(MONTH_SPACING, makeMStepper(1), '%b'),
TickSpec(DAY_SPACING, makeSStepper(DAY_SPACING), '%d', autoSkip=[1, 5])
], "MMM")
DAY_HOUR_ZOOM_LEVEL = ZoomLevel([
TickSpec(DAY_SPACING, makeSStepper(DAY_SPACING), '%a %d'),
TickSpec(HOUR_SPACING, makeSStepper(HOUR_SPACING), '%H:%M', autoSkip=[1, 6])
], "MMM 00")
HOUR_MINUTE_ZOOM_LEVEL = ZoomLevel([
TickSpec(DAY_SPACING, makeSStepper(DAY_SPACING), '%a %d'),
TickSpec(MINUTE_SPACING, makeSStepper(MINUTE_SPACING), '%H:%M',
autoSkip=[1, 5, 15])
], "MMM 00")
HMS_ZOOM_LEVEL = ZoomLevel([
TickSpec(SECOND_SPACING, makeSStepper(SECOND_SPACING), '%H:%M:%S',
autoSkip=[1, 5, 15, 30])
], "99:99:99")
MS_ZOOM_LEVEL = ZoomLevel([
TickSpec(MINUTE_SPACING, makeSStepper(MINUTE_SPACING), '%H:%M:%S'),
TickSpec(MS_SPACING, makeMSStepper(MS_SPACING), '%S.%f',
autoSkip=[1, 5, 10, 25])
], "99:99:99")
class DateAxisItem(AxisItem):
"""
**Bases:** :class:`AxisItem <pyqtgraph.AxisItem>`
An AxisItem that displays dates from unix timestamps.
The display format is adjusted automatically depending on the current time
density (seconds/point) on the axis. For more details on changing this
behaviour, see :func:`setZoomLevelForDensity() <pyqtgraph.DateAxisItem.setZoomLevelForDensity>`.
Can be added to an existing plot e.g. via
:func:`setAxisItems({'bottom':axis}) <pyqtgraph.PlotItem.setAxisItems>`.
"""
def __init__(self, orientation='bottom', **kwargs):
"""
Create a new DateAxisItem.
For `orientation` and `**kwargs`, see
:func:`AxisItem.__init__ <pyqtgraph.AxisItem.__init__>`.
"""
super(DateAxisItem, self).__init__(orientation, **kwargs)
# Set the zoom level to use depending on the time density on the axis
self.utcOffset = time.timezone
self.zoomLevels = OrderedDict([
(np.inf, YEAR_MONTH_ZOOM_LEVEL),
(5 * 3600*24, MONTH_DAY_ZOOM_LEVEL),
(6 * 3600, DAY_HOUR_ZOOM_LEVEL),
(15 * 60, HOUR_MINUTE_ZOOM_LEVEL),
(30, HMS_ZOOM_LEVEL),
(1, MS_ZOOM_LEVEL),
])
def tickStrings(self, values, scale, spacing):
tickSpecs = self.zoomLevel.tickSpecs
tickSpec = next((s for s in tickSpecs if s.spacing == spacing), None)
try:
dates = [utcfromtimestamp(v - self.utcOffset) for v in values]
except (OverflowError, ValueError, OSError):
# should not normally happen
return ['%g' % ((v-self.utcOffset)//SEC_PER_YEAR + 1970) for v in values]
formatStrings = []
for x in dates:
try:
s = x.strftime(tickSpec.format)
if '%f' in tickSpec.format:
# we only support ms precision
s = s[:-3]
elif '%Y' in tickSpec.format:
s = s.lstrip('0')
formatStrings.append(s)
except ValueError: # Windows can't handle dates before 1970
formatStrings.append('')
return formatStrings
def tickValues(self, minVal, maxVal, size):
density = (maxVal - minVal) / size
self.setZoomLevelForDensity(density)
values = self.zoomLevel.tickValues(minVal, maxVal, minSpc=self.minSpacing)
return values
def setZoomLevelForDensity(self, density):
"""
Setting `zoomLevel` and `minSpacing` based on given density of seconds per pixel
The display format is adjusted automatically depending on the current time
density (seconds/point) on the axis. You can customize the behaviour by
overriding this function or setting a different set of zoom levels
than the default one. The `zoomLevels` variable is a dictionary with the
maximal distance of ticks in seconds which are allowed for each zoom level
before the axis switches to the next coarser level. To create custom
zoom levels, override this function and provide custom `zoomLevelWidths` and
`zoomLevels`.
"""
padding = 10
# Size in pixels a specific tick label will take
if self.orientation in ['bottom', 'top']:
def sizeOf(text):
return self.fontMetrics.boundingRect(text).width() + padding*self.fontScaleFactor
else:
def sizeOf(text):
return self.fontMetrics.boundingRect(text).height() + padding*self.fontScaleFactor
# Fallback zoom level: Years/Months
self.zoomLevel = YEAR_MONTH_ZOOM_LEVEL
for maximalSpacing, zoomLevel in self.zoomLevels.items():
size = sizeOf(zoomLevel.exampleText)
# Test if zoom level is too fine grained
if maximalSpacing/size < density:
break
self.zoomLevel = zoomLevel
# Set up zoomLevel
self.zoomLevel.utcOffset = self.utcOffset
# Calculate minimal spacing of items on the axis
size = sizeOf(self.zoomLevel.exampleText)
self.minSpacing = density*size
def linkToView(self, view):
super(DateAxisItem, self).linkToView(view)
# Set default limits
_min = MIN_REGULAR_TIMESTAMP
_max = MAX_REGULAR_TIMESTAMP
if self.orientation in ['right', 'left']:
view.setLimits(yMin=_min, yMax=_max)
else:
view.setLimits(xMin=_min, xMax=_max)
def generateDrawSpecs(self, p):
# Get font metrics from QPainter
# Not happening in "paint", as the QPainter p there is a different one from the one here,
# so changing that font could cause unwanted side effects
if self.tickFont is not None:
p.setFont(self.tickFont)
self.fontMetrics = p.fontMetrics()
# Get font scale factor by current window resolution
self.fontScaleFactor = p.device().logicalDpiX() / 96
return super(DateAxisItem, self).generateDrawSpecs(p)

View File

@ -95,7 +95,7 @@ class PlotItem(GraphicsWidget):
def __init__(self, parent=None, name=None, labels=None, title=None, viewBox=None, axisItems=None, enableMenu=True, **kargs):
"""
Create a new PlotItem. All arguments are optional.
Any extra keyword arguments are passed to PlotItem.plot().
Any extra keyword arguments are passed to :func:`PlotItem.plot() <pyqtgraph.PlotItem.plot>`.
============== ==========================================================================================
**Arguments:**
@ -153,20 +153,9 @@ class PlotItem(GraphicsWidget):
self.legend = None
## Create and place axis items
if axisItems is None:
axisItems = {}
# Initialize axis items
self.axes = {}
for k, pos in (('top', (1,1)), ('bottom', (3,1)), ('left', (2,0)), ('right', (2,2))):
if k in axisItems:
axis = axisItems[k]
else:
axis = AxisItem(orientation=k, parent=self)
axis.linkToView(self.vb)
self.axes[k] = {'item': axis, 'pos': pos}
self.layout.addItem(axis, *pos)
axis.setZValue(-1000)
axis.setFlag(axis.ItemNegativeZStacksBehindParent)
self.setAxisItems(axisItems)
self.titleLabel = LabelItem('', size='11pt', parent=self)
self.layout.addItem(self.titleLabel, 0, 1)
@ -254,11 +243,6 @@ class PlotItem(GraphicsWidget):
self.ctrl.maxTracesCheck.toggled.connect(self.updateDecimation)
self.ctrl.maxTracesSpin.valueChanged.connect(self.updateDecimation)
self.hideAxis('right')
self.hideAxis('top')
self.showAxis('left')
self.showAxis('bottom')
if labels is None:
labels = {}
for label in list(self.axes.keys()):
@ -300,6 +284,58 @@ class PlotItem(GraphicsWidget):
locals()[m] = _create_method(m)
del _create_method
def setAxisItems(self, axisItems=None):
"""
Place axis items as given by `axisItems`. Initializes non-existing axis items.
============== ==========================================================================================
**Arguments:**<
*axisItems* Optional dictionary instructing the PlotItem to use pre-constructed items
for its axes. The dict keys must be axis names ('left', 'bottom', 'right', 'top')
and the values must be instances of AxisItem (or at least compatible with AxisItem).
============== ==========================================================================================
"""
if axisItems is None:
axisItems = {}
# Array containing visible axis items
# Also containing potentially hidden axes, but they are not touched so it does not matter
visibleAxes = ['left', 'bottom']
visibleAxes.append(axisItems.keys()) # Note that it does not matter that this adds
# some values to visibleAxes a second time
for k, pos in (('top', (1,1)), ('bottom', (3,1)), ('left', (2,0)), ('right', (2,2))):
if k in self.axes:
if k not in axisItems:
continue # Nothing to do here
# Remove old axis
oldAxis = self.axes[k]['item']
self.layout.removeItem(oldAxis)
oldAxis.scene().removeItem(oldAxis)
oldAxis.unlinkFromView()
# Create new axis
if k in axisItems:
axis = axisItems[k]
if axis.scene() is not None:
if axis != self.axes[k]["item"]:
raise RuntimeError("Can't add an axis to multiple plots.")
else:
axis = AxisItem(orientation=k, parent=self)
# Set up new axis
axis.linkToView(self.vb)
self.axes[k] = {'item': axis, 'pos': pos}
self.layout.addItem(axis, *pos)
axis.setZValue(-1000)
axis.setFlag(axis.ItemNegativeZStacksBehindParent)
axisVisible = k in visibleAxes
self.showAxis(k, axisVisible)
def setLogMode(self, x=None, y=None):
"""

View File

@ -24,6 +24,7 @@ class PlotWidget(GraphicsView):
:func:`addItem <pyqtgraph.PlotItem.addItem>`,
:func:`removeItem <pyqtgraph.PlotItem.removeItem>`,
:func:`clear <pyqtgraph.PlotItem.clear>`,
:func:`setAxisItems <pyqtgraph.PlotItem.setAxisItems>`,
:func:`setXRange <pyqtgraph.ViewBox.setXRange>`,
:func:`setYRange <pyqtgraph.ViewBox.setYRange>`,
:func:`setRange <pyqtgraph.ViewBox.setRange>`,
@ -55,7 +56,7 @@ class PlotWidget(GraphicsView):
self.setCentralItem(self.plotItem)
## Explicitly wrap methods from plotItem
## NOTE: If you change this list, update the documentation above as well.
for m in ['addItem', 'removeItem', 'autoRange', 'clear', 'setXRange',
for m in ['addItem', 'removeItem', 'autoRange', 'clear', 'setAxisItems', 'setXRange',
'setYRange', 'setRange', 'setAspectLocked', 'setMouseEnabled',
'setXLink', 'setYLink', 'enableAutoRange', 'disableAutoRange',
'setLimits', 'register', 'unregister', 'viewRect']:
@ -96,4 +97,4 @@ class PlotWidget(GraphicsView):
return self.plotItem