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
|
# 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,67 +459,89 @@ 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]
|
||||||
|
|
||||||
# center = pg.Point(
|
if (
|
||||||
# fn.invertQTransform(self.childGroup.transform()).map(ev.pos())
|
# zoom happened on axis
|
||||||
# )
|
axis == 1
|
||||||
|
|
||||||
# XXX: scroll "around" the right most element in the view
|
# if already in axis zoom mode then keep it
|
||||||
# which stays "pinned" in place.
|
or self.chart._static_yrange == 'axis'
|
||||||
|
):
|
||||||
|
self.chart._static_yrange = 'axis'
|
||||||
|
self.setLimits(yMin=None, yMax=None)
|
||||||
|
|
||||||
# furthest_right_coord = self.boundingRect().topRight()
|
# 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)
|
||||||
|
|
||||||
# yaxis = pg.Point(
|
else:
|
||||||
# fn.invertQTransform(
|
|
||||||
# self.childGroup.transform()
|
|
||||||
# ).map(furthest_right_coord)
|
|
||||||
# )
|
|
||||||
|
|
||||||
# This seems like the most "intuitive option, a hybrid of
|
# center = pg.Point(
|
||||||
# tws and tv styles
|
# fn.invertQTransform(self.childGroup.transform()).map(ev.pos())
|
||||||
last_bar = pg.Point(int(rbar)) + 1
|
# )
|
||||||
|
|
||||||
ryaxis = chart.getAxis('right')
|
# XXX: scroll "around" the right most element in the view
|
||||||
r_axis_x = ryaxis.pos().x()
|
# which stays "pinned" in place.
|
||||||
|
|
||||||
end_of_l1 = pg.Point(
|
# furthest_right_coord = self.boundingRect().topRight()
|
||||||
round(
|
|
||||||
chart._vb.mapToView(
|
# yaxis = pg.Point(
|
||||||
pg.Point(r_axis_x - chart._max_l1_line_len)
|
# fn.invertQTransform(
|
||||||
# QPointF(chart._max_l1_line_len, 0)
|
# self.childGroup.transform()
|
||||||
).x()
|
# ).map(furthest_right_coord)
|
||||||
|
# )
|
||||||
|
|
||||||
|
# This seems like the most "intuitive option, a hybrid of
|
||||||
|
# tws and tv styles
|
||||||
|
last_bar = pg.Point(int(rbar)) + 1
|
||||||
|
|
||||||
|
ryaxis = chart.getAxis('right')
|
||||||
|
r_axis_x = ryaxis.pos().x()
|
||||||
|
|
||||||
|
end_of_l1 = pg.Point(
|
||||||
|
round(
|
||||||
|
chart.cv.mapToView(
|
||||||
|
pg.Point(r_axis_x - chart._max_l1_line_len)
|
||||||
|
# QPointF(chart._max_l1_line_len, 0)
|
||||||
|
).x()
|
||||||
|
)
|
||||||
|
) # .x()
|
||||||
|
|
||||||
|
# self.state['viewRange'][0][1] = end_of_l1
|
||||||
|
# focal = pg.Point((last_bar.x() + end_of_l1)/2)
|
||||||
|
|
||||||
|
focal = min(
|
||||||
|
last_bar,
|
||||||
|
end_of_l1,
|
||||||
|
key=lambda p: p.x()
|
||||||
)
|
)
|
||||||
) # .x()
|
# focal = pg.Point(last_bar.x() + end_of_l1)
|
||||||
|
|
||||||
# self.state['viewRange'][0][1] = end_of_l1
|
self._resetTarget()
|
||||||
|
self.scaleBy(s, focal)
|
||||||
# focal = pg.Point((last_bar.x() + end_of_l1)/2)
|
self.sigRangeChangedManually.emit(mask)
|
||||||
|
ev.accept()
|
||||||
focal = min(
|
|
||||||
last_bar,
|
|
||||||
end_of_l1,
|
|
||||||
key=lambda p: p.x()
|
|
||||||
)
|
|
||||||
# focal = pg.Point(last_bar.x() + end_of_l1)
|
|
||||||
|
|
||||||
self._resetTarget()
|
|
||||||
self.scaleBy(s, focal)
|
|
||||||
self.sigRangeChangedManually.emit(mask)
|
|
||||||
ev.accept()
|
|
||||||
|
|
||||||
def mouseDragEvent(
|
def mouseDragEvent(
|
||||||
self,
|
self,
|
||||||
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'
|
||||||
|
|
Loading…
Reference in New Issue