Add scrolling from right and cross-hair

Modify the default ``ViewBox`` scroll to zoom behaviour such that
whatever right-most point is visible is used as the "center" for
zooming. Add a "traditional" cross-hair cursor.
bar_select
Tyler Goodlet 2020-06-13 19:25:44 -04:00
parent c8afdb0adc
commit 730241bb8a
2 changed files with 73 additions and 34 deletions

View File

@ -39,11 +39,10 @@ def run_qtrio(
if app is None: if app is None:
app = PyQt5.QtWidgets.QApplication([]) app = PyQt5.QtWidgets.QApplication([])
# This code is from Nathaniel: # This code is from Nathaniel, and I quote:
# "This is substantially faster than using a signal... for some
# This is substantially faster than using a signal... for some
# reason Qt signal dispatch is really slow (and relies on events # reason Qt signal dispatch is really slow (and relies on events
# underneath anyway, so this is strictly less work) # underneath anyway, so this is strictly less work)."
REENTER_EVENT = QtCore.QEvent.Type(QtCore.QEvent.registerEventType()) REENTER_EVENT = QtCore.QEvent.Type(QtCore.QEvent.registerEventType())
class ReenterEvent(QtCore.QEvent): class ReenterEvent(QtCore.QEvent):

View File

@ -260,32 +260,54 @@ class YAxisLabel(AxisLabel):
self.setPos(new_pos) self.setPos(new_pos)
class ScrollFromRightView(pg.ViewBox):
def wheelEvent(self, ev, axis=None):
if axis in (0, 1):
mask = [False, False]
mask[axis] = self.state['mouseEnabled'][axis]
else:
mask = self.state['mouseEnabled'][:]
# actual scaling factor
s = 1.02 ** (ev.delta() * self.state['wheelScaleFactor'])
s = [(None if m is False else s) for m in mask]
# XXX: scroll "around" the right most element in the view
# center = pg.Point(
# fn.invertQTransform(self.childGroup.transform()).map(ev.pos())
# )
furthest_right_coord = self.boundingRect().topRight()
center = pg.Point(
fn.invertQTransform(self.childGroup.transform()).map(furthest_right_coord)
)
self._resetTarget()
self.scaleBy(s, center)
ev.accept()
self.sigRangeChangedManually.emit(mask)
# TODO: convert this to a ``ViewBox`` type giving us # TODO: convert this to a ``ViewBox`` type giving us
# control over mouse scrolling and a context menu # control over mouse scrolling and a context menu
# This is a sub-class of ``GracphicView`` which can
# take a ``background`` color setting.
class CustomPlotWidget(pg.PlotWidget): class CustomPlotWidget(pg.PlotWidget):
"""``GraphicsView`` subtype containing a single ``PlotItem``.
(Could be replaced with a ``pg.GraphicsLayoutWidget`` if we
eventually want multiple plots managed together).
"""
sig_mouse_leave = QtCore.Signal(object) sig_mouse_leave = QtCore.Signal(object)
sig_mouse_enter = QtCore.Signal(object) sig_mouse_enter = QtCore.Signal(object)
# def wheelEvent(self, ev, axis=None):
# if axis in (0, 1):
# mask = [False, False]
# mask[axis] = self.state['mouseEnabled'][axis]
# else:
# mask = self.state['mouseEnabled'][:]
# # actual scaling factor
# s = 1.02 ** (ev.delta() * self.state['wheelScaleFactor'])
# s = [(None if m is False else s) for m in mask]
# self._resetTarget()
# self.scaleBy(s, center)
# ev.accept()
# self.sigRangeChangedManually.emit(mask)
def enterEvent(self, ev): # noqa def enterEvent(self, ev): # noqa
pg.PlotWidget.enterEvent(self, ev)
self.sig_mouse_enter.emit(self) self.sig_mouse_enter.emit(self)
def leaveEvent(self, ev): # noqa def leaveEvent(self, ev): # noqa
pg.PlotWidget.leaveEvent(self, ev)
self.sig_mouse_leave.emit(self) self.sig_mouse_leave.emit(self)
self.scene().leaveEvent(ev) self.scene().leaveEvent(ev)
@ -441,6 +463,8 @@ class CrossHairItem(pg.GraphicsObject):
class BarItem(pg.GraphicsObject): class BarItem(pg.GraphicsObject):
# XXX: From the customGraphicsItem.py example:
# The only required methods are paint() and boundingRect()
w = 0.5 w = 0.5
@ -452,7 +476,19 @@ class BarItem(pg.GraphicsObject):
super().__init__() super().__init__()
self.generatePicture() self.generatePicture()
# TODO: this is the routine to be retriggered for redraw
@timeit
def generatePicture(self):
# pre-computing a QPicture object allows paint() to run much
# more quickly, rather than re-drawing the shapes every time.
self.picture = QtGui.QPicture()
p = QtGui.QPainter(self.picture)
self._generate(p)
p.end()
def _generate(self, p): def _generate(self, p):
# XXX: overloaded method to allow drawing other candle types
high_to_low = np.array( high_to_low = np.array(
[QtCore.QLineF(q.id, q.low, q.id, q.high) for q in Quotes] [QtCore.QLineF(q.id, q.low, q.id, q.high) for q in Quotes]
) )
@ -476,18 +512,14 @@ class BarItem(pg.GraphicsObject):
p.setPen(self.bear_brush) p.setPen(self.bear_brush)
p.drawLines(*lines[short_bars]) p.drawLines(*lines[short_bars])
# TODO: this is the routine to be retriggered for redraw
@timeit
def generatePicture(self):
self.picture = QtGui.QPicture()
p = QtGui.QPainter(self.picture)
self._generate(p)
p.end()
def paint(self, p, *args): def paint(self, p, *args):
p.drawPicture(0, 0, self.picture) p.drawPicture(0, 0, self.picture)
def boundingRect(self): def boundingRect(self):
# boundingRect _must_ indicate the entire area that will be
# drawn on or else we will get artifacts and possibly crashing.
# (in this case, QPicture does all the work of computing the
# bouning rect for us)
return QtCore.QRectF(self.picture.boundingRect()) return QtCore.QRectF(self.picture.boundingRect())
@ -529,7 +561,13 @@ def _configure_quotes_chart(
chart.hideAxis('left') chart.hideAxis('left')
chart.showAxis('right') chart.showAxis('right')
# adds all bar/candle graphics objects for each
# data point in the np array buffer to
# be drawn on next render cycle
chart.addItem(_get_chart_points(style)) chart.addItem(_get_chart_points(style))
# set panning limits
chart.setLimits( chart.setLimits(
xMin=Quotes[0].id, xMin=Quotes[0].id,
xMax=Quotes[-1].id, xMax=Quotes[-1].id,
@ -537,8 +575,8 @@ def _configure_quotes_chart(
yMin=Quotes.low.min() * 0.98, yMin=Quotes.low.min() * 0.98,
yMax=Quotes.high.max() * 1.02, yMax=Quotes.high.max() * 1.02,
) )
chart.showGrid(x=True, y=True) chart.showGrid(x=True, y=True, alpha=0.4)
chart.setCursor(QtCore.Qt.BlankCursor) chart.setCursor(QtCore.Qt.CrossCursor)
# assign callback for rescaling y-axis automatically # assign callback for rescaling y-axis automatically
# based on y-range contents # based on y-range contents
@ -607,6 +645,7 @@ class QuotesChart(QtGui.QWidget):
ind.addItem(curve) ind.addItem(curve)
ind.hideAxis('left') ind.hideAxis('left')
ind.showAxis('right') ind.showAxis('right')
# XXX: never do this lol
# ind.setAspectLocked(1) # ind.setAspectLocked(1)
ind.setXLink(self.chart) ind.setXLink(self.chart)
ind.setLimits( ind.setLimits(
@ -616,8 +655,8 @@ class QuotesChart(QtGui.QWidget):
yMin=Quotes.open.min() * 0.98, yMin=Quotes.open.min() * 0.98,
yMax=Quotes.open.max() * 1.02, yMax=Quotes.open.max() * 1.02,
) )
ind.showGrid(x=True, y=True) ind.showGrid(x=True, y=True, alpha=0.4)
ind.setCursor(QtCore.Qt.BlankCursor) ind.setCursor(QtCore.Qt.CrossCursor)
def _update_sizes(self): def _update_sizes(self):
min_h_ind = int(self.height() * 0.3 / len(self.indicators)) min_h_ind = int(self.height() * 0.3 / len(self.indicators))
@ -655,7 +694,8 @@ class QuotesChart(QtGui.QWidget):
self.chart = CustomPlotWidget( self.chart = CustomPlotWidget(
parent=self.splitter, parent=self.splitter,
axisItems={'bottom': self.xaxis, 'right': PriceAxis()}, axisItems={'bottom': self.xaxis, 'right': PriceAxis()},
enableMenu=False, viewBox=ScrollFromRightView,
# enableMenu=False,
) )
self.chart.getPlotItem().setContentsMargins(*CHART_MARGINS) self.chart.getPlotItem().setContentsMargins(*CHART_MARGINS)
self.chart.setFrameStyle(QtGui.QFrame.StyledPanel | QtGui.QFrame.Plain) self.chart.setFrameStyle(QtGui.QFrame.StyledPanel | QtGui.QFrame.Plain)
@ -668,7 +708,7 @@ class QuotesChart(QtGui.QWidget):
parent=self.splitter, parent=self.splitter,
axisItems={'bottom': self.xaxis_ind, 'right': PriceAxis()}, axisItems={'bottom': self.xaxis_ind, 'right': PriceAxis()},
# axisItems={'top': self.xaxis_ind, 'right': PriceAxis()}, # axisItems={'top': self.xaxis_ind, 'right': PriceAxis()},
enableMenu=False, # enableMenu=False,
) )
ind.setFrameStyle(QtGui.QFrame.StyledPanel | QtGui.QFrame.Plain) ind.setFrameStyle(QtGui.QFrame.StyledPanel | QtGui.QFrame.Plain)
ind.getPlotItem().setContentsMargins(*CHART_MARGINS) ind.getPlotItem().setContentsMargins(*CHART_MARGINS)