Start grouping interactions into a ``ViewBox``
Move chart resize code into our ``ViewBox`` subtype (a ``ChartView``) in an effort to start organizing interaction behaviour closer to the appropriate underlying objects. Add some docs for all this and do some renaming.its_happening
parent
68266f5a20
commit
ea234f4472
|
@ -89,5 +89,6 @@ def run_qtrio(
|
||||||
|
|
||||||
window.main_widget = main_widget
|
window.main_widget = main_widget
|
||||||
window.setCentralWidget(instance)
|
window.setCentralWidget(instance)
|
||||||
|
# actually render to screen
|
||||||
window.show()
|
window.show()
|
||||||
app.exec_()
|
app.exec_()
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
"""
|
"""
|
||||||
Real-time quotes charting components
|
Real-time quotes charting components
|
||||||
"""
|
"""
|
||||||
from typing import Callable, List, Tuple
|
from typing import List, Tuple
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import pyqtgraph as pg
|
import pyqtgraph as pg
|
||||||
|
@ -13,10 +13,10 @@ from .const import ChartType
|
||||||
from .portfolio import Order, Portfolio
|
from .portfolio import Order, Portfolio
|
||||||
from .utils import fromtimestamp, timeit
|
from .utils import fromtimestamp, timeit
|
||||||
|
|
||||||
__all__ = ('QuotesChart')
|
__all__ = ('SplitterChart')
|
||||||
|
|
||||||
|
|
||||||
# white background for tinas like xb
|
# white background (for tinas like our pal xb)
|
||||||
# pg.setConfigOption('background', 'w')
|
# pg.setConfigOption('background', 'w')
|
||||||
|
|
||||||
# margins
|
# margins
|
||||||
|
@ -260,9 +260,79 @@ class YAxisLabel(AxisLabel):
|
||||||
self.setPos(new_pos)
|
self.setPos(new_pos)
|
||||||
|
|
||||||
|
|
||||||
class ScrollFromRightView(pg.ViewBox):
|
class ChartView(pg.ViewBox):
|
||||||
|
"""Price chart view box with interaction behaviors you'd expect from
|
||||||
|
an interactive platform:
|
||||||
|
|
||||||
|
- zoom on mouse scroll that auto fits y-axis
|
||||||
|
- no vertical scrolling
|
||||||
|
- zoom to a "fixed point" on the y-axis
|
||||||
|
"""
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
parent=None,
|
||||||
|
**kwargs,
|
||||||
|
# invertY=False,
|
||||||
|
):
|
||||||
|
super().__init__(parent=parent, **kwargs)
|
||||||
|
self.chart = parent
|
||||||
|
|
||||||
|
# disable vertical scrolling
|
||||||
|
self.setMouseEnabled(x=True, y=False)
|
||||||
|
|
||||||
|
# assign callback for rescaling y-axis automatically
|
||||||
|
# based on y-range contents
|
||||||
|
self.chart.sigXRangeChanged.connect(self._update_yrange_limits)
|
||||||
|
|
||||||
|
def _update_yrange_limits(self):
|
||||||
|
"""Callback for each y-range update.
|
||||||
|
|
||||||
|
This adds auto-scaling like zoom on the scroll wheel such
|
||||||
|
that data always fits nicely inside the current view of the
|
||||||
|
data set.
|
||||||
|
"""
|
||||||
|
# TODO: this can likely be ported in part to the built-ins:
|
||||||
|
# self.setYRange(Quotes.low.min() * .98, Quotes.high.max() * 1.02)
|
||||||
|
# self.setMouseEnabled(x=True, y=False)
|
||||||
|
# self.setXRange(Quotes[0].id, Quotes[-1].id)
|
||||||
|
# self.setAutoVisible(x=False, y=True)
|
||||||
|
# self.enableAutoRange(x=False, y=True)
|
||||||
|
|
||||||
|
chart = self.chart
|
||||||
|
chart_parent = chart.parent
|
||||||
|
|
||||||
|
vr = self.chart.viewRect()
|
||||||
|
lbar, rbar = int(vr.left()), int(vr.right())
|
||||||
|
|
||||||
|
if chart_parent.signals_visible:
|
||||||
|
chart_parent._show_text_signals(lbar, rbar)
|
||||||
|
|
||||||
|
bars = Quotes[lbar:rbar]
|
||||||
|
ylow = bars.low.min() * 0.98
|
||||||
|
yhigh = bars.high.max() * 1.02
|
||||||
|
|
||||||
|
std = np.std(bars.close)
|
||||||
|
chart.setLimits(yMin=ylow, yMax=yhigh, minYRange=std)
|
||||||
|
chart.setYRange(ylow, yhigh)
|
||||||
|
|
||||||
|
for i, d in chart_parent.indicators:
|
||||||
|
# ydata = i.plotItem.items[0].getData()[1]
|
||||||
|
ydata = d[lbar:rbar]
|
||||||
|
ylow = ydata.min() * 0.98
|
||||||
|
yhigh = ydata.max() * 1.02
|
||||||
|
std = np.std(ydata)
|
||||||
|
i.setLimits(yMin=ylow, yMax=yhigh, minYRange=std)
|
||||||
|
i.setYRange(ylow, yhigh)
|
||||||
|
|
||||||
def wheelEvent(self, ev, axis=None):
|
def wheelEvent(self, ev, axis=None):
|
||||||
|
"""Override "center-point" location for scrolling.
|
||||||
|
|
||||||
|
This is an override of the ``ViewBox`` method simply changing
|
||||||
|
the center of the zoom to be the y-axis.
|
||||||
|
|
||||||
|
TODO: PR a method into ``pyqtgraph`` to make this configurable
|
||||||
|
"""
|
||||||
|
|
||||||
if axis in (0, 1):
|
if axis in (0, 1):
|
||||||
mask = [False, False]
|
mask = [False, False]
|
||||||
mask[axis] = self.state['mouseEnabled'][axis]
|
mask[axis] = self.state['mouseEnabled'][axis]
|
||||||
|
@ -307,11 +377,11 @@ class ChartPlotWidget(pg.PlotWidget):
|
||||||
sig_mouse_enter = QtCore.Signal(object)
|
sig_mouse_enter = QtCore.Signal(object)
|
||||||
|
|
||||||
def enterEvent(self, ev): # noqa
|
def enterEvent(self, ev): # noqa
|
||||||
pg.PlotWidget.enterEvent(self, ev)
|
# 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)
|
# 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)
|
||||||
|
|
||||||
|
@ -562,16 +632,19 @@ def _configure_chart(
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Configure a chart with common settings.
|
"""Configure a chart with common settings.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# show only right side axes
|
# show only right side axes
|
||||||
chart.hideAxis('left')
|
chart.hideAxis('left')
|
||||||
chart.showAxis('right')
|
chart.showAxis('right')
|
||||||
|
|
||||||
|
# highest = Quotes.high.max() * 1.02
|
||||||
|
# lowest = Quotes.low.min() * 0.98
|
||||||
|
|
||||||
# set panning limits
|
# set panning limits
|
||||||
chart.setLimits(
|
chart.setLimits(
|
||||||
xMin=Quotes[0].id,
|
xMin=Quotes[0].id,
|
||||||
xMax=Quotes[-1].id,
|
xMax=Quotes[-1].id,
|
||||||
minXRange=60,
|
minXRange=40,
|
||||||
|
# maxYRange=highest-lowest,
|
||||||
yMin=Quotes.low.min() * 0.98,
|
yMin=Quotes.low.min() * 0.98,
|
||||||
yMax=Quotes.high.max() * 1.02,
|
yMax=Quotes.high.max() * 1.02,
|
||||||
)
|
)
|
||||||
|
@ -584,8 +657,7 @@ def _configure_chart(
|
||||||
|
|
||||||
def _configure_quotes_chart(
|
def _configure_quotes_chart(
|
||||||
chart: ChartPlotWidget,
|
chart: ChartPlotWidget,
|
||||||
style: ChartType,
|
style: ChartType = ChartType.BAR,
|
||||||
update_yrange_limits: Callable,
|
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Update and format a chart with quotes data.
|
"""Update and format a chart with quotes data.
|
||||||
"""
|
"""
|
||||||
|
@ -596,19 +668,15 @@ def _configure_quotes_chart(
|
||||||
# be drawn on next render cycle
|
# be drawn on next render cycle
|
||||||
chart.addItem(_get_chart_points(style))
|
chart.addItem(_get_chart_points(style))
|
||||||
|
|
||||||
# assign callback for rescaling y-axis automatically
|
|
||||||
# based on y-range contents
|
|
||||||
|
|
||||||
# TODO: this can likely be ported to built-in: .enableAutoRange()
|
|
||||||
# but needs testing
|
|
||||||
chart.sigXRangeChanged.connect(update_yrange_limits)
|
|
||||||
|
|
||||||
|
|
||||||
def _configure_ind_charts(
|
def _configure_ind_charts(
|
||||||
indicators: List[Tuple[ChartPlotWidget, np.ndarray]],
|
indicators: List[Tuple[ChartPlotWidget, np.ndarray]],
|
||||||
xlink_to_chart: ChartPlotWidget,
|
xlink_to_chart: ChartPlotWidget,
|
||||||
) -> None:
|
) -> None:
|
||||||
for ind_chart, d in indicators:
|
for ind_chart, d in indicators:
|
||||||
|
# link chart x-axis to main quotes chart
|
||||||
|
ind_chart.setXLink(xlink_to_chart)
|
||||||
|
|
||||||
# default config
|
# default config
|
||||||
_configure_chart(ind_chart)
|
_configure_chart(ind_chart)
|
||||||
|
|
||||||
|
@ -618,11 +686,8 @@ def _configure_ind_charts(
|
||||||
# XXX: never do this lol
|
# XXX: never do this lol
|
||||||
# ind.setAspectLocked(1)
|
# ind.setAspectLocked(1)
|
||||||
|
|
||||||
# link chart x-axis to main quotes chart
|
|
||||||
ind_chart.setXLink(xlink_to_chart)
|
|
||||||
|
|
||||||
|
class SplitterChart(QtGui.QWidget):
|
||||||
class QuotesChart(QtGui.QWidget):
|
|
||||||
|
|
||||||
long_pen = pg.mkPen('#006000')
|
long_pen = pg.mkPen('#006000')
|
||||||
long_brush = pg.mkBrush('#00ff00')
|
long_brush = pg.mkBrush('#00ff00')
|
||||||
|
@ -634,7 +699,6 @@ class QuotesChart(QtGui.QWidget):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.signals_visible = False
|
self.signals_visible = False
|
||||||
self.style = ChartType.BAR
|
|
||||||
self.indicators = []
|
self.indicators = []
|
||||||
|
|
||||||
self.xaxis = FromTimeFieldDateAxis(orientation='bottom')
|
self.xaxis = FromTimeFieldDateAxis(orientation='bottom')
|
||||||
|
@ -677,44 +741,23 @@ class QuotesChart(QtGui.QWidget):
|
||||||
self.signals_visible = False
|
self.signals_visible = False
|
||||||
|
|
||||||
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.2 / len(self.indicators))
|
||||||
sizes = [int(self.height() * 0.7)]
|
sizes = [int(self.height() * 0.8)]
|
||||||
sizes.extend([min_h_ind] * len(self.indicators))
|
sizes.extend([min_h_ind] * len(self.indicators))
|
||||||
self.splitter.setSizes(sizes) # , int(self.height()*0.2)
|
self.splitter.setSizes(sizes) # , int(self.height()*0.2)
|
||||||
|
|
||||||
def _update_yrange_limits(self):
|
|
||||||
"""Callback for each y-range update.
|
|
||||||
"""
|
|
||||||
vr = self.chart.viewRect()
|
|
||||||
lbar, rbar = int(vr.left()), int(vr.right())
|
|
||||||
|
|
||||||
if self.signals_visible:
|
|
||||||
self._show_text_signals(lbar, rbar)
|
|
||||||
|
|
||||||
bars = Quotes[lbar:rbar]
|
|
||||||
ylow = bars.low.min() * 0.98
|
|
||||||
yhigh = bars.high.max() * 1.02
|
|
||||||
|
|
||||||
std = np.std(bars.close)
|
|
||||||
self.chart.setLimits(yMin=ylow, yMax=yhigh, minYRange=std)
|
|
||||||
self.chart.setYRange(ylow, yhigh)
|
|
||||||
for i, d in self.indicators:
|
|
||||||
# ydata = i.plotItem.items[0].getData()[1]
|
|
||||||
ydata = d[lbar:rbar]
|
|
||||||
ylow = ydata.min() * 0.98
|
|
||||||
yhigh = ydata.max() * 1.02
|
|
||||||
std = np.std(ydata)
|
|
||||||
i.setLimits(yMin=ylow, yMax=yhigh, minYRange=std)
|
|
||||||
i.setYRange(ylow, yhigh)
|
|
||||||
|
|
||||||
def plot(self, symbol):
|
def plot(self, symbol):
|
||||||
self.digits = symbol.digits
|
self.digits = symbol.digits
|
||||||
self.chart = ChartPlotWidget(
|
self.chart = ChartPlotWidget(
|
||||||
parent=self.splitter,
|
parent=self.splitter,
|
||||||
axisItems={'bottom': self.xaxis, 'right': PriceAxis()},
|
axisItems={'bottom': self.xaxis, 'right': PriceAxis()},
|
||||||
viewBox=ScrollFromRightView,
|
viewBox=ChartView,
|
||||||
# enableMenu=False,
|
# enableMenu=False,
|
||||||
)
|
)
|
||||||
|
# TODO: ``pyqtgraph`` doesn't pass through a parent to the
|
||||||
|
# ``PlotItem`` by default; maybe we should PR this in?
|
||||||
|
self.chart.plotItem.parent = self
|
||||||
|
|
||||||
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)
|
||||||
|
|
||||||
|
@ -726,8 +769,10 @@ 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,
|
viewBox=ChartView,
|
||||||
)
|
)
|
||||||
|
ind.plotItem.parent = self
|
||||||
|
|
||||||
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)
|
||||||
# self.splitter.addWidget(ind)
|
# self.splitter.addWidget(ind)
|
||||||
|
@ -735,8 +780,6 @@ class QuotesChart(QtGui.QWidget):
|
||||||
|
|
||||||
_configure_quotes_chart(
|
_configure_quotes_chart(
|
||||||
self.chart,
|
self.chart,
|
||||||
self.style,
|
|
||||||
self._update_yrange_limits
|
|
||||||
)
|
)
|
||||||
_configure_ind_charts(
|
_configure_ind_charts(
|
||||||
self.indicators,
|
self.indicators,
|
||||||
|
|
Loading…
Reference in New Issue