Add `ChartPlotWidget.maxmin()` to calc in-view hi/lo y-values
As part of factoring `._set_yrange()` into the lower level view box, move the y-range calculations into a new method. These calcs should eventually be completely separate (as they are for the real-time version in the graphics display update loop) and likely part of some kind of graphics-related lower level management API. Draft such an API as an `ArrayScene` (commented for now) as a sketch toward factoring array tracking **out of** the chart widget. Drop the `'ohlc'` array name and instead always use whatever `.name` was assigned to the chart widget to lookup its "main" / source data array for now. Enable auto-yranging on overlayed plotitems by enabling on its viewbox and, for now, assign an ad-hoc `._maxmin()` since the widget version from this commit has no easy way to know which internal array to use. If an FSP (`dolla_vlm` in this case) is overlayed on an existing chart without also having a full widget (which it doesn't in this case since we're using an overlayed `PlotItem` instead of a full `ChartPlotWidget`) we need some way to define the `.maxmin()` for the overlayed data/graphics. This likely means the `.maxmin()` will eventually get factored into wtv lowlevel `ArrayScene` API mentioned above.plotitem_overlays
parent
fbb765e1d8
commit
ced310c194
|
@ -19,6 +19,8 @@ High level chart-widget apis.
|
|||
|
||||
'''
|
||||
from __future__ import annotations
|
||||
from functools import partial
|
||||
from dataclasses import dataclass
|
||||
from typing import Optional
|
||||
|
||||
from PyQt5 import QtCore, QtWidgets
|
||||
|
@ -29,10 +31,10 @@ from PyQt5.QtWidgets import (
|
|||
QHBoxLayout,
|
||||
QVBoxLayout,
|
||||
QSplitter,
|
||||
# QSizePolicy,
|
||||
)
|
||||
import numpy as np
|
||||
import pyqtgraph as pg
|
||||
from pyqtgraph.widgets._plotitemoverlay import PlotItemOverlay
|
||||
import trio
|
||||
|
||||
from ._axes import (
|
||||
|
@ -484,7 +486,7 @@ class LinkedSplits(QWidget):
|
|||
# presuming we only want it at the true bottom of all charts.
|
||||
# XXX: uses new api from our ``pyqtgraph`` fork.
|
||||
# https://github.com/pikers/pyqtgraph/tree/plotitemoverlay_onto_pg_master
|
||||
axis = self.xaxis_chart.removeAxis('bottom', unlink=False)
|
||||
_ = self.xaxis_chart.removeAxis('bottom', unlink=False)
|
||||
assert 'bottom' not in self.xaxis_chart.plotItem.axes
|
||||
self.xaxis_chart = cpw
|
||||
|
||||
|
@ -608,6 +610,18 @@ class LinkedSplits(QWidget):
|
|||
cpw.sidepane.setMinimumWidth(sp_w)
|
||||
cpw.sidepane.setMaximumWidth(sp_w)
|
||||
|
||||
# import pydantic
|
||||
|
||||
# class ArrayScene(pydantic.BaseModel):
|
||||
# '''
|
||||
# Data-AGGRegate: high level API onto multiple (categorized)
|
||||
# ``ShmArray``s with high level processing routines mostly for
|
||||
# graphics summary and display.
|
||||
|
||||
# '''
|
||||
# arrays: dict[str, np.ndarray] = {}
|
||||
# graphics: dict[str, pg.GraphicsObject] = {}
|
||||
|
||||
|
||||
class ChartPlotWidget(pg.PlotWidget):
|
||||
'''
|
||||
|
@ -683,9 +697,12 @@ class ChartPlotWidget(pg.PlotWidget):
|
|||
# (see our custom view mode in `._interactions.py`)
|
||||
cv.chart = self
|
||||
|
||||
# ensure internal pi matches
|
||||
assert self.cv is self.plotItem.vb
|
||||
|
||||
self.useOpenGL(use_open_gl)
|
||||
self.name = name
|
||||
self.data_key = data_key
|
||||
self.data_key = data_key or name
|
||||
|
||||
# scene-local placeholder for book graphics
|
||||
# sizing to avoid overlap with data contents
|
||||
|
@ -694,9 +711,10 @@ class ChartPlotWidget(pg.PlotWidget):
|
|||
# self.setViewportMargins(0, 0, 0, 0)
|
||||
# self._ohlc = array # readonly view of ohlc data
|
||||
|
||||
# TODO: move to Aggr above XD
|
||||
# readonly view of data arrays
|
||||
self._arrays = {
|
||||
'ohlc': array,
|
||||
self.data_key: array,
|
||||
}
|
||||
self._graphics = {} # registry of underlying graphics
|
||||
# registry of overlay curve names
|
||||
|
@ -707,7 +725,6 @@ class ChartPlotWidget(pg.PlotWidget):
|
|||
self._labels = {} # registry of underlying graphics
|
||||
self._ysticks = {} # registry of underlying graphics
|
||||
|
||||
self._vb = self.plotItem.vb
|
||||
self._static_yrange = static_yrange # for "known y-range style"
|
||||
self._view_mode: str = 'follow'
|
||||
|
||||
|
@ -720,14 +737,8 @@ class ChartPlotWidget(pg.PlotWidget):
|
|||
self.showGrid(x=False, y=True, alpha=0.3)
|
||||
|
||||
self.default_view()
|
||||
self.cv.enable_auto_yrange()
|
||||
|
||||
# Assign callback for rescaling y-axis automatically
|
||||
# based on data contents and ``ViewBox`` state.
|
||||
self.sigXRangeChanged.connect(self._set_yrange)
|
||||
self._vb.sigRangeChangedManually.connect(self._set_yrange) # mouse wheel doesn't emit XRangeChanged
|
||||
self._vb.sigResized.connect(self._set_yrange) # splitter(s) resizing
|
||||
|
||||
from pyqtgraph.widgets._plotitemoverlay import PlotItemOverlay
|
||||
self.overlay: PlotItemOverlay = PlotItemOverlay(self.plotItem)
|
||||
|
||||
def resume_all_feeds(self):
|
||||
|
@ -740,16 +751,16 @@ class ChartPlotWidget(pg.PlotWidget):
|
|||
|
||||
@property
|
||||
def view(self) -> ChartView:
|
||||
return self._vb
|
||||
return self.plotItem.vb
|
||||
|
||||
def focus(self) -> None:
|
||||
self._vb.setFocus()
|
||||
self.view.setFocus()
|
||||
|
||||
def last_bar_in_view(self) -> int:
|
||||
self._arrays['ohlc'][-1]['index']
|
||||
self._arrays[self.name][-1]['index']
|
||||
|
||||
def is_valid_index(self, index: int) -> bool:
|
||||
return index >= 0 and index < self._arrays['ohlc'][-1]['index']
|
||||
return index >= 0 and index < self._arrays[self.name][-1]['index']
|
||||
|
||||
def _set_xlimits(
|
||||
self,
|
||||
|
@ -773,7 +784,7 @@ class ChartPlotWidget(pg.PlotWidget):
|
|||
"""Return a range tuple for the bars present in view.
|
||||
"""
|
||||
l, r = self.view_range()
|
||||
array = self._arrays['ohlc']
|
||||
array = self._arrays[self.name]
|
||||
lbar = max(l, array[0]['index'])
|
||||
rbar = min(r, array[-1]['index'])
|
||||
return l, lbar, rbar, r
|
||||
|
@ -785,7 +796,7 @@ class ChartPlotWidget(pg.PlotWidget):
|
|||
"""Set the view box to the "default" startup view of the scene.
|
||||
|
||||
"""
|
||||
xlast = self._arrays['ohlc'][index]['index']
|
||||
xlast = self._arrays[self.name][index]['index']
|
||||
begin = xlast - _bars_to_left_in_follow_mode
|
||||
end = xlast + _bars_from_right_in_follow_mode
|
||||
|
||||
|
@ -793,12 +804,13 @@ class ChartPlotWidget(pg.PlotWidget):
|
|||
if self._static_yrange == 'axis':
|
||||
self._static_yrange = None
|
||||
|
||||
self.plotItem.vb.setXRange(
|
||||
view = self.view
|
||||
view.setXRange(
|
||||
min=begin,
|
||||
max=end,
|
||||
padding=0,
|
||||
)
|
||||
self._set_yrange()
|
||||
view._set_yrange()
|
||||
|
||||
def increment_view(
|
||||
self,
|
||||
|
@ -809,7 +821,7 @@ class ChartPlotWidget(pg.PlotWidget):
|
|||
|
||||
"""
|
||||
l, r = self.view_range()
|
||||
self._vb.setXRange(
|
||||
self.view.setXRange(
|
||||
min=l + 1,
|
||||
max=r + 1,
|
||||
|
||||
|
@ -908,22 +920,31 @@ class ChartPlotWidget(pg.PlotWidget):
|
|||
|
||||
# register curve graphics and backing array for name
|
||||
self._graphics[name] = curve
|
||||
self._arrays[data_key or name] = data
|
||||
self._arrays[data_key] = data
|
||||
|
||||
if overlay:
|
||||
anchor_at = ('bottom', 'left')
|
||||
# anchor_at = ('bottom', 'left')
|
||||
self._overlays[name] = None
|
||||
|
||||
if separate_axes:
|
||||
|
||||
# Custom viewbox impl
|
||||
cv = self.mk_vb(name)
|
||||
cv.chart = self
|
||||
# cv.enableAutoRange(axis=1)
|
||||
|
||||
xaxis = DynamicDateAxis(
|
||||
orientation='bottom',
|
||||
linkedsplits=self.linked,
|
||||
)
|
||||
def maxmin():
|
||||
return self.maxmin(name=data_key)
|
||||
|
||||
# ensure view maxmin is computed from correct array
|
||||
# cv._maxmin = partial(self.maxmin, name=data_key)
|
||||
|
||||
cv._maxmin = maxmin
|
||||
|
||||
cv.chart = self
|
||||
|
||||
# xaxis = DynamicDateAxis(
|
||||
# orientation='bottom',
|
||||
# linkedsplits=self.linked,
|
||||
# )
|
||||
yaxis = PriceAxis(
|
||||
orientation='right',
|
||||
linkedsplits=self.linked,
|
||||
|
@ -950,10 +971,11 @@ class ChartPlotWidget(pg.PlotWidget):
|
|||
# plotite.hideAxis('right')
|
||||
# plotite.hideAxis('bottom')
|
||||
plotitem.addItem(curve)
|
||||
cv.enable_auto_yrange()
|
||||
|
||||
# config
|
||||
# plotitem.enableAutoRange(axis='y')
|
||||
# plotitem.setAutoVisible(y=True)
|
||||
# plotitem.enableAutoRange(axis='y')
|
||||
plotitem.hideButtons()
|
||||
|
||||
self.overlay.add_plotitem(
|
||||
|
@ -971,7 +993,7 @@ class ChartPlotWidget(pg.PlotWidget):
|
|||
# graphics object
|
||||
self.addItem(curve)
|
||||
|
||||
anchor_at = ('top', 'left')
|
||||
# anchor_at = ('top', 'left')
|
||||
|
||||
# TODO: something instead of stickies for overlays
|
||||
# (we need something that avoids clutter on x-axis).
|
||||
|
@ -1018,7 +1040,7 @@ class ChartPlotWidget(pg.PlotWidget):
|
|||
'''Update the named internal graphics from ``array``.
|
||||
|
||||
'''
|
||||
self._arrays['ohlc'] = array
|
||||
self._arrays[self.name] = array
|
||||
graphics = self._graphics[graphics_name]
|
||||
graphics.update_from_array(array, **kwargs)
|
||||
return graphics
|
||||
|
@ -1039,7 +1061,7 @@ class ChartPlotWidget(pg.PlotWidget):
|
|||
data_key = array_key or graphics_name
|
||||
|
||||
if graphics_name not in self._overlays:
|
||||
self._arrays['ohlc'] = array
|
||||
self._arrays[self.name] = array
|
||||
else:
|
||||
self._arrays[data_key] = array
|
||||
|
||||
|
@ -1061,103 +1083,6 @@ class ChartPlotWidget(pg.PlotWidget):
|
|||
|
||||
return curve
|
||||
|
||||
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,
|
||||
|
||||
) -> 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
|
||||
|
||||
if self._static_yrange == 'axis':
|
||||
set_range = False
|
||||
|
||||
elif self._static_yrange is not None:
|
||||
ylow, yhigh = self._static_yrange
|
||||
|
||||
elif yrange is not None:
|
||||
ylow, yhigh = yrange
|
||||
|
||||
else:
|
||||
# Determine max, min y values in viewable x-range from data.
|
||||
# Make sure min bars/datums on screen is adhered.
|
||||
|
||||
l, lbar, rbar, r = bars_range or self.bars_range()
|
||||
|
||||
if autoscale_linked_plots:
|
||||
# avoid recursion by sibling plots
|
||||
linked = self.linked
|
||||
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._set_yrange(
|
||||
bars_range=(l, lbar, rbar, r),
|
||||
autoscale_linked_plots=False,
|
||||
)
|
||||
|
||||
# TODO: logic to check if end of bars in view
|
||||
# extra = view_len - _min_points_to_show
|
||||
# begin = self._arrays['ohlc'][0]['index'] - extra
|
||||
# # end = len(self._arrays['ohlc']) - 1 + extra
|
||||
# end = self._arrays['ohlc'][-1]['index'] - 1 + extra
|
||||
|
||||
# bars_len = rbar - lbar
|
||||
# log.debug(
|
||||
# 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}"
|
||||
# )
|
||||
|
||||
a = self._arrays['ohlc']
|
||||
ifirst = a[0]['index']
|
||||
bars = a[lbar - ifirst:rbar - ifirst + 1]
|
||||
|
||||
if not len(bars):
|
||||
# likely no data loaded yet or extreme scrolling?
|
||||
log.error(f"WTF bars_range = {lbar}:{rbar}")
|
||||
return
|
||||
|
||||
if self.data_key != self.linked.symbol.key:
|
||||
bars = bars[self.data_key]
|
||||
ylow = np.nanmin(bars)
|
||||
yhigh = np.nanmax(bars)
|
||||
# print(f'{(ylow, yhigh)}')
|
||||
else:
|
||||
# just the std ohlc bars
|
||||
ylow = np.nanmin(bars['low'])
|
||||
yhigh = np.nanmax(bars['high'])
|
||||
|
||||
if set_range:
|
||||
# view margins: stay within a % of the "true range"
|
||||
diff = yhigh - ylow
|
||||
ylow = ylow - (diff * range_margin)
|
||||
yhigh = yhigh + (diff * range_margin)
|
||||
|
||||
self.setLimits(
|
||||
yMin=ylow,
|
||||
yMax=yhigh,
|
||||
)
|
||||
self.setYRange(ylow, yhigh)
|
||||
|
||||
# def _label_h(self, yhigh: float, ylow: float) -> float:
|
||||
# # compute contents label "height" in view terms
|
||||
# # to avoid having data "contents" overlap with them
|
||||
|
@ -1210,3 +1135,59 @@ class ChartPlotWidget(pg.PlotWidget):
|
|||
return ohlc['index'][indexes][-1]
|
||||
else:
|
||||
return ohlc['index'][-1]
|
||||
|
||||
def maxmin(
|
||||
self,
|
||||
name: Optional[str] = None,
|
||||
bars_range: Optional[tuple[int, int, int, int]] = None,
|
||||
|
||||
) -> tuple[float, float]:
|
||||
'''
|
||||
Return the max and min y-data values "in view".
|
||||
|
||||
If ``bars_range`` is provided use that range.
|
||||
|
||||
'''
|
||||
l, lbar, rbar, r = bars_range or self.bars_range()
|
||||
# TODO: logic to check if end of bars in view
|
||||
# extra = view_len - _min_points_to_show
|
||||
# begin = self._arrays['ohlc'][0]['index'] - extra
|
||||
# # end = len(self._arrays['ohlc']) - 1 + extra
|
||||
# end = self._arrays['ohlc'][-1]['index'] - 1 + extra
|
||||
|
||||
# bars_len = rbar - lbar
|
||||
# log.debug(
|
||||
# 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}"
|
||||
# )
|
||||
|
||||
a = self._arrays[name or self.name]
|
||||
ifirst = a[0]['index']
|
||||
bars = a[lbar - ifirst:rbar - ifirst + 1]
|
||||
|
||||
if not len(bars):
|
||||
# likely no data loaded yet or extreme scrolling?
|
||||
log.error(f"WTF bars_range = {lbar}:{rbar}")
|
||||
return
|
||||
|
||||
if (
|
||||
self.data_key == self.linked.symbol.key
|
||||
):
|
||||
# ohlc sampled bars hi/lo lookup
|
||||
ylow = np.nanmin(bars['low'])
|
||||
yhigh = np.nanmax(bars['high'])
|
||||
|
||||
else:
|
||||
try:
|
||||
view = bars[name or self.data_key]
|
||||
except:
|
||||
breakpoint()
|
||||
# if self.data_key != 'volume':
|
||||
# else:
|
||||
# view = bars
|
||||
ylow = np.nanmin(view)
|
||||
yhigh = np.nanmax(view)
|
||||
# print(f'{(ylow, yhigh)}')
|
||||
|
||||
return ylow, yhigh
|
||||
|
|
Loading…
Reference in New Issue