Break specialized graphics into specific mods
							parent
							
								
									575b3a0605
								
							
						
					
					
						commit
						a68fff4139
					
				|  | @ -30,9 +30,11 @@ from ._axes import ( | |||
|     DynamicDateAxis, | ||||
|     PriceAxis, | ||||
| ) | ||||
| from ._graphics import ( | ||||
| from ._graphics._cursor import ( | ||||
|     CrossHair, | ||||
|     ContentsLabel, | ||||
| ) | ||||
| from ._graphics._lines import ( | ||||
|     level_line, | ||||
|     L1Labels, | ||||
| ) | ||||
|  |  | |||
|  | @ -15,582 +15,6 @@ | |||
| # along with this program.  If not, see <https://www.gnu.org/licenses/>. | ||||
| 
 | ||||
| """ | ||||
| Chart graphics for displaying a slew of different data types. | ||||
| Internal custom graphics mostly built for low latency and reuse. | ||||
| 
 | ||||
| """ | ||||
| import inspect | ||||
| from typing import List, Optional, Tuple | ||||
| 
 | ||||
| import numpy as np | ||||
| import pyqtgraph as pg | ||||
| from numba import jit, float64, int64  # , optional | ||||
| # from numba import types as ntypes | ||||
| from PyQt5 import QtCore, QtGui | ||||
| from PyQt5.QtCore import QLineF, QPointF | ||||
| 
 | ||||
| # from .._profile import timeit | ||||
| # from ..data._source import numba_ohlc_dtype | ||||
| from .._style import ( | ||||
|     _xaxis_at, | ||||
|     hcolor, | ||||
|     _font, | ||||
|     _down_2_font_inches_we_like, | ||||
| ) | ||||
| from .._axes import YAxisLabel, XAxisLabel, YSticky | ||||
| 
 | ||||
| 
 | ||||
| # XXX: these settings seem to result in really decent mouse scroll | ||||
| # latency (in terms of perceived lag in cross hair) so really be sure | ||||
| # there's an improvement if you want to change it! | ||||
| _mouse_rate_limit = 60  # TODO; should we calc current screen refresh rate? | ||||
| _debounce_delay = 1 / 2e3 | ||||
| _ch_label_opac = 1 | ||||
| 
 | ||||
| 
 | ||||
| # TODO: we need to handle the case where index is outside | ||||
| # the underlying datums range | ||||
| class LineDot(pg.CurvePoint): | ||||
| 
 | ||||
|     def __init__( | ||||
|         self, | ||||
|         curve: pg.PlotCurveItem, | ||||
|         index: int, | ||||
|         plot: 'ChartPlotWidget', | ||||
|         pos=None, | ||||
|         size: int = 2,  # in pxs | ||||
|         color: str = 'default_light', | ||||
|     ) -> None: | ||||
|         pg.CurvePoint.__init__( | ||||
|             self, | ||||
|             curve, | ||||
|             index=index, | ||||
|             pos=pos, | ||||
|             rotate=False, | ||||
|         ) | ||||
|         self._plot = plot | ||||
| 
 | ||||
|         # TODO: get pen from curve if not defined? | ||||
|         cdefault = hcolor(color) | ||||
|         pen = pg.mkPen(cdefault) | ||||
|         brush = pg.mkBrush(cdefault) | ||||
| 
 | ||||
|         # presuming this is fast since it's built in? | ||||
|         dot = self.dot = QtGui.QGraphicsEllipseItem( | ||||
|             QtCore.QRectF(-size / 2, -size / 2, size, size) | ||||
|         ) | ||||
|         # if we needed transformable dot? | ||||
|         # dot.translate(-size*0.5, -size*0.5) | ||||
|         dot.setPen(pen) | ||||
|         dot.setBrush(brush) | ||||
|         dot.setParentItem(self) | ||||
| 
 | ||||
|         # keep a static size | ||||
|         self.setFlag(self.ItemIgnoresTransformations) | ||||
| 
 | ||||
|     def event( | ||||
|         self, | ||||
|         ev: QtCore.QEvent, | ||||
|     ) -> None: | ||||
|         # print((ev, type(ev))) | ||||
|         if not isinstance(ev, QtCore.QDynamicPropertyChangeEvent) or self.curve() is None: | ||||
|             return False | ||||
| 
 | ||||
|         # if ev.propertyName() == 'index': | ||||
|         #     print(ev) | ||||
|         #     # self.setProperty | ||||
| 
 | ||||
|         (x, y) = self.curve().getData() | ||||
|         index = self.property('index') | ||||
|         # first = self._plot._ohlc[0]['index'] | ||||
|         # first = x[0] | ||||
|         # i = index - first | ||||
|         i = index - x[0] | ||||
|         if i > 0 and i < len(y): | ||||
|             newPos = (index, y[i]) | ||||
|             QtGui.QGraphicsItem.setPos(self, *newPos) | ||||
|             return True | ||||
| 
 | ||||
|         return False | ||||
| 
 | ||||
| 
 | ||||
| _corner_anchors = { | ||||
|     'top': 0, | ||||
|     'left': 0, | ||||
|     'bottom': 1, | ||||
|     'right': 1, | ||||
| } | ||||
| # XXX: fyi naming here is confusing / opposite to coords | ||||
| _corner_margins = { | ||||
|     ('top', 'left'): (-4, -5), | ||||
|     ('top', 'right'): (4, -5), | ||||
| 
 | ||||
|     ('bottom', 'left'): (-4, lambda font_size: font_size * 2), | ||||
|     ('bottom', 'right'): (4, lambda font_size: font_size * 2), | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
| class ContentsLabel(pg.LabelItem): | ||||
|     """Label anchored to a ``ViewBox`` typically for displaying | ||||
|     datum-wise points from the "viewed" contents. | ||||
| 
 | ||||
|     """ | ||||
|     def __init__( | ||||
|         self, | ||||
|         chart: 'ChartPlotWidget',  # noqa | ||||
|         anchor_at: str = ('top', 'right'), | ||||
|         justify_text: str = 'left', | ||||
|         font_size: Optional[int] = None, | ||||
|     ) -> None: | ||||
|         font_size = font_size or _font.font.pixelSize() | ||||
|         super().__init__( | ||||
|             justify=justify_text, | ||||
|             size=f'{str(font_size)}px' | ||||
|         ) | ||||
| 
 | ||||
|         # anchor to viewbox | ||||
|         self.setParentItem(chart._vb) | ||||
|         chart.scene().addItem(self) | ||||
|         self.chart = chart | ||||
| 
 | ||||
|         v, h = anchor_at | ||||
|         index = (_corner_anchors[h], _corner_anchors[v]) | ||||
|         margins = _corner_margins[(v, h)] | ||||
| 
 | ||||
|         ydim = margins[1] | ||||
|         if inspect.isfunction(margins[1]): | ||||
|             margins = margins[0], ydim(font_size) | ||||
| 
 | ||||
|         self.anchor(itemPos=index, parentPos=index, offset=margins) | ||||
| 
 | ||||
|     def update_from_ohlc( | ||||
|         self, | ||||
|         name: str, | ||||
|         index: int, | ||||
|         array: np.ndarray, | ||||
|     ) -> None: | ||||
|         # this being "html" is the dumbest shit :eyeroll: | ||||
|         first = array[0]['index'] | ||||
| 
 | ||||
|         self.setText( | ||||
|             "<b>i</b>:{index}<br/>" | ||||
|             "<b>O</b>:{}<br/>" | ||||
|             "<b>H</b>:{}<br/>" | ||||
|             "<b>L</b>:{}<br/>" | ||||
|             "<b>C</b>:{}<br/>" | ||||
|             "<b>V</b>:{}<br/>" | ||||
|             "<b>wap</b>:{}".format( | ||||
|                 *array[index - first][ | ||||
|                     ['open', 'high', 'low', 'close', 'volume', 'bar_wap'] | ||||
|                 ], | ||||
|                 name=name, | ||||
|                 index=index, | ||||
|             ) | ||||
|         ) | ||||
| 
 | ||||
|     def update_from_value( | ||||
|         self, | ||||
|         name: str, | ||||
|         index: int, | ||||
|         array: np.ndarray, | ||||
|     ) -> None: | ||||
|         first = array[0]['index'] | ||||
|         if index < array[-1]['index'] and index > first: | ||||
|             data = array[index - first][name] | ||||
|             self.setText(f"{name}: {data:.2f}") | ||||
| 
 | ||||
| 
 | ||||
| class CrossHair(pg.GraphicsObject): | ||||
| 
 | ||||
|     def __init__( | ||||
|         self, | ||||
|         linkedsplitcharts: 'LinkedSplitCharts',  # noqa | ||||
|         digits: int = 0 | ||||
|     ) -> None: | ||||
|         super().__init__() | ||||
|         # XXX: not sure why these are instance variables? | ||||
|         # It's not like we can change them on the fly..? | ||||
|         self.pen = pg.mkPen( | ||||
|             color=hcolor('default'), | ||||
|             style=QtCore.Qt.DashLine, | ||||
|         ) | ||||
|         self.lines_pen = pg.mkPen( | ||||
|             color='#a9a9a9',  # gray? | ||||
|             style=QtCore.Qt.DashLine, | ||||
|         ) | ||||
|         self.lsc = linkedsplitcharts | ||||
|         self.graphics = {} | ||||
|         self.plots = [] | ||||
|         self.active_plot = None | ||||
|         self.digits = digits | ||||
|         self._lastx = None | ||||
|         # self.setCacheMode(QtGui.QGraphicsItem.DeviceCoordinateCache) | ||||
| 
 | ||||
|     def add_plot( | ||||
|         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.lines_pen, movable=False) | ||||
|         vl.setCacheMode(QtGui.QGraphicsItem.DeviceCoordinateCache) | ||||
| 
 | ||||
|         hl = plot.addLine(y=0, pen=self.lines_pen, movable=False) | ||||
|         hl.setCacheMode(QtGui.QGraphicsItem.DeviceCoordinateCache) | ||||
|         hl.hide() | ||||
| 
 | ||||
|         yl = YAxisLabel( | ||||
|             parent=plot.getAxis('right'), | ||||
|             digits=digits or self.digits, | ||||
|             opacity=_ch_label_opac, | ||||
|             bg_color='default', | ||||
|         ) | ||||
|         yl.setCacheMode(QtGui.QGraphicsItem.DeviceCoordinateCache) | ||||
|         yl.hide()  # on startup if mouse is off screen | ||||
| 
 | ||||
|         # TODO: checkout what ``.sigDelayed`` can be used for | ||||
|         # (emitted once a sufficient delay occurs in mouse movement) | ||||
|         px_moved = pg.SignalProxy( | ||||
|             plot.scene().sigMouseMoved, | ||||
|             rateLimit=_mouse_rate_limit, | ||||
|             slot=self.mouseMoved, | ||||
|             delay=_debounce_delay, | ||||
|         ) | ||||
|         px_enter = pg.SignalProxy( | ||||
|             plot.sig_mouse_enter, | ||||
|             rateLimit=_mouse_rate_limit, | ||||
|             slot=lambda: self.mouseAction('Enter', plot), | ||||
|             delay=_debounce_delay, | ||||
|         ) | ||||
|         px_leave = pg.SignalProxy( | ||||
|             plot.sig_mouse_leave, | ||||
|             rateLimit=_mouse_rate_limit, | ||||
|             slot=lambda: self.mouseAction('Leave', plot), | ||||
|             delay=_debounce_delay, | ||||
|         ) | ||||
|         self.graphics[plot] = { | ||||
|             'vl': vl, | ||||
|             'hl': hl, | ||||
|             'yl': yl, | ||||
|             'px': (px_moved, px_enter, px_leave), | ||||
|         } | ||||
|         self.plots.append(plot) | ||||
| 
 | ||||
|         # Determine where to place x-axis label. | ||||
|         # Place below the last plot by default, ow | ||||
|         # keep x-axis right below main chart | ||||
|         plot_index = -1 if _xaxis_at == 'bottom' else 0 | ||||
| 
 | ||||
|         self.xaxis_label = XAxisLabel( | ||||
|             parent=self.plots[plot_index].getAxis('bottom'), | ||||
|             opacity=_ch_label_opac, | ||||
|             bg_color='default', | ||||
|         ) | ||||
|         # place label off-screen during startup | ||||
|         self.xaxis_label.setPos(self.plots[0].mapFromView(QPointF(0, 0))) | ||||
|         self.xaxis_label.setCacheMode(QtGui.QGraphicsItem.DeviceCoordinateCache) | ||||
| 
 | ||||
|     def add_curve_cursor( | ||||
|         self, | ||||
|         plot: 'ChartPlotWidget',  # noqa | ||||
|         curve: 'PlotCurveItem',  # noqa | ||||
|     ) -> LineDot: | ||||
|         # if this plot contains curves add line dot "cursors" to denote | ||||
|         # the current sample under the mouse | ||||
|         cursor = LineDot(curve, index=plot._ohlc[-1]['index'], plot=plot) | ||||
|         plot.addItem(cursor) | ||||
|         self.graphics[plot].setdefault('cursors', []).append(cursor) | ||||
|         return cursor | ||||
| 
 | ||||
|     def mouseAction(self, action, plot):  # noqa | ||||
|         if action == 'Enter': | ||||
|             self.active_plot = plot | ||||
| 
 | ||||
|             # show horiz line and y-label | ||||
|             self.graphics[plot]['hl'].show() | ||||
|             self.graphics[plot]['yl'].show() | ||||
| 
 | ||||
|         else:  # Leave | ||||
|             self.active_plot = None | ||||
| 
 | ||||
|             # hide horiz line and y-label | ||||
|             self.graphics[plot]['hl'].hide() | ||||
|             self.graphics[plot]['yl'].hide() | ||||
| 
 | ||||
|     def mouseMoved( | ||||
|         self, | ||||
|         evt: 'Tuple[QMouseEvent]',  # noqa | ||||
|     ) -> None:  # noqa | ||||
|         """Update horizonal and vertical lines when mouse moves inside | ||||
|         either the main chart or any indicator subplot. | ||||
|         """ | ||||
|         pos = evt[0] | ||||
| 
 | ||||
|         # find position inside active plot | ||||
|         try: | ||||
|             # map to view coordinate system | ||||
|             mouse_point = self.active_plot.mapToView(pos) | ||||
|         except AttributeError: | ||||
|             # mouse was not on active plot | ||||
|             return | ||||
| 
 | ||||
|         x, y = mouse_point.x(), mouse_point.y() | ||||
|         plot = self.active_plot | ||||
| 
 | ||||
|         # update y-range items | ||||
|         self.graphics[plot]['hl'].setY(y) | ||||
| 
 | ||||
|         self.graphics[self.active_plot]['yl'].update_label( | ||||
|             abs_pos=pos, value=y | ||||
|         ) | ||||
| 
 | ||||
|         # Update x if cursor changed after discretization calc | ||||
|         # (this saves draw cycles on small mouse moves) | ||||
|         lastx = self._lastx | ||||
|         ix = round(x)  # since bars are centered around index | ||||
| 
 | ||||
|         if ix != lastx: | ||||
|             for plot, opts in self.graphics.items(): | ||||
| 
 | ||||
|                 # move the vertical line to the current "center of bar" | ||||
|                 opts['vl'].setX(ix) | ||||
| 
 | ||||
|                 # update the chart's "contents" label | ||||
|                 plot.update_contents_labels(ix) | ||||
| 
 | ||||
|                 # update all subscribed curve dots | ||||
|                 # first = plot._ohlc[0]['index'] | ||||
|                 for cursor in opts.get('cursors', ()): | ||||
|                     cursor.setIndex(ix) | ||||
| 
 | ||||
|             # update the label on the bottom of the crosshair | ||||
|             self.xaxis_label.update_label( | ||||
| 
 | ||||
|                 # XXX: requires: | ||||
|                 # https://github.com/pyqtgraph/pyqtgraph/pull/1418 | ||||
|                 # otherwise gobbles tons of CPU.. | ||||
| 
 | ||||
|                 # map back to abs (label-local) coordinates | ||||
|                 abs_pos=plot.mapFromView(QPointF(ix, y)), | ||||
|                 value=x, | ||||
|             ) | ||||
| 
 | ||||
|         self._lastx = ix | ||||
| 
 | ||||
|     def boundingRect(self): | ||||
|         try: | ||||
|             return self.active_plot.boundingRect() | ||||
|         except AttributeError: | ||||
|             return self.plots[0].boundingRect() | ||||
| 
 | ||||
| 
 | ||||
| class LevelLabel(YSticky): | ||||
| 
 | ||||
|     line_pen = pg.mkPen(hcolor('bracket')) | ||||
| 
 | ||||
|     _w_margin = 4 | ||||
|     _h_margin = 3 | ||||
|     level: float = 0 | ||||
| 
 | ||||
|     def __init__( | ||||
|         self, | ||||
|         chart, | ||||
|         *args, | ||||
|         orient_v: str = 'bottom', | ||||
|         orient_h: str = 'left', | ||||
|         **kwargs | ||||
|     ) -> None: | ||||
|         super().__init__(chart, *args, **kwargs) | ||||
| 
 | ||||
|         # orientation around axis options | ||||
|         self._orient_v = orient_v | ||||
|         self._orient_h = orient_h | ||||
|         self._v_shift = { | ||||
|             'top': 1., | ||||
|             'bottom': 0, | ||||
|             'middle': 1 / 2. | ||||
|         }[orient_v] | ||||
| 
 | ||||
|         self._h_shift = { | ||||
|             'left': -1., 'right': 0 | ||||
|         }[orient_h] | ||||
| 
 | ||||
|     def update_label( | ||||
|         self, | ||||
|         abs_pos: QPointF,  # scene coords | ||||
|         level: float,  # data for text | ||||
|         offset: int = 1  # if have margins, k? | ||||
|     ) -> None: | ||||
| 
 | ||||
|         # write contents, type specific | ||||
|         self.set_label_str(level) | ||||
| 
 | ||||
|         br = self.boundingRect() | ||||
|         h, w = br.height(), br.width() | ||||
| 
 | ||||
|         # this triggers ``.pain()`` implicitly? | ||||
|         self.setPos(QPointF( | ||||
|             self._h_shift * w - offset, | ||||
|             abs_pos.y() - (self._v_shift * h) - offset | ||||
|         )) | ||||
|         self.update() | ||||
| 
 | ||||
|         self.level = level | ||||
| 
 | ||||
|     def set_label_str(self, level: float): | ||||
|         # this is read inside ``.paint()`` | ||||
|         # self.label_str = '{size} x {level:.{digits}f}'.format( | ||||
|         self.label_str = '{level:.{digits}f}'.format( | ||||
|             # size=self._size, | ||||
|             digits=self.digits, | ||||
|             level=level | ||||
|         ).replace(',', ' ') | ||||
| 
 | ||||
|     def size_hint(self) -> Tuple[None, None]: | ||||
|         return None, None | ||||
| 
 | ||||
|     def draw( | ||||
|         self, | ||||
|         p: QtGui.QPainter, | ||||
|         rect: QtCore.QRectF | ||||
|     ) -> None: | ||||
|         p.setPen(self.line_pen) | ||||
| 
 | ||||
|         if self._orient_v == 'bottom': | ||||
|             lp, rp = rect.topLeft(), rect.topRight() | ||||
|             # p.drawLine(rect.topLeft(), rect.topRight()) | ||||
|         elif self._orient_v == 'top': | ||||
|             lp, rp = rect.bottomLeft(), rect.bottomRight() | ||||
| 
 | ||||
|         p.drawLine(lp.x(), lp.y(), rp.x(), rp.y()) | ||||
| 
 | ||||
| 
 | ||||
| class L1Label(LevelLabel): | ||||
| 
 | ||||
|     size: float = 0 | ||||
|     size_digits: float = 3 | ||||
| 
 | ||||
|     text_flags = ( | ||||
|         QtCore.Qt.TextDontClip | ||||
|         | QtCore.Qt.AlignLeft | ||||
|     ) | ||||
| 
 | ||||
|     def set_label_str(self, level: float) -> None: | ||||
|         """Reimplement the label string write to include the level's order-queue's | ||||
|         size in the text, eg. 100 x 323.3. | ||||
| 
 | ||||
|         """ | ||||
|         self.label_str = '{size:.{size_digits}f} x {level:,.{digits}f}'.format( | ||||
|             size_digits=self.size_digits, | ||||
|             size=self.size or '?', | ||||
|             digits=self.digits, | ||||
|             level=level | ||||
|         ).replace(',', ' ') | ||||
| 
 | ||||
| 
 | ||||
| class L1Labels: | ||||
|     """Level 1 bid ask labels for dynamic update on price-axis. | ||||
| 
 | ||||
|     """ | ||||
|     max_value: float = '100.0 x 100 000.00' | ||||
| 
 | ||||
|     def __init__( | ||||
|         self, | ||||
|         chart: 'ChartPlotWidget',  # noqa | ||||
|         digits: int = 2, | ||||
|         size_digits: int = 0, | ||||
|         font_size_inches: float = _down_2_font_inches_we_like, | ||||
|     ) -> None: | ||||
| 
 | ||||
|         self.chart = chart | ||||
| 
 | ||||
|         self.bid_label = L1Label( | ||||
|             chart=chart, | ||||
|             parent=chart.getAxis('right'), | ||||
|             # TODO: pass this from symbol data | ||||
|             digits=digits, | ||||
|             opacity=1, | ||||
|             font_size_inches=font_size_inches, | ||||
|             bg_color='papas_special', | ||||
|             fg_color='bracket', | ||||
|             orient_v='bottom', | ||||
|         ) | ||||
|         self.bid_label.size_digits = size_digits | ||||
|         self.bid_label._size_br_from_str(self.max_value) | ||||
| 
 | ||||
|         self.ask_label = L1Label( | ||||
|             chart=chart, | ||||
|             parent=chart.getAxis('right'), | ||||
|             # TODO: pass this from symbol data | ||||
|             digits=digits, | ||||
|             opacity=1, | ||||
|             font_size_inches=font_size_inches, | ||||
|             bg_color='papas_special', | ||||
|             fg_color='bracket', | ||||
|             orient_v='top', | ||||
|         ) | ||||
|         self.ask_label.size_digits = size_digits | ||||
|         self.ask_label._size_br_from_str(self.max_value) | ||||
| 
 | ||||
| 
 | ||||
| class LevelLine(pg.InfiniteLine): | ||||
|     def __init__( | ||||
|         self, | ||||
|         label: LevelLabel, | ||||
|         **kwargs, | ||||
|     ) -> None: | ||||
|         self.label = label | ||||
|         super().__init__(**kwargs) | ||||
|         self.sigPositionChanged.connect(self.set_level) | ||||
| 
 | ||||
|     def set_level(self, value: float) -> None: | ||||
|         self.label.update_from_data(0, self.value()) | ||||
| 
 | ||||
| 
 | ||||
| def level_line( | ||||
|     chart: 'ChartPlogWidget',  # noqa | ||||
|     level: float, | ||||
|     digits: int = 1, | ||||
| 
 | ||||
|     # size 4 font on 4k screen scaled down, so small-ish. | ||||
|     font_size_inches: float = _down_2_font_inches_we_like, | ||||
| 
 | ||||
|     show_label: bool = True, | ||||
| 
 | ||||
|     **linelabelkwargs | ||||
| ) -> LevelLine: | ||||
|     """Convenience routine to add a styled horizontal line to a plot. | ||||
| 
 | ||||
|     """ | ||||
|     label = LevelLabel( | ||||
|         chart=chart, | ||||
|         parent=chart.getAxis('right'), | ||||
|         # TODO: pass this from symbol data | ||||
|         digits=digits, | ||||
|         opacity=1, | ||||
|         font_size_inches=font_size_inches, | ||||
|         # TODO: make this take the view's bg pen | ||||
|         bg_color='papas_special', | ||||
|         fg_color='default', | ||||
|         **linelabelkwargs | ||||
|     ) | ||||
|     label.update_from_data(0, level) | ||||
| 
 | ||||
|     # TODO: can we somehow figure out a max value from the parent axis? | ||||
|     label._size_br_from_str(label.label_str) | ||||
| 
 | ||||
|     line = LevelLine( | ||||
|         label, | ||||
|         movable=True, | ||||
|         angle=0, | ||||
|     ) | ||||
|     line.setValue(level) | ||||
|     line.setPen(pg.mkPen(hcolor('default'))) | ||||
|     # activate/draw label | ||||
|     line.setValue(level) | ||||
| 
 | ||||
|     chart.plotItem.addItem(line) | ||||
| 
 | ||||
|     if not show_label: | ||||
|         label.hide() | ||||
| 
 | ||||
|     return line | ||||
|  |  | |||
|  | @ -0,0 +1,380 @@ | |||
| # piker: trading gear for hackers | ||||
| # Copyright (C) 2018-present  Tyler Goodlet (in stewardship of piker0) | ||||
| 
 | ||||
| # This program is free software: you can redistribute it and/or modify | ||||
| # it under the terms of the GNU Affero General Public License as published by | ||||
| # the Free Software Foundation, either version 3 of the License, or | ||||
| # (at your option) any later version. | ||||
| 
 | ||||
| # This program is distributed in the hope that it will be useful, | ||||
| # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||
| # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | ||||
| # GNU Affero General Public License for more details. | ||||
| 
 | ||||
| # You should have received a copy of the GNU Affero General Public License | ||||
| # along with this program.  If not, see <https://www.gnu.org/licenses/>. | ||||
| """ | ||||
| Mouse interaction graphics | ||||
| 
 | ||||
| """ | ||||
| from typing import Optional, Tuple | ||||
| 
 | ||||
| import inspect | ||||
| import numpy as np | ||||
| import pyqtgraph as pg | ||||
| from PyQt5 import QtCore, QtGui | ||||
| from PyQt5.QtCore import QPointF | ||||
| 
 | ||||
| from .._style import ( | ||||
|     _xaxis_at, | ||||
|     hcolor, | ||||
|     _font, | ||||
| ) | ||||
| from .._axes import YAxisLabel, XAxisLabel | ||||
| 
 | ||||
| # XXX: these settings seem to result in really decent mouse scroll | ||||
| # latency (in terms of perceived lag in cross hair) so really be sure | ||||
| # there's an improvement if you want to change it! | ||||
| _mouse_rate_limit = 60  # TODO; should we calc current screen refresh rate? | ||||
| _debounce_delay = 1 / 2e3 | ||||
| _ch_label_opac = 1 | ||||
| 
 | ||||
| 
 | ||||
| # TODO: we need to handle the case where index is outside | ||||
| # the underlying datums range | ||||
| class LineDot(pg.CurvePoint): | ||||
| 
 | ||||
|     def __init__( | ||||
|         self, | ||||
|         curve: pg.PlotCurveItem, | ||||
|         index: int, | ||||
|         plot: 'ChartPlotWidget',  # type: ingore # noqa | ||||
|         pos=None, | ||||
|         size: int = 2,  # in pxs | ||||
|         color: str = 'default_light', | ||||
|     ) -> None: | ||||
|         pg.CurvePoint.__init__( | ||||
|             self, | ||||
|             curve, | ||||
|             index=index, | ||||
|             pos=pos, | ||||
|             rotate=False, | ||||
|         ) | ||||
|         self._plot = plot | ||||
| 
 | ||||
|         # TODO: get pen from curve if not defined? | ||||
|         cdefault = hcolor(color) | ||||
|         pen = pg.mkPen(cdefault) | ||||
|         brush = pg.mkBrush(cdefault) | ||||
| 
 | ||||
|         # presuming this is fast since it's built in? | ||||
|         dot = self.dot = QtGui.QGraphicsEllipseItem( | ||||
|             QtCore.QRectF(-size / 2, -size / 2, size, size) | ||||
|         ) | ||||
|         # if we needed transformable dot? | ||||
|         # dot.translate(-size*0.5, -size*0.5) | ||||
|         dot.setPen(pen) | ||||
|         dot.setBrush(brush) | ||||
|         dot.setParentItem(self) | ||||
| 
 | ||||
|         # keep a static size | ||||
|         self.setFlag(self.ItemIgnoresTransformations) | ||||
| 
 | ||||
|     def event( | ||||
|         self, | ||||
|         ev: QtCore.QEvent, | ||||
|     ) -> None: | ||||
|         # print((ev, type(ev))) | ||||
|         if not isinstance( | ||||
|             ev, QtCore.QDynamicPropertyChangeEvent | ||||
|         ) or self.curve() is None: | ||||
|             return False | ||||
| 
 | ||||
|         # if ev.propertyName() == 'index': | ||||
|         #     print(ev) | ||||
|         #     # self.setProperty | ||||
| 
 | ||||
|         (x, y) = self.curve().getData() | ||||
|         index = self.property('index') | ||||
|         # first = self._plot._ohlc[0]['index'] | ||||
|         # first = x[0] | ||||
|         # i = index - first | ||||
|         i = index - x[0] | ||||
|         if i > 0 and i < len(y): | ||||
|             newPos = (index, y[i]) | ||||
|             QtGui.QGraphicsItem.setPos(self, *newPos) | ||||
|             return True | ||||
| 
 | ||||
|         return False | ||||
| 
 | ||||
| 
 | ||||
| _corner_anchors = { | ||||
|     'top': 0, | ||||
|     'left': 0, | ||||
|     'bottom': 1, | ||||
|     'right': 1, | ||||
| } | ||||
| # XXX: fyi naming here is confusing / opposite to coords | ||||
| _corner_margins = { | ||||
|     ('top', 'left'): (-4, -5), | ||||
|     ('top', 'right'): (4, -5), | ||||
| 
 | ||||
|     ('bottom', 'left'): (-4, lambda font_size: font_size * 2), | ||||
|     ('bottom', 'right'): (4, lambda font_size: font_size * 2), | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
| class ContentsLabel(pg.LabelItem): | ||||
|     """Label anchored to a ``ViewBox`` typically for displaying | ||||
|     datum-wise points from the "viewed" contents. | ||||
| 
 | ||||
|     """ | ||||
|     def __init__( | ||||
|         self, | ||||
|         chart: 'ChartPlotWidget',  # noqa | ||||
|         anchor_at: str = ('top', 'right'), | ||||
|         justify_text: str = 'left', | ||||
|         font_size: Optional[int] = None, | ||||
|     ) -> None: | ||||
|         font_size = font_size or _font.font.pixelSize() | ||||
|         super().__init__( | ||||
|             justify=justify_text, | ||||
|             size=f'{str(font_size)}px' | ||||
|         ) | ||||
| 
 | ||||
|         # anchor to viewbox | ||||
|         self.setParentItem(chart._vb) | ||||
|         chart.scene().addItem(self) | ||||
|         self.chart = chart | ||||
| 
 | ||||
|         v, h = anchor_at | ||||
|         index = (_corner_anchors[h], _corner_anchors[v]) | ||||
|         margins = _corner_margins[(v, h)] | ||||
| 
 | ||||
|         ydim = margins[1] | ||||
|         if inspect.isfunction(margins[1]): | ||||
|             margins = margins[0], ydim(font_size) | ||||
| 
 | ||||
|         self.anchor(itemPos=index, parentPos=index, offset=margins) | ||||
| 
 | ||||
|     def update_from_ohlc( | ||||
|         self, | ||||
|         name: str, | ||||
|         index: int, | ||||
|         array: np.ndarray, | ||||
|     ) -> None: | ||||
|         # this being "html" is the dumbest shit :eyeroll: | ||||
|         first = array[0]['index'] | ||||
| 
 | ||||
|         self.setText( | ||||
|             "<b>i</b>:{index}<br/>" | ||||
|             "<b>O</b>:{}<br/>" | ||||
|             "<b>H</b>:{}<br/>" | ||||
|             "<b>L</b>:{}<br/>" | ||||
|             "<b>C</b>:{}<br/>" | ||||
|             "<b>V</b>:{}<br/>" | ||||
|             "<b>wap</b>:{}".format( | ||||
|                 *array[index - first][ | ||||
|                     ['open', 'high', 'low', 'close', 'volume', 'bar_wap'] | ||||
|                 ], | ||||
|                 name=name, | ||||
|                 index=index, | ||||
|             ) | ||||
|         ) | ||||
| 
 | ||||
|     def update_from_value( | ||||
|         self, | ||||
|         name: str, | ||||
|         index: int, | ||||
|         array: np.ndarray, | ||||
|     ) -> None: | ||||
|         first = array[0]['index'] | ||||
|         if index < array[-1]['index'] and index > first: | ||||
|             data = array[index - first][name] | ||||
|             self.setText(f"{name}: {data:.2f}") | ||||
| 
 | ||||
| 
 | ||||
| class CrossHair(pg.GraphicsObject): | ||||
| 
 | ||||
|     def __init__( | ||||
|         self, | ||||
|         linkedsplitcharts: 'LinkedSplitCharts',  # noqa | ||||
|         digits: int = 0 | ||||
|     ) -> None: | ||||
|         super().__init__() | ||||
|         # XXX: not sure why these are instance variables? | ||||
|         # It's not like we can change them on the fly..? | ||||
|         self.pen = pg.mkPen( | ||||
|             color=hcolor('default'), | ||||
|             style=QtCore.Qt.DashLine, | ||||
|         ) | ||||
|         self.lines_pen = pg.mkPen( | ||||
|             color='#a9a9a9',  # gray? | ||||
|             style=QtCore.Qt.DashLine, | ||||
|         ) | ||||
|         self.lsc = linkedsplitcharts | ||||
|         self.graphics = {} | ||||
|         self.plots = [] | ||||
|         self.active_plot = None | ||||
|         self.digits = digits | ||||
|         self._lastx = None | ||||
|         # self.setCacheMode(QtGui.QGraphicsItem.DeviceCoordinateCache) | ||||
| 
 | ||||
|     def add_plot( | ||||
|         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.lines_pen, movable=False) | ||||
|         vl.setCacheMode(QtGui.QGraphicsItem.DeviceCoordinateCache) | ||||
| 
 | ||||
|         hl = plot.addLine(y=0, pen=self.lines_pen, movable=False) | ||||
|         hl.setCacheMode(QtGui.QGraphicsItem.DeviceCoordinateCache) | ||||
|         hl.hide() | ||||
| 
 | ||||
|         yl = YAxisLabel( | ||||
|             parent=plot.getAxis('right'), | ||||
|             digits=digits or self.digits, | ||||
|             opacity=_ch_label_opac, | ||||
|             bg_color='default', | ||||
|         ) | ||||
|         yl.setCacheMode(QtGui.QGraphicsItem.DeviceCoordinateCache) | ||||
|         yl.hide()  # on startup if mouse is off screen | ||||
| 
 | ||||
|         # TODO: checkout what ``.sigDelayed`` can be used for | ||||
|         # (emitted once a sufficient delay occurs in mouse movement) | ||||
|         px_moved = pg.SignalProxy( | ||||
|             plot.scene().sigMouseMoved, | ||||
|             rateLimit=_mouse_rate_limit, | ||||
|             slot=self.mouseMoved, | ||||
|             delay=_debounce_delay, | ||||
|         ) | ||||
|         px_enter = pg.SignalProxy( | ||||
|             plot.sig_mouse_enter, | ||||
|             rateLimit=_mouse_rate_limit, | ||||
|             slot=lambda: self.mouseAction('Enter', plot), | ||||
|             delay=_debounce_delay, | ||||
|         ) | ||||
|         px_leave = pg.SignalProxy( | ||||
|             plot.sig_mouse_leave, | ||||
|             rateLimit=_mouse_rate_limit, | ||||
|             slot=lambda: self.mouseAction('Leave', plot), | ||||
|             delay=_debounce_delay, | ||||
|         ) | ||||
|         self.graphics[plot] = { | ||||
|             'vl': vl, | ||||
|             'hl': hl, | ||||
|             'yl': yl, | ||||
|             'px': (px_moved, px_enter, px_leave), | ||||
|         } | ||||
|         self.plots.append(plot) | ||||
| 
 | ||||
|         # Determine where to place x-axis label. | ||||
|         # Place below the last plot by default, ow | ||||
|         # keep x-axis right below main chart | ||||
|         plot_index = -1 if _xaxis_at == 'bottom' else 0 | ||||
| 
 | ||||
|         self.xaxis_label = XAxisLabel( | ||||
|             parent=self.plots[plot_index].getAxis('bottom'), | ||||
|             opacity=_ch_label_opac, | ||||
|             bg_color='default', | ||||
|         ) | ||||
|         # place label off-screen during startup | ||||
|         self.xaxis_label.setPos(self.plots[0].mapFromView(QPointF(0, 0))) | ||||
|         self.xaxis_label.setCacheMode( | ||||
|             QtGui.QGraphicsItem.DeviceCoordinateCache) | ||||
| 
 | ||||
|     def add_curve_cursor( | ||||
|         self, | ||||
|         plot: 'ChartPlotWidget',  # noqa | ||||
|         curve: 'PlotCurveItem',  # noqa | ||||
|     ) -> LineDot: | ||||
|         # if this plot contains curves add line dot "cursors" to denote | ||||
|         # the current sample under the mouse | ||||
|         cursor = LineDot(curve, index=plot._ohlc[-1]['index'], plot=plot) | ||||
|         plot.addItem(cursor) | ||||
|         self.graphics[plot].setdefault('cursors', []).append(cursor) | ||||
|         return cursor | ||||
| 
 | ||||
|     def mouseAction(self, action, plot):  # noqa | ||||
|         if action == 'Enter': | ||||
|             self.active_plot = plot | ||||
| 
 | ||||
|             # show horiz line and y-label | ||||
|             self.graphics[plot]['hl'].show() | ||||
|             self.graphics[plot]['yl'].show() | ||||
| 
 | ||||
|         else:  # Leave | ||||
|             self.active_plot = None | ||||
| 
 | ||||
|             # hide horiz line and y-label | ||||
|             self.graphics[plot]['hl'].hide() | ||||
|             self.graphics[plot]['yl'].hide() | ||||
| 
 | ||||
|     def mouseMoved( | ||||
|         self, | ||||
|         evt: 'Tuple[QMouseEvent]',  # noqa | ||||
|     ) -> None:  # noqa | ||||
|         """Update horizonal and vertical lines when mouse moves inside | ||||
|         either the main chart or any indicator subplot. | ||||
|         """ | ||||
|         pos = evt[0] | ||||
| 
 | ||||
|         # find position inside active plot | ||||
|         try: | ||||
|             # map to view coordinate system | ||||
|             mouse_point = self.active_plot.mapToView(pos) | ||||
|         except AttributeError: | ||||
|             # mouse was not on active plot | ||||
|             return | ||||
| 
 | ||||
|         x, y = mouse_point.x(), mouse_point.y() | ||||
|         plot = self.active_plot | ||||
| 
 | ||||
|         # update y-range items | ||||
|         self.graphics[plot]['hl'].setY(y) | ||||
| 
 | ||||
|         self.graphics[self.active_plot]['yl'].update_label( | ||||
|             abs_pos=pos, value=y | ||||
|         ) | ||||
| 
 | ||||
|         # Update x if cursor changed after discretization calc | ||||
|         # (this saves draw cycles on small mouse moves) | ||||
|         lastx = self._lastx | ||||
|         ix = round(x)  # since bars are centered around index | ||||
| 
 | ||||
|         if ix != lastx: | ||||
|             for plot, opts in self.graphics.items(): | ||||
| 
 | ||||
|                 # move the vertical line to the current "center of bar" | ||||
|                 opts['vl'].setX(ix) | ||||
| 
 | ||||
|                 # update the chart's "contents" label | ||||
|                 plot.update_contents_labels(ix) | ||||
| 
 | ||||
|                 # update all subscribed curve dots | ||||
|                 # first = plot._ohlc[0]['index'] | ||||
|                 for cursor in opts.get('cursors', ()): | ||||
|                     cursor.setIndex(ix) | ||||
| 
 | ||||
|             # update the label on the bottom of the crosshair | ||||
|             self.xaxis_label.update_label( | ||||
| 
 | ||||
|                 # XXX: requires: | ||||
|                 # https://github.com/pyqtgraph/pyqtgraph/pull/1418 | ||||
|                 # otherwise gobbles tons of CPU.. | ||||
| 
 | ||||
|                 # map back to abs (label-local) coordinates | ||||
|                 abs_pos=plot.mapFromView(QPointF(ix, y)), | ||||
|                 value=x, | ||||
|             ) | ||||
| 
 | ||||
|         self._lastx = ix | ||||
| 
 | ||||
|     def boundingRect(self): | ||||
|         try: | ||||
|             return self.active_plot.boundingRect() | ||||
|         except AttributeError: | ||||
|             return self.plots[0].boundingRect() | ||||
|  | @ -0,0 +1,244 @@ | |||
| # piker: trading gear for hackers | ||||
| # Copyright (C) 2018-present  Tyler Goodlet (in stewardship of piker0) | ||||
| 
 | ||||
| # This program is free software: you can redistribute it and/or modify | ||||
| # it under the terms of the GNU Affero General Public License as published by | ||||
| # the Free Software Foundation, either version 3 of the License, or | ||||
| # (at your option) any later version. | ||||
| 
 | ||||
| # This program is distributed in the hope that it will be useful, | ||||
| # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||
| # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | ||||
| # GNU Affero General Public License for more details. | ||||
| 
 | ||||
| # You should have received a copy of the GNU Affero General Public License | ||||
| # along with this program.  If not, see <https://www.gnu.org/licenses/>. | ||||
| 
 | ||||
| """ | ||||
| Lines for orders, alerts, L2. | ||||
| 
 | ||||
| """ | ||||
| from typing import Tuple | ||||
| 
 | ||||
| import pyqtgraph as pg | ||||
| from PyQt5 import QtCore, QtGui | ||||
| from PyQt5.QtCore import QPointF | ||||
| 
 | ||||
| from .._style import ( | ||||
|     hcolor, | ||||
|     _down_2_font_inches_we_like, | ||||
| ) | ||||
| from .._axes import YSticky | ||||
| 
 | ||||
| 
 | ||||
| class LevelLabel(YSticky): | ||||
| 
 | ||||
|     line_pen = pg.mkPen(hcolor('bracket')) | ||||
| 
 | ||||
|     _w_margin = 4 | ||||
|     _h_margin = 3 | ||||
|     level: float = 0 | ||||
| 
 | ||||
|     def __init__( | ||||
|         self, | ||||
|         chart, | ||||
|         *args, | ||||
|         orient_v: str = 'bottom', | ||||
|         orient_h: str = 'left', | ||||
|         **kwargs | ||||
|     ) -> None: | ||||
|         super().__init__(chart, *args, **kwargs) | ||||
| 
 | ||||
|         # orientation around axis options | ||||
|         self._orient_v = orient_v | ||||
|         self._orient_h = orient_h | ||||
|         self._v_shift = { | ||||
|             'top': 1., | ||||
|             'bottom': 0, | ||||
|             'middle': 1 / 2. | ||||
|         }[orient_v] | ||||
| 
 | ||||
|         self._h_shift = { | ||||
|             'left': -1., 'right': 0 | ||||
|         }[orient_h] | ||||
| 
 | ||||
|     def update_label( | ||||
|         self, | ||||
|         abs_pos: QPointF,  # scene coords | ||||
|         level: float,  # data for text | ||||
|         offset: int = 1  # if have margins, k? | ||||
|     ) -> None: | ||||
| 
 | ||||
|         # write contents, type specific | ||||
|         self.set_label_str(level) | ||||
| 
 | ||||
|         br = self.boundingRect() | ||||
|         h, w = br.height(), br.width() | ||||
| 
 | ||||
|         # this triggers ``.pain()`` implicitly? | ||||
|         self.setPos(QPointF( | ||||
|             self._h_shift * w - offset, | ||||
|             abs_pos.y() - (self._v_shift * h) - offset | ||||
|         )) | ||||
|         self.update() | ||||
| 
 | ||||
|         self.level = level | ||||
| 
 | ||||
|     def set_label_str(self, level: float): | ||||
|         # this is read inside ``.paint()`` | ||||
|         # self.label_str = '{size} x {level:.{digits}f}'.format( | ||||
|         self.label_str = '{level:.{digits}f}'.format( | ||||
|             # size=self._size, | ||||
|             digits=self.digits, | ||||
|             level=level | ||||
|         ).replace(',', ' ') | ||||
| 
 | ||||
|     def size_hint(self) -> Tuple[None, None]: | ||||
|         return None, None | ||||
| 
 | ||||
|     def draw( | ||||
|         self, | ||||
|         p: QtGui.QPainter, | ||||
|         rect: QtCore.QRectF | ||||
|     ) -> None: | ||||
|         p.setPen(self.line_pen) | ||||
| 
 | ||||
|         if self._orient_v == 'bottom': | ||||
|             lp, rp = rect.topLeft(), rect.topRight() | ||||
|             # p.drawLine(rect.topLeft(), rect.topRight()) | ||||
|         elif self._orient_v == 'top': | ||||
|             lp, rp = rect.bottomLeft(), rect.bottomRight() | ||||
| 
 | ||||
|         p.drawLine(lp.x(), lp.y(), rp.x(), rp.y()) | ||||
| 
 | ||||
| 
 | ||||
| class L1Label(LevelLabel): | ||||
| 
 | ||||
|     size: float = 0 | ||||
|     size_digits: float = 3 | ||||
| 
 | ||||
|     text_flags = ( | ||||
|         QtCore.Qt.TextDontClip | ||||
|         | QtCore.Qt.AlignLeft | ||||
|     ) | ||||
| 
 | ||||
|     def set_label_str(self, level: float) -> None: | ||||
|         """Reimplement the label string write to include the level's order-queue's | ||||
|         size in the text, eg. 100 x 323.3. | ||||
| 
 | ||||
|         """ | ||||
|         self.label_str = '{size:.{size_digits}f} x {level:,.{digits}f}'.format( | ||||
|             size_digits=self.size_digits, | ||||
|             size=self.size or '?', | ||||
|             digits=self.digits, | ||||
|             level=level | ||||
|         ).replace(',', ' ') | ||||
| 
 | ||||
| 
 | ||||
| class L1Labels: | ||||
|     """Level 1 bid ask labels for dynamic update on price-axis. | ||||
| 
 | ||||
|     """ | ||||
|     max_value: float = '100.0 x 100 000.00' | ||||
| 
 | ||||
|     def __init__( | ||||
|         self, | ||||
|         chart: 'ChartPlotWidget',  # noqa | ||||
|         digits: int = 2, | ||||
|         size_digits: int = 0, | ||||
|         font_size_inches: float = _down_2_font_inches_we_like, | ||||
|     ) -> None: | ||||
| 
 | ||||
|         self.chart = chart | ||||
| 
 | ||||
|         self.bid_label = L1Label( | ||||
|             chart=chart, | ||||
|             parent=chart.getAxis('right'), | ||||
|             # TODO: pass this from symbol data | ||||
|             digits=digits, | ||||
|             opacity=1, | ||||
|             font_size_inches=font_size_inches, | ||||
|             bg_color='papas_special', | ||||
|             fg_color='bracket', | ||||
|             orient_v='bottom', | ||||
|         ) | ||||
|         self.bid_label.size_digits = size_digits | ||||
|         self.bid_label._size_br_from_str(self.max_value) | ||||
| 
 | ||||
|         self.ask_label = L1Label( | ||||
|             chart=chart, | ||||
|             parent=chart.getAxis('right'), | ||||
|             # TODO: pass this from symbol data | ||||
|             digits=digits, | ||||
|             opacity=1, | ||||
|             font_size_inches=font_size_inches, | ||||
|             bg_color='papas_special', | ||||
|             fg_color='bracket', | ||||
|             orient_v='top', | ||||
|         ) | ||||
|         self.ask_label.size_digits = size_digits | ||||
|         self.ask_label._size_br_from_str(self.max_value) | ||||
| 
 | ||||
| 
 | ||||
| class LevelLine(pg.InfiniteLine): | ||||
|     def __init__( | ||||
|         self, | ||||
|         label: LevelLabel, | ||||
|         **kwargs, | ||||
|     ) -> None: | ||||
|         self.label = label | ||||
|         super().__init__(**kwargs) | ||||
|         self.sigPositionChanged.connect(self.set_level) | ||||
| 
 | ||||
|     def set_level(self, value: float) -> None: | ||||
|         self.label.update_from_data(0, self.value()) | ||||
| 
 | ||||
| 
 | ||||
| def level_line( | ||||
|     chart: 'ChartPlogWidget',  # noqa | ||||
|     level: float, | ||||
|     digits: int = 1, | ||||
| 
 | ||||
|     # size 4 font on 4k screen scaled down, so small-ish. | ||||
|     font_size_inches: float = _down_2_font_inches_we_like, | ||||
| 
 | ||||
|     show_label: bool = True, | ||||
| 
 | ||||
|     **linelabelkwargs | ||||
| ) -> LevelLine: | ||||
|     """Convenience routine to add a styled horizontal line to a plot. | ||||
| 
 | ||||
|     """ | ||||
|     label = LevelLabel( | ||||
|         chart=chart, | ||||
|         parent=chart.getAxis('right'), | ||||
|         # TODO: pass this from symbol data | ||||
|         digits=digits, | ||||
|         opacity=1, | ||||
|         font_size_inches=font_size_inches, | ||||
|         # TODO: make this take the view's bg pen | ||||
|         bg_color='papas_special', | ||||
|         fg_color='default', | ||||
|         **linelabelkwargs | ||||
|     ) | ||||
|     label.update_from_data(0, level) | ||||
| 
 | ||||
|     # TODO: can we somehow figure out a max value from the parent axis? | ||||
|     label._size_br_from_str(label.label_str) | ||||
| 
 | ||||
|     line = LevelLine( | ||||
|         label, | ||||
|         movable=True, | ||||
|         angle=0, | ||||
|     ) | ||||
|     line.setValue(level) | ||||
|     line.setPen(pg.mkPen(hcolor('default'))) | ||||
|     # activate/draw label | ||||
|     line.setValue(level) | ||||
| 
 | ||||
|     chart.plotItem.addItem(line) | ||||
| 
 | ||||
|     if not show_label: | ||||
|         label.hide() | ||||
| 
 | ||||
|     return line | ||||
|  | @ -15,15 +15,18 @@ | |||
| # along with this program.  If not, see <https://www.gnu.org/licenses/>. | ||||
| """ | ||||
| Super fast OHLC sampling graphics types. | ||||
| 
 | ||||
| """ | ||||
| from typing import List, Optional, Tuple | ||||
| 
 | ||||
| import numpy as np | ||||
| import pyqtgraph as pg | ||||
| from numba import jit, float64, int64  # , optional | ||||
| # from numba import types as ntypes | ||||
| from PyQt5 import QtCore, QtGui | ||||
| from PyQt5.QtCore import QLineF, QPointF | ||||
| # from numba import types as ntypes | ||||
| # from .._profile import timeit | ||||
| # from ..data._source import numba_ohlc_dtype | ||||
| 
 | ||||
| from .._style import hcolor | ||||
| 
 | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue