diff --git a/piker/ui/_chart.py b/piker/ui/_chart.py index 1718c4ee..80c9b369 100644 --- a/piker/ui/_chart.py +++ b/piker/ui/_chart.py @@ -2,6 +2,7 @@ High level Qt chart widgets. """ from typing import Tuple, Dict, Any, Optional +from functools import partial from PyQt5 import QtCore, QtGui import numpy as np @@ -15,6 +16,7 @@ from ._axes import ( ) from ._graphics import ( CrossHair, + ContentsLabel, BarItems, h_line, ) @@ -27,7 +29,6 @@ from ._style import ( _min_points_to_show, _bars_from_right_in_follow_mode, _bars_to_left_in_follow_mode, - _font, ) from ..data._source import Symbol from .. import brokers @@ -326,7 +327,7 @@ class ChartPlotWidget(pg.PlotWidget): self.showAxis('right') # show background grid - self.showGrid(x=True, y=True, alpha=0.4) + self.showGrid(x=True, y=True, alpha=0.5) # use cross-hair for cursor? # self.setCursor(QtCore.Qt.CrossCursor) @@ -344,10 +345,12 @@ class ChartPlotWidget(pg.PlotWidget): def last_bar_in_view(self) -> bool: self._array[-1]['index'] - def _update_contents_label(self, index: int) -> None: + def update_contents_labels(self, index: int) -> None: if index >= 0 and index < len(self._array): + array = self._array + for name, (label, update) in self._labels.items(): - update(index) + update(index, array) def _set_xlimits( self, @@ -375,54 +378,6 @@ class ChartPlotWidget(pg.PlotWidget): rbar = min(r, len(self._array)) return l, lbar, rbar, r - def draw_ohlc( - self, - name: str, - data: np.ndarray, - # XXX: pretty sure this is dumb and we don't need an Enum - style: pg.GraphicsObject = BarItems, - ) -> pg.GraphicsObject: - """Draw OHLC datums to chart. - """ - graphics = style(self.plotItem) - # adds all bar/candle graphics objects for each data point in - # the np array buffer to be drawn on next render cycle - self.addItem(graphics) - - # draw after to allow self.scene() to work... - graphics.draw_from_data(data) - - self._graphics[name] = graphics - - # XXX: How to stack labels vertically? - # Ogi says: "use ..." - label = pg.LabelItem( - justify='left', - size=f'{_font.pixelSize()}px', - ) - label.setParentItem(self._vb) - self.scene().addItem(label) - - # keep close to top - label.anchor(itemPos=(0, 0), parentPos=(0, 0), offset=(0, -4)) - - def update(index: int) -> None: - label.setText( - "{name}[{index}] -> O:{} H:{} L:{} C:{} V:{}".format( - *self._array[index].item()[2:8], - name=name, - index=index, - ) - ) - - self._labels[name] = (label, update) - self._update_contents_label(len(data) - 1) - label.show() - - self._add_sticky(name) - - return graphics - def default_view( self, index: int = -1, @@ -456,6 +411,34 @@ class ChartPlotWidget(pg.PlotWidget): padding=0, ) + def draw_ohlc( + self, + name: str, + data: np.ndarray, + # XXX: pretty sure this is dumb and we don't need an Enum + style: pg.GraphicsObject = BarItems, + ) -> pg.GraphicsObject: + """Draw OHLC datums to chart. + """ + graphics = style(self.plotItem) + # adds all bar/candle graphics objects for each data point in + # the np array buffer to be drawn on next render cycle + self.addItem(graphics) + + # draw after to allow self.scene() to work... + graphics.draw_from_data(data) + + self._graphics[name] = graphics + + label = ContentsLabel(chart=self, anchor_at=('top', 'left')) + self._labels[name] = (label, partial(label.update_from_ohlc, name)) + label.show() + self.update_contents_labels(len(data) - 1) + + self._add_sticky(name) + + return graphics + def draw_curve( self, name: str, @@ -482,37 +465,21 @@ class ChartPlotWidget(pg.PlotWidget): # register overlay curve with name self._graphics[name] = curve - # XXX: How to stack labels vertically? - label = pg.LabelItem( - justify='left', - size=f'{_font.pixelSize()}px', - ) - - # anchor to the viewbox - label.setParentItem(self._vb) - # label.setParentItem(self.getPlotItem()) - if overlay: - # position bottom left if an overlay - label.anchor(itemPos=(1, 1), parentPos=(1, 1), offset=(0, 3)) + anchor_at = ('bottom', 'right') self._overlays[name] = curve else: - label.anchor(itemPos=(0, 0), parentPos=(0, 0), offset=(0, -4)) + anchor_at = ('top', 'right') # TODO: something instead of stickies for overlays # (we need something that avoids clutter on x-axis). self._add_sticky(name) - def update(index: int) -> None: - data = self._array[index][name] - label.setText(f"{name}: {data:.2f}") - + label = ContentsLabel(chart=self, anchor_at=anchor_at) + self._labels[name] = (label, partial(label.update_from_value, name)) label.show() - self.scene().addItem(label) - - self._labels[name] = (label, update) - self._update_contents_label(len(data) - 1) + self.update_contents_labels(len(data) - 1) if self._cursor: self._cursor.add_curve_cursor(self, curve) @@ -909,7 +876,7 @@ async def chart_from_fsp( ) # display contents labels asap - chart._update_contents_label(len(shm.array) - 1) + chart.update_contents_labels(len(shm.array) - 1) array = shm.array value = array[fsp_func_name][-1] diff --git a/piker/ui/_graphics.py b/piker/ui/_graphics.py index 19532693..88089812 100644 --- a/piker/ui/_graphics.py +++ b/piker/ui/_graphics.py @@ -6,10 +6,11 @@ from typing import List import numpy as np import pyqtgraph as pg +# from numba import jit, float64, optional, int64 from PyQt5 import QtCore, QtGui from PyQt5.QtCore import QLineF, QPointF -# from .quantdom.utils import timeit +# from .._profile import timeit from ._style import _xaxis_at, hcolor, _font from ._axes import YAxisLabel, XAxisLabel @@ -56,6 +57,78 @@ class LineDot(pg.CurvePoint): self.setFlag(self.ItemIgnoresTransformations) +_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, 5), + ('bottom', 'right'): (4, 5), +} + + +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', + size: str = f'{_font.pixelSize()}px', + ) -> None: + + super().__init__(justify=justify_text, size=size) + + # 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)] + + 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: + self.setText( + "i:{index}
" + "O:{}
" + "H:{}
" + "L:{}
" + "C:{}
" + "V:{}".format( + # *self._array[index].item()[2:8], + *array[index].item()[2:8], + name=name, + index=index, + ) + ) + + def update_from_value( + self, + name: str, + index: int, + array: np.ndarray, + ) -> None: + data = array[index][name] + self.setText(f"{name}: {data:.2f}") + + class CrossHair(pg.GraphicsObject): def __init__( @@ -208,7 +281,7 @@ class CrossHair(pg.GraphicsObject): opts['vl'].setX(ix) # update the chart's "contents" label - plot._update_contents_label(ix) + plot.update_contents_labels(ix) # update all subscribed curve dots for cursor in opts.get('cursors', ()): @@ -235,6 +308,15 @@ class CrossHair(pg.GraphicsObject): return self.plots[0].boundingRect() +# @jit( +# # float64[:]( +# # float64[:], +# # optional(float64), +# # optional(int16) +# # ), +# nopython=True, +# nogil=True +# ) def _mk_lines_array(data: List, size: int) -> np.ndarray: """Create an ndarray to hold lines graphics objects. """ @@ -246,6 +328,16 @@ def _mk_lines_array(data: List, size: int) -> np.ndarray: # TODO: `numba` this? + +# @jit( +# # float64[:]( +# # float64[:], +# # optional(float64), +# # optional(int16) +# # ), +# nopython=True, +# nogil=True +# ) def bars_from_ohlc( data: np.ndarray, w: float,