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.
its_happening
Tyler Goodlet 2020-07-16 21:54:24 -04:00
parent d6bd964fac
commit a1032a0cd7
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
digits: int = 0,
) -> None:
# add ``pg.graphicsItems.InfiniteLine``s
# 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.proxy_moved = pg.SignalProxy( # TODO: checkout what ``.sigDelayed`` can be used for
self.parent.scene().sigMouseMoved, # (emitted once a sufficient delay occurs in mouse movement)
px_moved = pg.SignalProxy(
plot.scene().sigMouseMoved,
rateLimit=_mouse_rate_limit, rateLimit=_mouse_rate_limit,
slot=self.mouseMoved, slot=self.mouseMoved
) )
px_enter = pg.SignalProxy(
self.yaxis_label = YAxisLabel( plot.sig_mouse_enter,
parent=self.yaxis, digits=digits, opacity=1 rateLimit=_mouse_rate_limit,
slot=lambda: self.mouseAction('Enter', plot),
) )
px_leave = pg.SignalProxy(
indicators = indicators or [] plot.sig_mouse_leave,
rateLimit=_mouse_rate_limit,
if indicators: slot=lambda: self.mouseAction('Leave', plot),
# when there are indicators present in sub-plot rows )
# take the last one (nearest to the bottom) and place the self.graphics[plot] = {
# crosshair label on it's x-axis. 'vl': vl,
last_ind = indicators[-1] 'hl': hl,
'yl': yl,
self.proxy_enter = pg.SignalProxy( 'px': (px_moved, px_enter, px_leave),
self.parent.sig_mouse_enter, }
rateLimit=_mouse_rate_limit, self.plots.append(plot)
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 # determine where to place x-axis label
if _xaxis_at == 'bottom': if _xaxis_at == 'bottom':
# place below is last indicator subplot # place below the last plot
self.xaxis_label = XAxisLabel( self.xaxis_label = XAxisLabel(
parent=last_ind.getAxis('bottom'), opacity=1 parent=self.plots[-1].getAxis('bottom'),
opacity=1
) )
else: else:
# keep x-axis right below main chart # keep x-axis right below main chart
self.xaxis_label = XAxisLabel(parent=self.xaxis, opacity=1) first = self.plots[0]
xaxis = first.getAxis('bottom')
self.xaxis_label = XAxisLabel(parent=xaxis, opacity=1)
for i in indicators: def mouseAction(self, action, plot): # noqa
# add vertial and horizonal lines and a y-axis label # TODO: why do we no handle all plots the same?
vl = i.addLine(x=0, pen=self.pen, movable=False) # -> main plot has special path? would simplify code.
hl = i.addLine(y=0, pen=self.pen, movable=False)
yl = YAxisLabel(parent=i.getAxis('right'), opacity=1)
px_moved = pg.SignalProxy(
i.scene().sigMouseMoved,
rateLimit=_mouse_rate_limit,
slot=self.mouseMoved
)
px_enter = pg.SignalProxy(
i.sig_mouse_enter,
rateLimit=_mouse_rate_limit,
slot=lambda: self.mouseAction('Enter', i),
)
px_leave = pg.SignalProxy(
i.sig_mouse_leave,
rateLimit=_mouse_rate_limit,
slot=lambda: self.mouseAction('Leave', i),
)
self.indicators[i] = {
'vl': vl,
'hl': hl,
'yl': yl,
'px': (px_moved, px_enter, px_leave),
}
def mouseAction(self, action, ind=False): # noqa
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.y()
mouse_point_ind = self.activeIndicator.mapToView(pos) )
self.indicators[self.activeIndicator]['hl'].setY( self.graphics[self.active_plot]['yl'].update_label(
mouse_point_ind.y() evt_post=pos, point_view=mouse_point_ind
) )
self.indicators[self.activeIndicator]['yl'].update_label(
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: