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