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
parent
e66b3792bb
commit
80d16886cb
|
@ -156,6 +156,7 @@ async def handle_viewmode_kb_inputs(
|
|||
# View modes
|
||||
if key == Qt.Key_R:
|
||||
|
||||
# TODO: set this for all subplots
|
||||
# edge triggered default view activation
|
||||
view.chart.default_view()
|
||||
|
||||
|
@ -349,6 +350,7 @@ class ChartView(ViewBox):
|
|||
name: str,
|
||||
|
||||
parent: pg.PlotItem = None,
|
||||
static_yrange: Optional[tuple[float, float]] = None,
|
||||
**kwargs,
|
||||
|
||||
):
|
||||
|
@ -361,8 +363,15 @@ class ChartView(ViewBox):
|
|||
**kwargs
|
||||
)
|
||||
|
||||
# for "known y-range style"
|
||||
self._static_yrange = static_yrange
|
||||
self._maxmin = None
|
||||
|
||||
# disable vertical scrolling
|
||||
self.setMouseEnabled(x=True, y=False)
|
||||
self.setMouseEnabled(
|
||||
x=True,
|
||||
y=True,
|
||||
)
|
||||
|
||||
self.linkedsplits = None
|
||||
self._chart: 'ChartPlotWidget' = None # noqa
|
||||
|
@ -409,6 +418,8 @@ class ChartView(ViewBox):
|
|||
def chart(self, chart: 'ChartPlotWidget') -> None: # type: ignore # noqa
|
||||
self._chart = chart
|
||||
self.select_box.chart = chart
|
||||
if self._maxmin is None:
|
||||
self._maxmin = chart.maxmin
|
||||
|
||||
def wheelEvent(
|
||||
self,
|
||||
|
@ -440,7 +451,7 @@ class ChartView(ViewBox):
|
|||
log.debug("Max zoom bruh...")
|
||||
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...")
|
||||
return
|
||||
|
||||
|
@ -448,6 +459,27 @@ class ChartView(ViewBox):
|
|||
s = 1.015 ** (ev.delta() * -1 / 20) # self.state['wheelScaleFactor'])
|
||||
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(
|
||||
# fn.invertQTransform(self.childGroup.transform()).map(ev.pos())
|
||||
# )
|
||||
|
@ -472,7 +504,7 @@ class ChartView(ViewBox):
|
|||
|
||||
end_of_l1 = pg.Point(
|
||||
round(
|
||||
chart._vb.mapToView(
|
||||
chart.cv.mapToView(
|
||||
pg.Point(r_axis_x - chart._max_l1_line_len)
|
||||
# QPointF(chart._max_l1_line_len, 0)
|
||||
).x()
|
||||
|
@ -480,7 +512,6 @@ class ChartView(ViewBox):
|
|||
) # .x()
|
||||
|
||||
# self.state['viewRange'][0][1] = end_of_l1
|
||||
|
||||
# focal = pg.Point((last_bar.x() + end_of_l1)/2)
|
||||
|
||||
focal = min(
|
||||
|
@ -500,15 +531,17 @@ class ChartView(ViewBox):
|
|||
ev,
|
||||
axis: Optional[int] = None,
|
||||
relayed_from: ChartView = None,
|
||||
|
||||
) -> None:
|
||||
# if axis is specified, event will only affect that axis.
|
||||
button = ev.button()
|
||||
|
||||
pos = ev.pos()
|
||||
lastPos = ev.lastPos()
|
||||
dif = pos - lastPos
|
||||
dif = dif * -1
|
||||
|
||||
# NOTE: if axis is specified, event will only affect that axis.
|
||||
button = ev.button()
|
||||
|
||||
# Ignore axes if mouse is disabled
|
||||
mouseEnabled = np.array(self.state['mouseEnabled'], dtype=np.float)
|
||||
mask = mouseEnabled.copy()
|
||||
|
@ -516,22 +549,26 @@ class ChartView(ViewBox):
|
|||
mask[1-axis] = 0.0
|
||||
|
||||
# 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
|
||||
if axis == 1:
|
||||
# set a static y range special value on chart widget to
|
||||
# prevent sizing to data in view.
|
||||
self.chart._static_yrange = 'axis'
|
||||
# if axis == 1:
|
||||
# # set a static y range special value on chart widget to
|
||||
# # prevent sizing to data in view.
|
||||
# self.chart._static_yrange = 'axis'
|
||||
|
||||
scale_y = 1.3 ** (dif.y() * -1 / 20)
|
||||
self.setLimits(yMin=None, yMax=None)
|
||||
# scale_y = 1.3 ** (dif.y() * -1 / 20)
|
||||
# self.setLimits(yMin=None, yMax=None)
|
||||
|
||||
# print(scale_y)
|
||||
self.scaleBy((0, scale_y))
|
||||
# # print(scale_y)
|
||||
# self.scaleBy((0, scale_y))
|
||||
|
||||
# SELECTION MODE
|
||||
if self.state['mouseMode'] == ViewBox.RectMode:
|
||||
if (
|
||||
self.state['mouseMode'] == ViewBox.RectMode
|
||||
and axis is None
|
||||
):
|
||||
# XXX: WHY
|
||||
ev.accept()
|
||||
|
||||
|
@ -542,26 +579,36 @@ class ChartView(ViewBox):
|
|||
|
||||
self.select_box.mouse_drag_released(down_pos, pos)
|
||||
|
||||
# ax = QtCore.QRectF(down_pos, pos)
|
||||
# ax = self.childGroup.mapRectFromParent(ax)
|
||||
# print(ax)
|
||||
ax = QtCore.QRectF(down_pos, pos)
|
||||
ax = self.childGroup.mapRectFromParent(ax)
|
||||
|
||||
# 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:
|
||||
print('drag finish?')
|
||||
self.select_box.set_pos(down_pos, pos)
|
||||
|
||||
# update shape of scale box
|
||||
# self.updateScaleBox(ev.buttonDownPos(), ev.pos())
|
||||
self.updateScaleBox(
|
||||
down_pos,
|
||||
ev.pos(),
|
||||
)
|
||||
|
||||
# PANNING MODE
|
||||
else:
|
||||
# XXX: WHY
|
||||
ev.accept()
|
||||
|
||||
if axis == 1:
|
||||
self.chart._static_yrange = 'axis'
|
||||
|
||||
tr = self.childGroup.transform()
|
||||
tr = fn.invertQTransform(tr)
|
||||
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.
|
||||
'''
|
||||
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'
|
||||
|
|
Loading…
Reference in New Issue