Fix static yrange and last bar double draw issues

bar_select
Tyler Goodlet 2020-10-16 12:18:14 -04:00
parent fc23b2180d
commit c7d5ea6e15
3 changed files with 76 additions and 66 deletions

View File

@ -19,6 +19,7 @@ from ._style import (
_xaxis_at, _min_points_to_show, hcolor, _xaxis_at, _min_points_to_show, hcolor,
CHART_MARGINS, CHART_MARGINS,
_bars_from_right_in_follow_mode, _bars_from_right_in_follow_mode,
_bars_to_left_in_follow_mode,
) )
from ..data._source import Symbol from ..data._source import Symbol
from .. import brokers from .. import brokers
@ -206,6 +207,7 @@ class LinkedSplitCharts(QtGui.QWidget):
xaxis: DynamicDateAxis = None, xaxis: DynamicDateAxis = None,
ohlc: bool = False, ohlc: bool = False,
_is_main: bool = False, _is_main: bool = False,
**cpw_kwargs,
) -> 'ChartPlotWidget': ) -> 'ChartPlotWidget':
"""Add (sub)plots to chart widget by name. """Add (sub)plots to chart widget by name.
@ -226,6 +228,7 @@ class LinkedSplitCharts(QtGui.QWidget):
parent=self.splitter, parent=self.splitter,
axisItems={'bottom': xaxis, 'right': PriceAxis()}, axisItems={'bottom': xaxis, 'right': PriceAxis()},
viewBox=cv, viewBox=cv,
**cpw_kwargs,
) )
# this name will be used to register the primary # this name will be used to register the primary
# graphics curve managed by the subchart # graphics curve managed by the subchart
@ -283,7 +286,7 @@ class ChartPlotWidget(pg.PlotWidget):
self, self,
# the data view we generate graphics from # the data view we generate graphics from
array: np.ndarray, array: np.ndarray,
yrange: Optional[Tuple[float, float]] = None, static_yrange: Optional[Tuple[float, float]] = None,
**kwargs, **kwargs,
): ):
"""Configure chart display settings. """Configure chart display settings.
@ -299,9 +302,8 @@ class ChartPlotWidget(pg.PlotWidget):
self._overlays = {} # registry of overlay curves self._overlays = {} # registry of overlay curves
self._labels = {} # registry of underlying graphics self._labels = {} # registry of underlying graphics
self._ysticks = {} # registry of underlying graphics self._ysticks = {} # registry of underlying graphics
self._yrange = yrange
self._vb = self.plotItem.vb self._vb = self.plotItem.vb
self._static_yrange = None self._static_yrange = static_yrange # for "known y-range style"
# show only right side axes # show only right side axes
self.hideAxis('left') self.hideAxis('left')
@ -310,20 +312,21 @@ class ChartPlotWidget(pg.PlotWidget):
# show background grid # show background grid
self.showGrid(x=True, y=True, alpha=0.4) self.showGrid(x=True, y=True, alpha=0.4)
self.plotItem.vb.setXRange(0, 0) # don't need right?
# self._vb.setXRange(0, 0)
# use cross-hair for cursor # use cross-hair for cursor?
self.setCursor(QtCore.Qt.CrossCursor) # self.setCursor(QtCore.Qt.CrossCursor)
# Assign callback for rescaling y-axis automatically # Assign callback for rescaling y-axis automatically
# based on data contents and ``ViewBox`` state. # based on data contents and ``ViewBox`` state.
self.sigXRangeChanged.connect(self._set_yrange) self.sigXRangeChanged.connect(self._set_yrange)
vb = self._vb
# for mouse wheel which doesn't seem to emit XRangeChanged # for mouse wheel which doesn't seem to emit XRangeChanged
vb.sigRangeChangedManually.connect(self._set_yrange) self._vb.sigRangeChangedManually.connect(self._set_yrange)
# for when the splitter(s) are resized # for when the splitter(s) are resized
vb.sigResized.connect(self._set_yrange) self._vb.sigResized.connect(self._set_yrange)
def _update_contents_label(self, index: int) -> None: def _update_contents_label(self, index: int) -> None:
if index >= 0 and index < len(self._array): if index >= 0 and index < len(self._array):
@ -369,6 +372,7 @@ class ChartPlotWidget(pg.PlotWidget):
# adds all bar/candle graphics objects for each data point in # adds all bar/candle graphics objects for each data point in
# the np array buffer to be drawn on next render cycle # the np array buffer to be drawn on next render cycle
self.addItem(graphics) self.addItem(graphics)
# draw after to allow self.scene() to work... # draw after to allow self.scene() to work...
graphics.draw_from_data(data) graphics.draw_from_data(data)
@ -392,7 +396,7 @@ class ChartPlotWidget(pg.PlotWidget):
) )
self._labels[name] = (label, update) self._labels[name] = (label, update)
self._update_contents_label(index=-1) self._update_contents_label(len(data) - 1)
label.show() label.show()
# set xrange limits # set xrange limits
@ -400,7 +404,7 @@ class ChartPlotWidget(pg.PlotWidget):
# show last 50 points on startup # show last 50 points on startup
self.plotItem.vb.setXRange( self.plotItem.vb.setXRange(
xlast - 50, xlast - _bars_to_left_in_follow_mode,
xlast + _bars_from_right_in_follow_mode xlast + _bars_from_right_in_follow_mode
) )
@ -437,7 +441,7 @@ class ChartPlotWidget(pg.PlotWidget):
# XXX: How to stack labels vertically? # XXX: How to stack labels vertically?
label = pg.LabelItem( label = pg.LabelItem(
justify='left', justify='left',
size='4pt', size='6px',
) )
label.setParentItem(self._vb) label.setParentItem(self._vb)
# label.setParentItem(self.getPlotItem()) # label.setParentItem(self.getPlotItem())
@ -455,14 +459,14 @@ class ChartPlotWidget(pg.PlotWidget):
label.setText(f"{name} -> {data}") label.setText(f"{name} -> {data}")
self._labels[name] = (label, update) self._labels[name] = (label, update)
self._update_contents_label(index=-1) self._update_contents_label(len(data) - 1)
# set a "startup view" # set a "startup view"
xlast = len(data) - 1 xlast = len(data) - 1
# show last 50 points on startup # configure "follow mode" view on startup
self.plotItem.vb.setXRange( self.plotItem.vb.setXRange(
xlast - 50, xlast - _bars_to_left_in_follow_mode,
xlast + _bars_from_right_in_follow_mode xlast + _bars_from_right_in_follow_mode
) )
@ -514,46 +518,47 @@ class ChartPlotWidget(pg.PlotWidget):
that data always fits nicely inside the current view of the that data always fits nicely inside the current view of the
data set. data set.
""" """
l, lbar, rbar, r = self.bars_range()
# figure out x-range in view such that user can scroll "off" the data
# set up to the point where ``_min_points_to_show`` are left.
# if l < lbar or r > rbar:
view_len = r - l
# TODO: logic to check if end of bars in view
extra = view_len - _min_points_to_show
begin = 0 - extra
end = len(self._array) - 1 + extra
# XXX: test code for only rendering lines for the bars in view.
# This turns out to be very very poor perf when scaling out to
# many bars (think > 1k) on screen.
# name = self.name
# bars = self._graphics[self.name]
# bars.draw_lines(
# istart=max(lbar, l), iend=min(rbar, r), just_history=True)
# bars_len = rbar - lbar
# log.trace(
# f"\nl: {l}, lbar: {lbar}, rbar: {rbar}, r: {r}\n"
# f"view_len: {view_len}, bars_len: {bars_len}\n"
# f"begin: {begin}, end: {end}, extra: {extra}"
# )
self._set_xlimits(begin, end)
# yrange # yrange
if self._static_yrange is not None: # if self._static_yrange is not None:
yrange = self._static_yrange # yrange = self._static_yrange
if yrange is not None: if self._static_yrange is not None:
ylow, yhigh = yrange ylow, yhigh = self._static_yrange
self._static_yrange = yrange
else: else: # determine max, min y values in viewable x-range
l, lbar, rbar, r = self.bars_range()
# figure out x-range in view such that user can scroll "off"
# the data set up to the point where ``_min_points_to_show``
# are left.
view_len = r - l
# TODO: logic to check if end of bars in view
extra = view_len - _min_points_to_show
begin = 0 - extra
end = len(self._array) - 1 + extra
# XXX: test code for only rendering lines for the bars in view.
# This turns out to be very very poor perf when scaling out to
# many bars (think > 1k) on screen.
# name = self.name
# bars = self._graphics[self.name]
# bars.draw_lines(
# istart=max(lbar, l), iend=min(rbar, r), just_history=True)
# bars_len = rbar - lbar
# log.trace(
# f"\nl: {l}, lbar: {lbar}, rbar: {rbar}, r: {r}\n"
# f"view_len: {view_len}, bars_len: {bars_len}\n"
# f"begin: {begin}, end: {end}, extra: {extra}"
# )
self._set_xlimits(begin, end)
# TODO: this should be some kind of numpy view api # TODO: this should be some kind of numpy view api
bars = self._array[lbar:rbar] bars = self._array[lbar:rbar]
if not len(bars): if not len(bars):
# likely no data loaded yet # likely no data loaded yet or extreme scrolling?
log.error(f"WTF bars_range = {lbar}:{rbar}") log.error(f"WTF bars_range = {lbar}:{rbar}")
return return
@ -562,19 +567,18 @@ class ChartPlotWidget(pg.PlotWidget):
try: try:
ylow = np.nanmin(bars['low']) ylow = np.nanmin(bars['low'])
yhigh = np.nanmax(bars['high']) yhigh = np.nanmax(bars['high'])
# std = np.std(bars['close'])
except (IndexError, ValueError): except (IndexError, ValueError):
# must be non-ohlc array? # must be non-ohlc array?
ylow = np.nanmin(bars) ylow = np.nanmin(bars)
yhigh = np.nanmax(bars) yhigh = np.nanmax(bars)
# std = np.std(bars)
# view margins: stay within 10% of the "true range" # view margins: stay within a % of the "true range"
diff = yhigh - ylow diff = yhigh - ylow
ylow = ylow - (diff * 0.04) ylow = ylow - (diff * 0.08)
yhigh = yhigh + (diff * 0.01) yhigh = yhigh + (diff * 0.01)
# compute contents label "height" in view terms # compute contents label "height" in view terms
# to avoid having data "contents" overlap with them
if self._labels: if self._labels:
label = self._labels[self.name][0] label = self._labels[self.name][0]
rect = label.itemRect() rect = label.itemRect()
@ -590,13 +594,14 @@ class ChartPlotWidget(pg.PlotWidget):
else: else:
label_h = 0 label_h = 0
chart = self if label_h > yhigh - ylow:
chart.setLimits( label_h = 0
self.setLimits(
yMin=ylow, yMin=ylow,
yMax=yhigh + label_h, yMax=yhigh + label_h,
# minYRange=std
) )
chart.setYRange(ylow, yhigh + label_h) self.setYRange(ylow, yhigh + label_h)
def enterEvent(self, ev): # noqa def enterEvent(self, ev): # noqa
# pg.PlotWidget.enterEvent(self, ev) # pg.PlotWidget.enterEvent(self, ev)
@ -809,20 +814,23 @@ async def chart_from_fsp(
chart = linked_charts.add_plot( chart = linked_charts.add_plot(
name=func_name, name=func_name,
# TODO: enforce type checking here?
array=shm.array, array=shm.array,
# settings passed down to ``ChartPlotWidget``
static_yrange=(0, 100),
) )
# display contents labels asap
chart._update_contents_label(len(shm.array) - 1)
array = shm.array[func_name] array = shm.array[func_name]
value = array[-1] value = array[-1]
last_val_sticky = chart._ysticks[chart.name] last_val_sticky = chart._ysticks[chart.name]
last_val_sticky.update_from_data(-1, value) last_val_sticky.update_from_data(-1, value)
chart.update_from_array(chart.name, array) chart.update_from_array(chart.name, array)
chart._set_yrange(yrange=(0, 100))
chart._shm = shm chart._shm = shm
chart._set_yrange()
# update chart graphics # update chart graphics
async for value in stream: async for value in stream:
@ -830,7 +838,6 @@ async def chart_from_fsp(
value = array[-1] value = array[-1]
last_val_sticky.update_from_data(-1, value) last_val_sticky.update_from_data(-1, value)
chart.update_from_array(chart.name, array) chart.update_from_array(chart.name, array)
# chart._set_yrange()
async def check_for_new_bars(feed, ohlcv, linked_charts): async def check_for_new_bars(feed, ohlcv, linked_charts):

View File

@ -285,7 +285,10 @@ class BarItems(pg.GraphicsObject):
index = len(lines) index = len(lines)
self.lines[:index] = lines self.lines[:index] = lines
self.index = index self.index = index
self.draw_lines(just_history=True, iend=self.index)
# up to last to avoid double draw of last bar
self.draw_lines(just_history=True, iend=self.index - 1)
self.draw_lines(iend=self.index)
# @timeit # @timeit
def draw_lines( def draw_lines(

View File

@ -17,10 +17,10 @@ _i3_rgba = QtGui.QColor.fromRgbF(*[0.14]*3 + [1])
_xaxis_at = 'bottom' _xaxis_at = 'bottom'
# charting config # charting config
CHART_MARGINS = (0, 0, 2, 2)
_min_points_to_show = 3 _min_points_to_show = 3
_bars_from_right_in_follow_mode = 5 _bars_from_right_in_follow_mode = 5
CHART_MARGINS = (0, 0, 2, 2) _bars_to_left_in_follow_mode = 100
_tina_mode = False _tina_mode = False