Adjust range logic to avoid overlap with labels

By mapping any in view "contents labels" to the range of the
``ViewBox``'s data we can avoid having graphics overlap with labels.
Take this approach instead of specifying a min y-range using the std
and activate the range compute on resize and mouser scrolling.
Also, add y-sticky update for signal plots.
bar_select
Tyler Goodlet 2020-09-09 10:47:44 -04:00
parent fc0a03d597
commit 80f191c57d
1 changed files with 55 additions and 24 deletions

View File

@ -1,7 +1,7 @@
""" """
High level Qt chart widgets. High level Qt chart widgets.
""" """
from typing import Tuple, Dict, Any from typing import Tuple, Dict, Any, Optional
import time import time
from PyQt5 import QtCore, QtGui from PyQt5 import QtCore, QtGui
@ -274,6 +274,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,
**kwargs, **kwargs,
): ):
"""Configure chart display settings. """Configure chart display settings.
@ -282,12 +283,15 @@ class ChartPlotWidget(pg.PlotWidget):
background=hcolor('papas_special'), background=hcolor('papas_special'),
# parent=None, # parent=None,
# plotItem=None, # plotItem=None,
# useOpenGL=True,
**kwargs **kwargs
) )
self._array = array # readonly view of data self._array = array # readonly view of data
self._graphics = {} # registry of underlying graphics self._graphics = {} # registry of underlying graphics
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
# show only right side axes # show only right side axes
self.hideAxis('left') self.hideAxis('left')
@ -301,10 +305,16 @@ class ChartPlotWidget(pg.PlotWidget):
# 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 ohlc contents # 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
vb.sigRangeChangedManually.connect(self._set_yrange)
# for when the splitter(s) are resized
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):
for name, (label, update) in self._labels.items(): for name, (label, update) in self._labels.items():
@ -364,9 +374,10 @@ class ChartPlotWidget(pg.PlotWidget):
def update(index: int) -> None: def update(index: int) -> None:
label.setText( label.setText(
"{name} O:{} H:{} L:{} C:{} V:{}".format( "{name}[{index}] -> O:{} H:{} L:{} C:{} V:{}".format(
*self._array[index].item()[2:], *self._array[index].item()[2:],
name=name, name=name,
index=index,
) )
) )
@ -412,7 +423,7 @@ class ChartPlotWidget(pg.PlotWidget):
def update(index: int) -> None: def update(index: int) -> None:
data = self._array[index] data = self._array[index]
label.setText(f"{name}: {index} {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(index=-1)
@ -427,6 +438,8 @@ class ChartPlotWidget(pg.PlotWidget):
# "only update with new items" on the pg.PlotDataItem # "only update with new items" on the pg.PlotDataItem
curve.update_from_array = curve.setData curve.update_from_array = curve.setData
self._add_sticky(name)
return curve return curve
def _add_sticky( def _add_sticky(
@ -435,7 +448,7 @@ class ChartPlotWidget(pg.PlotWidget):
# retreive: Callable[None, np.ndarray], # retreive: Callable[None, np.ndarray],
) -> YSticky: ) -> YSticky:
# add y-axis "last" value label # add y-axis "last" value label
last = self._ysticks['last'] = YSticky( last = self._ysticks[name] = YSticky(
chart=self, chart=self,
parent=self.getAxis('right'), parent=self.getAxis('right'),
# digits=0, # digits=0,
@ -490,33 +503,43 @@ class ChartPlotWidget(pg.PlotWidget):
# likely no data loaded yet # likely no data loaded yet
log.error(f"WTF bars_range = {lbar}:{rbar}") log.error(f"WTF bars_range = {lbar}:{rbar}")
return return
elif lbar < 0:
breakpoint()
# TODO: should probably just have some kinda attr mark # TODO: should probably just have some kinda attr mark
# that determines this behavior based on array type # that determines this behavior based on array type
try: try:
ylow = bars['low'].min() ylow = bars['low'].min()
yhigh = bars['high'].max() yhigh = bars['high'].max()
std = np.std(bars['close']) # std = np.std(bars['close'])
except IndexError: except IndexError:
# must be non-ohlc array? # must be non-ohlc array?
ylow = bars.min() ylow = bars.min()
yhigh = bars.max() yhigh = bars.max()
std = np.std(bars) # std = np.std(bars)
# view margins: stay within 10% of the "true range" # view margins: stay within 10% of the "true range"
diff = yhigh - ylow diff = yhigh - ylow
ylow = ylow - (diff * 0.1) ylow = ylow - (diff * 0.04)
yhigh = yhigh + (diff * 0.1) yhigh = yhigh + (diff * 0.01)
# compute contents label "height" in view terms
if self._labels:
label = self._labels[self.name][0]
rect = label.itemRect()
tl, br = rect.topLeft(), rect.bottomRight()
vb = self.plotItem.vb
top, bottom = (vb.mapToView(tl).y(), vb.mapToView(br).y())
label_h = top - bottom
# print(f'label height {self.name}: {label_h}')
else:
label_h = 0
chart = self chart = self
chart.setLimits( chart.setLimits(
yMin=ylow, yMin=ylow,
yMax=yhigh, yMax=yhigh + label_h,
minYRange=std # minYRange=std
) )
chart.setYRange(ylow, yhigh) chart.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)
@ -577,7 +600,10 @@ async def add_new_bars(delay_s, linked_charts):
return new_array return new_array
# add new increment/bar # add new increment/bar
start = time.time()
ohlc = price_chart._array = incr_ohlc_array(ohlc) ohlc = price_chart._array = incr_ohlc_array(ohlc)
diff = time.time() - start
print(f'array append took {diff}')
# TODO: generalize this increment logic # TODO: generalize this increment logic
for name, chart in linked_charts.subplots.items(): for name, chart in linked_charts.subplots.items():
@ -660,7 +686,7 @@ async def _async_main(
n.start_soon( n.start_soon(
chart_from_fsp, chart_from_fsp,
linked_charts, linked_charts,
fsp.latency, 'rsi',
sym, sym,
bars, bars,
brokermod, brokermod,
@ -668,8 +694,10 @@ async def _async_main(
) )
# update last price sticky # update last price sticky
last = chart._ysticks['last'] last_price_sticky = chart._ysticks[chart.name]
last.update_from_data(*chart._array[-1][['index', 'close']]) last_price_sticky.update_from_data(
*chart._array[-1][['index', 'close']]
)
# graphics update loop # graphics update loop
@ -711,15 +739,14 @@ async def _async_main(
chart._array, chart._array,
) )
# update sticky(s) # update sticky(s)
last = chart._ysticks['last'] last_price_sticky.update_from_data(
last.update_from_data(
*chart._array[-1][['index', 'close']]) *chart._array[-1][['index', 'close']])
chart._set_yrange() chart._set_yrange()
async def chart_from_fsp( async def chart_from_fsp(
linked_charts, linked_charts,
fsp_func, func_name,
sym, sym,
bars, bars,
brokermod, brokermod,
@ -729,8 +756,6 @@ async def chart_from_fsp(
Pass target entrypoint and historical data. Pass target entrypoint and historical data.
""" """
func_name = fsp_func.__name__
async with tractor.open_nursery() as n: async with tractor.open_nursery() as n:
portal = await n.run_in_actor( portal = await n.run_in_actor(
f'fsp.{func_name}', # name as title of sub-chart f'fsp.{func_name}', # name as title of sub-chart
@ -749,7 +774,7 @@ async def chart_from_fsp(
stream = await portal.result() stream = await portal.result()
# receive processed historical data-array as first message # receive processed historical data-array as first message
history: np.ndarray = (await stream.__anext__()) history = (await stream.__anext__())
# TODO: enforce type checking here # TODO: enforce type checking here
newbars = np.array(history) newbars = np.array(history)
@ -768,9 +793,15 @@ async def chart_from_fsp(
np.full(abs(diff), data[-1], dtype=data.dtype) np.full(abs(diff), data[-1], dtype=data.dtype)
) )
value = chart._array[-1]
last_val_sticky = chart._ysticks[chart.name]
last_val_sticky.update_from_data(-1, value)
# update chart graphics # update chart graphics
async for value in stream: async for value in stream:
chart._array[-1] = value chart._array[-1] = value
last_val_sticky.update_from_data(-1, value)
chart._set_yrange()
chart.update_from_array(chart.name, chart._array) chart.update_from_array(chart.name, chart._array)
chart._set_yrange() chart._set_yrange()