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
parent
0131160896
commit
9e18afe0d7
|
@ -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
|
||||||
|
|
Loading…
Reference in New Issue