Add auto-yrange handler to our `ChartView`

Calculations for auto-yaxis ranging are both signalled and drawn by our
`ViewBox` so we might as well factor this handler down from the chart
widget into the view type. This makes it much easier (and clearer) that
`PlotItem` and other lower level overlayed `GraphicsObject`s can utilize
*size-to-data* style view modes easily without widget-level coupling.

Further changes,
- support a `._maxmin()` internal callable (temporarily) for allowing
  a viewed graphics object to define it's own y-range max/min calc.
- add `._static_range` var (though usage hasn't been moved from the
  chart plot widget yet
- drop y-axis click-drag zoom instead reverting back to default viewbox
  behaviour with wheel-zoom and click-drag-pan on the axis.
plotitem_overlays
Tyler Goodlet 2022-01-08 17:52:16 -05:00
parent e66b3792bb
commit 80d16886cb
1 changed files with 211 additions and 60 deletions

View File

@ -156,6 +156,7 @@ async def handle_viewmode_kb_inputs(
# View modes # View modes
if key == Qt.Key_R: if key == Qt.Key_R:
# TODO: set this for all subplots
# edge triggered default view activation # edge triggered default view activation
view.chart.default_view() view.chart.default_view()
@ -349,6 +350,7 @@ class ChartView(ViewBox):
name: str, name: str,
parent: pg.PlotItem = None, parent: pg.PlotItem = None,
static_yrange: Optional[tuple[float, float]] = None,
**kwargs, **kwargs,
): ):
@ -361,8 +363,15 @@ class ChartView(ViewBox):
**kwargs **kwargs
) )
# for "known y-range style"
self._static_yrange = static_yrange
self._maxmin = None
# disable vertical scrolling # disable vertical scrolling
self.setMouseEnabled(x=True, y=False) self.setMouseEnabled(
x=True,
y=True,
)
self.linkedsplits = None self.linkedsplits = None
self._chart: 'ChartPlotWidget' = None # noqa self._chart: 'ChartPlotWidget' = None # noqa
@ -409,6 +418,8 @@ class ChartView(ViewBox):
def chart(self, chart: 'ChartPlotWidget') -> None: # type: ignore # noqa def chart(self, chart: 'ChartPlotWidget') -> None: # type: ignore # noqa
self._chart = chart self._chart = chart
self.select_box.chart = chart self.select_box.chart = chart
if self._maxmin is None:
self._maxmin = chart.maxmin
def wheelEvent( def wheelEvent(
self, self,
@ -440,7 +451,7 @@ class ChartView(ViewBox):
log.debug("Max zoom bruh...") log.debug("Max zoom bruh...")
return return
if ev.delta() < 0 and vl >= len(chart._arrays['ohlc']) + 666: if ev.delta() < 0 and vl >= len(chart._arrays[chart.name]) + 666:
log.debug("Min zoom bruh...") log.debug("Min zoom bruh...")
return return
@ -448,6 +459,27 @@ class ChartView(ViewBox):
s = 1.015 ** (ev.delta() * -1 / 20) # self.state['wheelScaleFactor']) s = 1.015 ** (ev.delta() * -1 / 20) # self.state['wheelScaleFactor'])
s = [(None if m is False else s) for m in mask] s = [(None if m is False else s) for m in mask]
if (
# zoom happened on axis
axis == 1
# if already in axis zoom mode then keep it
or self.chart._static_yrange == 'axis'
):
self.chart._static_yrange = 'axis'
self.setLimits(yMin=None, yMax=None)
# print(scale_y)
# pos = ev.pos()
# lastPos = ev.lastPos()
# dif = pos - lastPos
# dif = dif * -1
center = Point(fn.invertQTransform(self.childGroup.transform()).map(ev.pos()))
# scale_y = 1.3 ** (center.y() * -1 / 20)
self.scaleBy(s, center)
else:
# center = pg.Point( # center = pg.Point(
# fn.invertQTransform(self.childGroup.transform()).map(ev.pos()) # fn.invertQTransform(self.childGroup.transform()).map(ev.pos())
# ) # )
@ -472,7 +504,7 @@ class ChartView(ViewBox):
end_of_l1 = pg.Point( end_of_l1 = pg.Point(
round( round(
chart._vb.mapToView( chart.cv.mapToView(
pg.Point(r_axis_x - chart._max_l1_line_len) pg.Point(r_axis_x - chart._max_l1_line_len)
# QPointF(chart._max_l1_line_len, 0) # QPointF(chart._max_l1_line_len, 0)
).x() ).x()
@ -480,7 +512,6 @@ class ChartView(ViewBox):
) # .x() ) # .x()
# self.state['viewRange'][0][1] = end_of_l1 # self.state['viewRange'][0][1] = end_of_l1
# focal = pg.Point((last_bar.x() + end_of_l1)/2) # focal = pg.Point((last_bar.x() + end_of_l1)/2)
focal = min( focal = min(
@ -500,15 +531,17 @@ class ChartView(ViewBox):
ev, ev,
axis: Optional[int] = None, axis: Optional[int] = None,
relayed_from: ChartView = None, relayed_from: ChartView = None,
) -> None: ) -> None:
# if axis is specified, event will only affect that axis.
button = ev.button()
pos = ev.pos() pos = ev.pos()
lastPos = ev.lastPos() lastPos = ev.lastPos()
dif = pos - lastPos dif = pos - lastPos
dif = dif * -1 dif = dif * -1
# NOTE: if axis is specified, event will only affect that axis.
button = ev.button()
# Ignore axes if mouse is disabled # Ignore axes if mouse is disabled
mouseEnabled = np.array(self.state['mouseEnabled'], dtype=np.float) mouseEnabled = np.array(self.state['mouseEnabled'], dtype=np.float)
mask = mouseEnabled.copy() mask = mouseEnabled.copy()
@ -516,22 +549,26 @@ class ChartView(ViewBox):
mask[1-axis] = 0.0 mask[1-axis] = 0.0
# Scale or translate based on mouse button # Scale or translate based on mouse button
if button & (QtCore.Qt.LeftButton | QtCore.Qt.MidButton): if button & (
QtCore.Qt.LeftButton | QtCore.Qt.MidButton
):
# zoom y-axis ONLY when click-n-drag on it # zoom y-axis ONLY when click-n-drag on it
if axis == 1: # if axis == 1:
# set a static y range special value on chart widget to # # set a static y range special value on chart widget to
# prevent sizing to data in view. # # prevent sizing to data in view.
self.chart._static_yrange = 'axis' # self.chart._static_yrange = 'axis'
scale_y = 1.3 ** (dif.y() * -1 / 20) # scale_y = 1.3 ** (dif.y() * -1 / 20)
self.setLimits(yMin=None, yMax=None) # self.setLimits(yMin=None, yMax=None)
# print(scale_y) # # print(scale_y)
self.scaleBy((0, scale_y)) # self.scaleBy((0, scale_y))
# SELECTION MODE # SELECTION MODE
if self.state['mouseMode'] == ViewBox.RectMode: if (
self.state['mouseMode'] == ViewBox.RectMode
and axis is None
):
# XXX: WHY # XXX: WHY
ev.accept() ev.accept()
@ -542,26 +579,36 @@ class ChartView(ViewBox):
self.select_box.mouse_drag_released(down_pos, pos) self.select_box.mouse_drag_released(down_pos, pos)
# ax = QtCore.QRectF(down_pos, pos) ax = QtCore.QRectF(down_pos, pos)
# ax = self.childGroup.mapRectFromParent(ax) ax = self.childGroup.mapRectFromParent(ax)
# print(ax)
# this is the zoom transform cmd # this is the zoom transform cmd
# self.showAxRect(ax) self.showAxRect(ax)
# axis history tracking
self.axHistoryPointer += 1
self.axHistory = self.axHistory[
:self.axHistoryPointer] + [ax]
# self.axHistoryPointer += 1
# self.axHistory = self.axHistory[
# :self.axHistoryPointer] + [ax]
else: else:
print('drag finish?')
self.select_box.set_pos(down_pos, pos) self.select_box.set_pos(down_pos, pos)
# update shape of scale box # update shape of scale box
# self.updateScaleBox(ev.buttonDownPos(), ev.pos()) # self.updateScaleBox(ev.buttonDownPos(), ev.pos())
self.updateScaleBox(
down_pos,
ev.pos(),
)
# PANNING MODE # PANNING MODE
else: else:
# XXX: WHY # XXX: WHY
ev.accept() ev.accept()
if axis == 1:
self.chart._static_yrange = 'axis'
tr = self.childGroup.transform() tr = self.childGroup.transform()
tr = fn.invertQTransform(tr) tr = fn.invertQTransform(tr)
tr = tr.map(dif*mask) - tr.map(Point(0, 0)) tr = tr.map(dif*mask) - tr.map(Point(0, 0))
@ -615,3 +662,107 @@ class ChartView(ViewBox):
'''This routine is rerouted to an async handler. '''This routine is rerouted to an async handler.
''' '''
pass pass
def _set_yrange(
self,
*,
yrange: Optional[tuple[float, float]] = None,
range_margin: float = 0.06,
bars_range: Optional[tuple[int, int, int, int]] = None,
# flag to prevent triggering sibling charts from the same linked
# set from recursion errors.
autoscale_linked_plots: bool = True,
autoscale_overlays: bool = False,
) -> None:
'''
Set the viewable y-range based on embedded data.
This adds auto-scaling like zoom on the scroll wheel such
that data always fits nicely inside the current view of the
data set.
'''
set_range = True
chart = self._chart
# view has been set in 'axis' mode
# meaning it can be panned and zoomed
# arbitrarily on the y-axis:
# - disable autoranging
# - remove any y range limits
if chart._static_yrange == 'axis':
set_range = False
self.setLimits(yMin=None, yMax=None)
# static y-range has been set likely by
# a specialized FSP configuration.
elif chart._static_yrange is not None:
ylow, yhigh = chart._static_yrange
# range passed in by caller, usually a
# maxmin detection algos inside the
# display loop for re-draw efficiency.
elif yrange is not None:
ylow, yhigh = yrange
# calculate max, min y values in viewable x-range from data.
# Make sure min bars/datums on screen is adhered.
else:
br = bars_range or chart.bars_range()
# TODO: maybe should be a method on the
# chart widget/item?
if autoscale_linked_plots:
# avoid recursion by sibling plots
linked = self.linkedsplits
plots = list(linked.subplots.copy().values())
main = linked.chart
if main:
plots.append(main)
for chart in plots:
if chart and not chart._static_yrange:
chart.cv._set_yrange(
bars_range=br,
autoscale_linked_plots=False,
)
if set_range:
ylow, yhigh = self._maxmin()
# view margins: stay within a % of the "true range"
diff = yhigh - ylow
ylow = ylow - (diff * range_margin)
yhigh = yhigh + (diff * range_margin)
# XXX: this often needs to be unset
# to get different view modes to operate
# correctly!
self.setLimits(
yMin=ylow,
yMax=yhigh,
)
self.setYRange(ylow, yhigh)
def enable_auto_yrange(
vb: ChartView,
) -> None:
'''
Assign callback for rescaling y-axis automatically
based on data contents and ``ViewBox`` state.
'''
vb.sigXRangeChanged.connect(vb._set_yrange)
# mouse wheel doesn't emit XRangeChanged
vb.sigRangeChangedManually.connect(vb._set_yrange)
vb.sigResized.connect(vb._set_yrange) # splitter(s) resizing
def disable_auto_yrange(
self,
) -> None:
self._chart._static_yrange = 'axis'