From d8ca799504f230bfa0597ad35b3a90a61d3d3cac Mon Sep 17 00:00:00 2001 From: Tyler Goodlet Date: Sun, 14 Jun 2020 15:10:08 -0400 Subject: [PATCH] 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. --- piker/ui/qt/_exec.py | 1 + piker/ui/qt/quantdom/charts.py | 149 +++++++++++++++++++++------------ 2 files changed, 97 insertions(+), 53 deletions(-) diff --git a/piker/ui/qt/_exec.py b/piker/ui/qt/_exec.py index 39aea685..c2eda278 100644 --- a/piker/ui/qt/_exec.py +++ b/piker/ui/qt/_exec.py @@ -89,5 +89,6 @@ def run_qtrio( window.main_widget = main_widget window.setCentralWidget(instance) + # actually render to screen window.show() app.exec_() diff --git a/piker/ui/qt/quantdom/charts.py b/piker/ui/qt/quantdom/charts.py index 9a0486e6..3b81e6ba 100644 --- a/piker/ui/qt/quantdom/charts.py +++ b/piker/ui/qt/quantdom/charts.py @@ -1,7 +1,7 @@ """ Real-time quotes charting components """ -from typing import Callable, List, Tuple +from typing import List, Tuple import numpy as np import pyqtgraph as pg @@ -13,10 +13,10 @@ from .const import ChartType from .portfolio import Order, Portfolio 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') # margins @@ -260,9 +260,79 @@ class YAxisLabel(AxisLabel): 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): + """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): mask = [False, False] mask[axis] = self.state['mouseEnabled'][axis] @@ -307,11 +377,11 @@ class ChartPlotWidget(pg.PlotWidget): sig_mouse_enter = QtCore.Signal(object) def enterEvent(self, ev): # noqa - pg.PlotWidget.enterEvent(self, ev) + # pg.PlotWidget.enterEvent(self, ev) self.sig_mouse_enter.emit(self) def leaveEvent(self, ev): # noqa - pg.PlotWidget.leaveEvent(self, ev) + # pg.PlotWidget.leaveEvent(self, ev) self.sig_mouse_leave.emit(self) self.scene().leaveEvent(ev) @@ -562,16 +632,19 @@ def _configure_chart( ) -> None: """Configure a chart with common settings. """ - # show only right side axes chart.hideAxis('left') chart.showAxis('right') + # highest = Quotes.high.max() * 1.02 + # lowest = Quotes.low.min() * 0.98 + # set panning limits chart.setLimits( xMin=Quotes[0].id, xMax=Quotes[-1].id, - minXRange=60, + minXRange=40, + # maxYRange=highest-lowest, yMin=Quotes.low.min() * 0.98, yMax=Quotes.high.max() * 1.02, ) @@ -584,8 +657,7 @@ def _configure_chart( def _configure_quotes_chart( chart: ChartPlotWidget, - style: ChartType, - update_yrange_limits: Callable, + style: ChartType = ChartType.BAR, ) -> None: """Update and format a chart with quotes data. """ @@ -596,19 +668,15 @@ def _configure_quotes_chart( # be drawn on next render cycle 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( indicators: List[Tuple[ChartPlotWidget, np.ndarray]], xlink_to_chart: ChartPlotWidget, ) -> None: for ind_chart, d in indicators: + # link chart x-axis to main quotes chart + ind_chart.setXLink(xlink_to_chart) + # default config _configure_chart(ind_chart) @@ -618,11 +686,8 @@ def _configure_ind_charts( # XXX: never do this lol # ind.setAspectLocked(1) - # link chart x-axis to main quotes chart - ind_chart.setXLink(xlink_to_chart) - -class QuotesChart(QtGui.QWidget): +class SplitterChart(QtGui.QWidget): long_pen = pg.mkPen('#006000') long_brush = pg.mkBrush('#00ff00') @@ -634,7 +699,6 @@ class QuotesChart(QtGui.QWidget): def __init__(self): super().__init__() self.signals_visible = False - self.style = ChartType.BAR self.indicators = [] self.xaxis = FromTimeFieldDateAxis(orientation='bottom') @@ -677,44 +741,23 @@ class QuotesChart(QtGui.QWidget): self.signals_visible = False def _update_sizes(self): - min_h_ind = int(self.height() * 0.3 / len(self.indicators)) - sizes = [int(self.height() * 0.7)] + min_h_ind = int(self.height() * 0.2 / len(self.indicators)) + sizes = [int(self.height() * 0.8)] sizes.extend([min_h_ind] * len(self.indicators)) 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): self.digits = symbol.digits self.chart = ChartPlotWidget( parent=self.splitter, axisItems={'bottom': self.xaxis, 'right': PriceAxis()}, - viewBox=ScrollFromRightView, + viewBox=ChartView, # 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.setFrameStyle(QtGui.QFrame.StyledPanel | QtGui.QFrame.Plain) @@ -726,8 +769,10 @@ class QuotesChart(QtGui.QWidget): parent=self.splitter, axisItems={'bottom': 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.getPlotItem().setContentsMargins(*CHART_MARGINS) # self.splitter.addWidget(ind) @@ -735,8 +780,6 @@ class QuotesChart(QtGui.QWidget): _configure_quotes_chart( self.chart, - self.style, - self._update_yrange_limits ) _configure_ind_charts( self.indicators,