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
							parent
							
								
									d6bd964fac
								
							
						
					
					
						commit
						a1032a0cd7
					
				| 
						 | 
					@ -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:
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
		Reference in New Issue