WIP `PlotItemOverlay` support to get multi-yaxes

This syncs with a dev branch in our `pyqtgraph` fork:
https://github.com/pyqtgraph/pyqtgraph/pull/2162

The main idea is to get mult-yaxis display fully functional with
multiple view boxes running in a "relay mode" where some focussed view
relays signals to overlaid views which may have independent axes. This
preps us for both displaying independent codomain-set FSP output as well
as so called "aggregate" feeds of multiple fins underlyings on the same
chart (eg. options and futures over top of ETFs and underlying stocks).
The eventual desired UX is to support fast switching of instruments for
order mode trading without requiring entirely separate charts as well as
simple real-time anal of associated instruments.

The first effort here is to display vlm and $_vlm alongside each other
as a built-in FSP subchart.
plotitem_overlays
Tyler Goodlet 2022-01-24 15:06:51 -05:00
parent 0131160896
commit 9e18afe0d7
1 changed files with 149 additions and 81 deletions

View File

@ -322,17 +322,8 @@ class LinkedSplits(QWidget):
self.subplots: dict[tuple[str, ...], ChartPlotWidget] = {}
self.godwidget = godwidget
self.xaxis = DynamicDateAxis(
orientation='bottom',
linkedsplits=self
)
# if _xaxis_at == 'bottom':
# self.xaxis.setStyle(showValues=False)
# self.xaxis.hide()
# else:
# self.xaxis_ind.setStyle(showValues=False)
# self.xaxis.hide()
# placeholder for last appended ``PlotItem``'s bottom axis.
self.xaxis_chart = None
self.splitter = QSplitter(QtCore.Qt.Vertical)
self.splitter.setMidLineWidth(0)
@ -410,7 +401,6 @@ class LinkedSplits(QWidget):
name=symbol.key,
array=array,
# xaxis=self.xaxis,
style=style,
_is_main=True,
@ -420,7 +410,10 @@ class LinkedSplits(QWidget):
self.chart.addItem(self.cursor)
# axis placement
if _xaxis_at == 'bottom':
if (
_xaxis_at == 'bottom' and
'bottom' in self.chart.plotItem.axes
):
self.chart.hideAxis('bottom')
# style?
@ -438,7 +431,6 @@ class LinkedSplits(QWidget):
array: np.ndarray,
array_key: Optional[str] = None,
# xaxis: Optional[DynamicDateAxis] = None,
style: str = 'line',
_is_main: bool = False,
@ -446,31 +438,28 @@ class LinkedSplits(QWidget):
**cpw_kwargs,
) -> 'ChartPlotWidget':
'''Add (sub)plots to chart widget by key.
) -> ChartPlotWidget:
'''
Add (sub)plots to chart widget by key.
'''
if self.chart is None and not _is_main:
raise RuntimeError(
"A main plot must be created first with `.plot_ohlc_main()`")
# source of our custom interactions
cv = ChartView(name)
cv.linkedsplits = self
# use "indicator axis" by default
# TODO: we gotta possibly assign this back
# to the last subplot on removal of some last subplot
xaxis = DynamicDateAxis(
orientation='bottom',
linkedsplits=self
)
if self.xaxis:
self.xaxis.hide()
self.xaxis = xaxis
axes = {
'right': PriceAxis(linkedsplits=self, orientation='right'),
'left': PriceAxis(linkedsplits=self, orientation='left'),
'bottom': xaxis,
}
qframe = ChartnPane(
sidepane=sidepane,
@ -486,15 +475,21 @@ class LinkedSplits(QWidget):
array=array,
parent=qframe,
linkedsplits=self,
axisItems={
'bottom': xaxis,
'right': PriceAxis(linkedsplits=self, orientation='right'),
'left': PriceAxis(linkedsplits=self, orientation='left'),
},
viewBox=cv,
axisItems=axes,
**cpw_kwargs,
)
if self.xaxis_chart:
# 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)
assert 'bottom' not in self.xaxis_chart.plotItem.axes
self.xaxis_chart = cpw
if self.xaxis_chart is None:
self.xaxis_chart = cpw
qframe.chart = cpw
qframe.hbox.addWidget(cpw)
@ -510,17 +505,13 @@ class LinkedSplits(QWidget):
)
cpw.sidepane = sidepane
# give viewbox as reference to chart
# allowing for kb controls and interactions on **this** widget
# (see our custom view mode in `._interactions.py`)
cv.chart = cpw
cpw.plotItem.vb.linkedsplits = self
cpw.setFrameStyle(
QtWidgets.QFrame.StyledPanel
# | QtWidgets.QFrame.Plain
)
# don't show the little "autoscale" A label.
cpw.hideButtons()
# XXX: gives us outline on backside of y-axis
@ -531,15 +522,27 @@ class LinkedSplits(QWidget):
# comes from ;)
cpw.setXLink(self.chart)
# add to cross-hair's known plots
self.cursor.add_plot(cpw)
add_label = False
anchor_at = ('top', 'left')
# draw curve graphics
if style == 'bar':
cpw.draw_ohlc(name, array, array_key=array_key)
graphics, data_key = cpw.draw_ohlc(
name,
array,
array_key=array_key
)
self.cursor.contents_labels.add_label(
cpw,
'ohlc',
anchor_at=('top', 'left'),
update_func=ContentsLabel.update_from_ohlc,
)
elif style == 'line':
cpw.draw_curve(
add_label = True
graphics, data_key = cpw.draw_curve(
name,
array,
array_key=array_key,
@ -547,7 +550,8 @@ class LinkedSplits(QWidget):
)
elif style == 'step':
cpw.draw_curve(
add_label = True
graphics, data_key = cpw.draw_curve(
name,
array,
array_key=array_key,
@ -569,6 +573,22 @@ class LinkedSplits(QWidget):
else:
assert style == 'bar', 'main chart must be OHLC'
# add to cross-hair's known plots
# NOTE: add **AFTER** creating the underlying ``PlotItem``s
# since we require that global (linked charts wide) axes have
# been created!
self.cursor.add_plot(cpw)
if self.cursor and style != 'bar':
self.cursor.add_curve_cursor(cpw, graphics)
if add_label:
self.cursor.contents_labels.add_label(
cpw,
data_key,
anchor_at=anchor_at,
)
self.resize_sidepanes()
return cpw
@ -611,6 +631,10 @@ class ChartPlotWidget(pg.PlotWidget):
# TODO: can take a ``background`` color setting - maybe there's
# a better one?
def mk_vb(self, name: str) -> ChartView:
cv = ChartView(name)
cv.linkedsplits = self.linked
return cv
def __init__(
self,
@ -639,17 +663,28 @@ class ChartPlotWidget(pg.PlotWidget):
self.view_color = view_color
self.pen_color = pen_color
# NOTE: must be set bfore calling ``.mk_vb()``
self.linked = linkedsplits
# source of our custom interactions
self.cv = cv = self.mk_vb(name)
super().__init__(
background=hcolor(view_color),
viewBox=cv,
# parent=None,
# plotItem=None,
# antialias=True,
**kwargs
)
# give viewbox as reference to chart
# allowing for kb controls and interactions on **this** widget
# (see our custom view mode in `._interactions.py`)
cv.chart = self
self.useOpenGL(use_open_gl)
self.name = name
self.data_key = data_key
self.linked = linkedsplits
# scene-local placeholder for book graphics
# sizing to avoid overlap with data contents
@ -687,13 +722,12 @@ class ChartPlotWidget(pg.PlotWidget):
# Assign callback for rescaling y-axis automatically
# based on data contents and ``ViewBox`` state.
# self.sigXRangeChanged.connect(self._set_yrange)
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
# for mouse wheel which doesn't seem to emit XRangeChanged
self._vb.sigRangeChangedManually.connect(self._set_yrange)
# for when the splitter(s) are resized
self._vb.sigResized.connect(self._set_yrange)
from pyqtgraph.widgets._plotitemoverlay import PlotItemOverlay
self.overlay: PlotItemOverlay = PlotItemOverlay(self.plotItem)
def resume_all_feeds(self):
for feed in self._feeds.values():
@ -791,11 +825,11 @@ class ChartPlotWidget(pg.PlotWidget):
array_key: Optional[str] = None,
) -> pg.GraphicsObject:
"""
) -> (pg.GraphicsObject, str):
'''
Draw OHLC datums to chart.
"""
'''
graphics = BarItems(
self.plotItem,
pen_color=self.pen_color
@ -810,17 +844,9 @@ class ChartPlotWidget(pg.PlotWidget):
data_key = array_key or name
self._graphics[data_key] = graphics
self.linked.cursor.contents_labels.add_label(
self,
'ohlc',
anchor_at=('top', 'left'),
update_func=ContentsLabel.update_from_ohlc,
)
self._add_sticky(name, bg_color='davies')
return graphics
return graphics, data_key
def draw_curve(
self,
@ -830,16 +856,18 @@ class ChartPlotWidget(pg.PlotWidget):
array_key: Optional[str] = None,
overlay: bool = False,
separate_axes: bool = True,
color: Optional[str] = None,
add_label: bool = True,
**pdi_kwargs,
) -> pg.PlotDataItem:
"""Draw a "curve" (line plot graphics) for the provided data in
) -> (pg.PlotDataItem, str):
'''
Draw a "curve" (line plot graphics) for the provided data in
the input array ``data``.
"""
'''
color = color or self.pen_color or 'default_light'
pdi_kwargs.update({
'color': color
@ -847,10 +875,6 @@ class ChartPlotWidget(pg.PlotWidget):
data_key = array_key or name
# pg internals for reference.
# curve = pg.PlotDataItem(
# curve = pg.PlotCurveItem(
# yah, we wrote our own B)
curve = FastAppendCurve(
y=data[data_key],
@ -881,8 +905,6 @@ class ChartPlotWidget(pg.PlotWidget):
# and is disastrous for performance.
# curve.setCacheMode(QtWidgets.QGraphicsItem.ItemCoordinateCache)
self.addItem(curve)
# register curve graphics and backing array for name
self._graphics[name] = curve
self._arrays[data_key or name] = data
@ -891,24 +913,69 @@ class ChartPlotWidget(pg.PlotWidget):
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,
)
yaxis = PriceAxis(
orientation='right',
linkedsplits=self.linked,
)
plotitem = pg.PlotItem(
parent=self.plotItem,
name=name,
enableMenu=False,
viewBox=cv,
axisItems={
# 'bottom': xaxis,
'right': yaxis,
},
)
# plotitem.setAxisItems(
# add_to_layout=False,
# axisItems={
# 'bottom': xaxis,
# 'right': yaxis,
# },
# )
# plotite.hideAxis('right')
# plotite.hideAxis('bottom')
plotitem.addItem(curve)
# config
plotitem.enableAutoRange(axis='y')
plotitem.setAutoVisible(y=True)
plotitem.hideButtons()
self.overlay.add_plotitem(
plotitem,
# only link x-axes,
link_axes=(0,),
)
else:
# this intnernally calls `PlotItem.addItem()` on the
# graphics object
self.addItem(curve)
else:
# this intnernally calls `PlotItem.addItem()` on the
# graphics object
self.addItem(curve)
anchor_at = ('top', 'left')
# TODO: something instead of stickies for overlays
# (we need something that avoids clutter on x-axis).
self._add_sticky(name, bg_color=color)
if self.linked.cursor:
self.linked.cursor.add_curve_cursor(self, curve)
if add_label:
self.linked.cursor.contents_labels.add_label(
self,
data_key or name,
anchor_at=anchor_at
)
return curve
return curve, data_key
# TODO: make this a ctx mngr
def _add_sticky(
@ -1005,7 +1072,8 @@ class ChartPlotWidget(pg.PlotWidget):
autoscale_linked_plots: bool = True,
) -> None:
'''Set the viewable y-range based on embedded data.
'''
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