Massively simplify the cross-hair monstrosity

Stop with all this "main chart" special treatment.
Manage all lines in the same way across all referenced plots.
Add `CrossHair.add_plot()` for adding new plots dynamically.

Just, smh.
bar_select
Tyler Goodlet 2020-07-16 21:54:24 -04:00
parent c56aee6347
commit b4f1ec7960
1 changed files with 77 additions and 112 deletions

View File

@ -23,152 +23,116 @@ from ._axes import YAxisLabel, XAxisLabel
_mouse_rate_limit = 50 _mouse_rate_limit = 50
class CrossHairItem(pg.GraphicsObject): class CrossHair(pg.GraphicsObject):
def __init__(self, parent, indicators=None, digits=0): def __init__(self, parent, digits: int = 0):
super().__init__() super().__init__()
# self.pen = pg.mkPen('#000000') # self.pen = pg.mkPen('#000000')
self.pen = pg.mkPen('#a9a9a9') self.pen = pg.mkPen('#a9a9a9')
self.parent = parent self.parent = parent
self.indicators = {} self.graphics = {}
self.activeIndicator = None self.plots = []
self.xaxis = self.parent.getAxis('bottom') self.active_plot = None
self.yaxis = self.parent.getAxis('right') self.digits = digits
self.vline = self.parent.addLine(x=0, pen=self.pen, movable=False) def add_plot(
self.hline = self.parent.addLine(y=0, pen=self.pen, movable=False) self,
plot: 'ChartPlotWidget', # noqa
self.proxy_moved = pg.SignalProxy( digits: int = 0,
self.parent.scene().sigMouseMoved, ) -> None:
rateLimit=_mouse_rate_limit, # add ``pg.graphicsItems.InfiniteLine``s
slot=self.mouseMoved, # vertical and horizonal lines and a y-axis label
vl = plot.addLine(x=0, pen=self.pen, movable=False)
hl = plot.addLine(y=0, pen=self.pen, movable=False)
yl = YAxisLabel(
parent=plot.getAxis('right'),
digits=digits or self.digits,
opacity=1
) )
self.yaxis_label = YAxisLabel( # TODO: checkout what ``.sigDelayed`` can be used for
parent=self.yaxis, digits=digits, opacity=1 # (emitted once a sufficient delay occurs in mouse movement)
)
indicators = indicators or []
if indicators:
# when there are indicators present in sub-plot rows
# take the last one (nearest to the bottom) and place the
# crosshair label on it's x-axis.
last_ind = indicators[-1]
self.proxy_enter = pg.SignalProxy(
self.parent.sig_mouse_enter,
rateLimit=_mouse_rate_limit,
slot=lambda: self.mouseAction('Enter', False),
)
self.proxy_leave = pg.SignalProxy(
self.parent.sig_mouse_leave,
rateLimit=_mouse_rate_limit,
slot=lambda: self.mouseAction('Leave', False),
)
# determine where to place x-axis label
if _xaxis_at == 'bottom':
# place below is last indicator subplot
self.xaxis_label = XAxisLabel(
parent=last_ind.getAxis('bottom'), opacity=1
)
else:
# keep x-axis right below main chart
self.xaxis_label = XAxisLabel(parent=self.xaxis, opacity=1)
for i in indicators:
# add vertial and horizonal lines and a y-axis label
vl = i.addLine(x=0, pen=self.pen, movable=False)
hl = i.addLine(y=0, pen=self.pen, movable=False)
yl = YAxisLabel(parent=i.getAxis('right'), opacity=1)
px_moved = pg.SignalProxy( px_moved = pg.SignalProxy(
i.scene().sigMouseMoved, plot.scene().sigMouseMoved,
rateLimit=_mouse_rate_limit, rateLimit=_mouse_rate_limit,
slot=self.mouseMoved slot=self.mouseMoved
) )
px_enter = pg.SignalProxy( px_enter = pg.SignalProxy(
i.sig_mouse_enter, plot.sig_mouse_enter,
rateLimit=_mouse_rate_limit, rateLimit=_mouse_rate_limit,
slot=lambda: self.mouseAction('Enter', i), slot=lambda: self.mouseAction('Enter', plot),
) )
px_leave = pg.SignalProxy( px_leave = pg.SignalProxy(
i.sig_mouse_leave, plot.sig_mouse_leave,
rateLimit=_mouse_rate_limit, rateLimit=_mouse_rate_limit,
slot=lambda: self.mouseAction('Leave', i), slot=lambda: self.mouseAction('Leave', plot),
) )
self.indicators[i] = { self.graphics[plot] = {
'vl': vl, 'vl': vl,
'hl': hl, 'hl': hl,
'yl': yl, 'yl': yl,
'px': (px_moved, px_enter, px_leave), 'px': (px_moved, px_enter, px_leave),
} }
self.plots.append(plot)
def mouseAction(self, action, ind=False): # noqa # determine where to place x-axis label
if _xaxis_at == 'bottom':
# place below the last plot
self.xaxis_label = XAxisLabel(
parent=self.plots[-1].getAxis('bottom'),
opacity=1
)
else:
# keep x-axis right below main chart
first = self.plots[0]
xaxis = first.getAxis('bottom')
self.xaxis_label = XAxisLabel(parent=xaxis, opacity=1)
def mouseAction(self, action, plot): # noqa
# TODO: why do we no handle all plots the same?
# -> main plot has special path? would simplify code.
if action == 'Enter': if action == 'Enter':
# show horiz line and y-label # show horiz line and y-label
if ind: self.graphics[plot]['hl'].show()
self.indicators[ind]['hl'].show() self.graphics[plot]['yl'].show()
self.indicators[ind]['yl'].show() self.active_plot = plot
self.activeIndicator = ind else: # Leave
else:
self.yaxis_label.show()
self.hline.show()
# Leave
else:
# hide horiz line and y-label # hide horiz line and y-label
if ind: self.graphics[plot]['hl'].hide()
self.indicators[ind]['hl'].hide() self.graphics[plot]['yl'].hide()
self.indicators[ind]['yl'].hide() self.active_plot = None
self.activeIndicator = None
else:
self.yaxis_label.hide()
self.hline.hide()
def mouseMoved(self, evt): # noqa def mouseMoved(self, evt): # noqa
"""Update horizonal and vertical lines when mouse moves inside """Update horizonal and vertical lines when mouse moves inside
either the main chart or any indicator subplot. either the main chart or any indicator subplot.
""" """
pos = evt[0] pos = evt[0]
# if the mouse is within the parent ``ChartPlotWidget`` # find position in main chart
if self.parent.sceneBoundingRect().contains(pos): mouse_point = self.plots[0].mapToView(pos)
# mouse_point = self.vb.mapSceneToView(pos)
mouse_point = self.parent.mapToView(pos)
# move the vertial line to the current x coordinate # move the vertical line to the current x coordinate in all charts
self.vline.setX(mouse_point.x()) for opts in self.graphics.values():
opts['vl'].setX(mouse_point.x())
# update the label on the bottom of the crosshair # update the label on the bottom of the crosshair
self.xaxis_label.update_label(evt_post=pos, point_view=mouse_point) self.xaxis_label.update_label(evt_post=pos, point_view=mouse_point)
# update the vertical line in any indicators subplots # vertical position of the mouse is inside an indicator
for opts in self.indicators.values(): mouse_point_ind = self.active_plot.mapToView(pos)
opts['vl'].setX(mouse_point.x())
if self.activeIndicator: self.graphics[self.active_plot]['hl'].setY(
# vertial position of the mouse is inside an indicator
mouse_point_ind = self.activeIndicator.mapToView(pos)
self.indicators[self.activeIndicator]['hl'].setY(
mouse_point_ind.y() mouse_point_ind.y()
) )
self.indicators[self.activeIndicator]['yl'].update_label( self.graphics[self.active_plot]['yl'].update_label(
evt_post=pos, point_view=mouse_point_ind evt_post=pos, point_view=mouse_point_ind
) )
else:
# vertial position of the mouse is inside the main chart
self.hline.setY(mouse_point.y())
self.yaxis_label.update_label(
evt_post=pos, point_view=mouse_point
)
def paint(self, p, *args): # def paint(self, p, *args):
pass # pass
def boundingRect(self): def boundingRect(self):
return self.parent.boundingRect() return self.plots[0].boundingRect()
def _mk_lines_array(data: List, size: int) -> np.ndarray: def _mk_lines_array(data: List, size: int) -> np.ndarray:
@ -198,7 +162,8 @@ def bars_from_ohlc(
lines = _mk_lines_array(data, data.shape[0]) lines = _mk_lines_array(data, data.shape[0])
for i, q in enumerate(data[start:], start=start): for i, q in enumerate(data[start:], start=start):
open, high, low, close, index = q[['open', 'high', 'low', 'close', 'index']] open, high, low, close, index = q[
['open', 'high', 'low', 'close', 'index']]
# high - low line # high - low line
if low != high: if low != high: