diff --git a/piker/__init__.py b/piker/__init__.py
index 92553306..75ec8ded 100644
--- a/piker/__init__.py
+++ b/piker/__init__.py
@@ -1,5 +1,5 @@
# piker: trading gear for hackers.
-# Copyright 2018 Tyler Goodlet
+# Copyright 2020-eternity Tyler Goodlet (in stewardship for 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
@@ -16,6 +16,7 @@
"""
piker: trading gear for hackers.
+
"""
import msgpack # noqa
import msgpack_numpy
diff --git a/piker/_profile.py b/piker/_profile.py
index a6f171c1..fa38d065 100644
--- a/piker/_profile.py
+++ b/piker/_profile.py
@@ -16,10 +16,18 @@
"""
Profiling wrappers for internal libs.
+
"""
import time
from functools import wraps
+_pg_profile: bool = False
+
+
+def pg_profile_enabled() -> bool:
+ global _pg_profile
+ return _pg_profile
+
def timeit(fn):
@wraps(fn)
diff --git a/piker/brokers/ib.py b/piker/brokers/ib.py
index 34f2b17d..e6e610d4 100644
--- a/piker/brokers/ib.py
+++ b/piker/brokers/ib.py
@@ -622,8 +622,7 @@ async def fill_bars(
sym: str,
first_bars: list,
shm: 'ShmArray', # type: ignore # noqa
- count: int = 21,
- # count: int = 1,
+ count: int = 21, # NOTE: any more and we'll overrun the underlying buffer
) -> None:
"""Fill historical bars into shared mem / storage afap.
diff --git a/piker/ui/_axes.py b/piker/ui/_axes.py
index d2bac3cd..08d2e1b5 100644
--- a/piker/ui/_axes.py
+++ b/piker/ui/_axes.py
@@ -22,7 +22,7 @@ from typing import List, Tuple, Optional
import pandas as pd
import pyqtgraph as pg
-from PyQt5 import QtCore, QtGui
+from PyQt5 import QtCore, QtGui, QtWidgets
from PyQt5.QtCore import QPointF
from ._style import DpiAwareFont, hcolor, _font
@@ -44,6 +44,10 @@ class Axis(pg.AxisItem):
) -> None:
super().__init__(**kwargs)
+
+ # XXX: pretty sure this makes things slower
+ # self.setCacheMode(QtGui.QGraphicsItem.DeviceCoordinateCache)
+
self.linked_charts = linked_charts
self._min_tick = min_tick
@@ -158,9 +162,12 @@ class AxisLabel(pg.GraphicsObject):
fg_color: str = 'black',
opacity: int = 0,
font_size_inches: Optional[float] = None,
- ):
+ ) -> None:
+
super().__init__(parent)
self.setFlag(self.ItemIgnoresTransformations)
+ # XXX: pretty sure this is faster
+ self.setCacheMode(QtGui.QGraphicsItem.DeviceCoordinateCache)
self.parent = parent
self.opacity = opacity
@@ -177,7 +184,12 @@ class AxisLabel(pg.GraphicsObject):
self.rect = None
- def paint(self, p, option, widget):
+ def paint(
+ self,
+ p: QtGui.QPainter,
+ opt: QtWidgets.QStyleOptionGraphicsItem,
+ w: QtWidgets.QWidget
+ ) -> None:
# p.setCompositionMode(QtGui.QPainter.CompositionMode_SourceOver)
if self.label_str:
diff --git a/piker/ui/_chart.py b/piker/ui/_chart.py
index 7ba23f5b..92eed5d2 100644
--- a/piker/ui/_chart.py
+++ b/piker/ui/_chart.py
@@ -30,13 +30,16 @@ from ._axes import (
DynamicDateAxis,
PriceAxis,
)
-from ._graphics import (
+from ._graphics._cursor import (
CrossHair,
ContentsLabel,
- BarItems,
+)
+from ._graphics._lines import (
level_line,
L1Labels,
)
+from ._graphics._ohlc import BarItems
+from ._graphics._curve import FastAppendCurve
from ._axes import YSticky
from ._style import (
_font,
@@ -550,20 +553,37 @@ class ChartPlotWidget(pg.PlotWidget):
}
pdi_kwargs.update(_pdi_defaults)
- curve = pg.PlotDataItem(
+ # curve = pg.PlotDataItem(
+ # curve = pg.PlotCurveItem(
+ curve = FastAppendCurve(
y=data[name],
x=data['index'],
# antialias=True,
name=name,
+ # XXX: pretty sure this is just more overhead
+ # on data reads and makes graphics rendering no faster
+ # clipToView=True,
+
# TODO: see how this handles with custom ohlcv bars graphics
# and/or if we can implement something similar for OHLC graphics
- # clipToView=True,
- autoDownsample=True,
- downsampleMethod='subsample',
+ # autoDownsample=True,
+ # downsample=60,
+ # downsampleMethod='subsample',
**pdi_kwargs,
)
+
+ # XXX: see explanation for differenct caching modes:
+ # https://stackoverflow.com/a/39410081
+ # seems to only be useful if we don't re-generate the entire
+ # QPainterPath every time
+ # curve.curve.setCacheMode(QtGui.QGraphicsItem.DeviceCoordinateCache)
+
+ # don't ever use this - it's a colossal nightmare of artefacts
+ # and is disastrous for performance.
+ # curve.setCacheMode(QtGui.QGraphicsItem.ItemCoordinateCache)
+
self.addItem(curve)
# register curve graphics and backing array for name
@@ -647,6 +667,7 @@ class ChartPlotWidget(pg.PlotWidget):
"""Update the named internal graphics from ``array``.
"""
+
if name not in self._overlays:
self._ohlc = array
else:
@@ -654,11 +675,13 @@ class ChartPlotWidget(pg.PlotWidget):
curve = self._graphics[name]
- # TODO: we should instead implement a diff based
- # "only update with new items" on the pg.PlotCurveItem
- # one place to dig around this might be the `QBackingStore`
- # https://doc.qt.io/qt-5/qbackingstore.html
- curve.setData(y=array[name], x=array['index'], **kwargs)
+ if len(array):
+ # TODO: we should instead implement a diff based
+ # "only update with new items" on the pg.PlotCurveItem
+ # one place to dig around this might be the `QBackingStore`
+ # https://doc.qt.io/qt-5/qbackingstore.html
+ # curve.setData(y=array[name], x=array['index'], **kwargs)
+ curve.update_from_array(x=array['index'], y=array[name], **kwargs)
return curve
@@ -689,13 +712,15 @@ class ChartPlotWidget(pg.PlotWidget):
# figure out x-range in view such that user can scroll "off"
# the data set up to the point where ``_min_points_to_show``
# are left.
- view_len = r - l
+ # view_len = r - l
# TODO: logic to check if end of bars in view
- extra = view_len - _min_points_to_show
- begin = self._ohlc[0]['index'] - extra
- # end = len(self._ohlc) - 1 + extra
- end = self._ohlc[-1]['index'] - 1 + extra
+ # extra = view_len - _min_points_to_show
+
+ # begin = self._ohlc[0]['index'] - extra
+
+ # # end = len(self._ohlc) - 1 + extra
+ # end = self._ohlc[-1]['index'] - 1 + extra
# XXX: test code for only rendering lines for the bars in view.
# This turns out to be very very poor perf when scaling out to
@@ -862,11 +887,11 @@ async def _async_main(
" dropping volume signals")
else:
fsp_conf.update({
- 'vwap': {
- 'overlay': True,
- 'anchor': 'session',
- },
- })
+ 'vwap': {
+ 'overlay': True,
+ 'anchor': 'session',
+ },
+ })
async with trio.open_nursery() as n:
@@ -1292,11 +1317,11 @@ async def check_for_new_bars(feed, ohlcv, linked_charts):
# current bar) and then either write the current bar manually
# or place a cursor for visual cue of the current time step.
- price_chart.update_ohlc_from_array(
- price_chart.name,
- ohlcv.array,
- just_history=True,
- )
+ # price_chart.update_ohlc_from_array(
+ # price_chart.name,
+ # ohlcv.array,
+ # just_history=True,
+ # )
# XXX: this puts a flat bar on the current time step
# TODO: if we eventually have an x-axis time-step "cursor"
diff --git a/piker/ui/_exec.py b/piker/ui/_exec.py
index bbb3633a..30a93e04 100644
--- a/piker/ui/_exec.py
+++ b/piker/ui/_exec.py
@@ -83,8 +83,7 @@ class MainWindow(QtGui.QMainWindow):
"""Cancel the root actor asap.
"""
- # raising KBI seems to get intercepted by by Qt so just use the
- # system.
+ # raising KBI seems to get intercepted by by Qt so just use the system.
os.kill(os.getpid(), signal.SIGINT)
@@ -160,20 +159,20 @@ def run_qtractor(
'main': instance,
}
- # setup tractor entry point args
- main = partial(
- tractor._main,
- async_fn=func,
- args=args + (widgets,),
- arbiter_addr=(
- tractor._default_arbiter_host,
- tractor._default_arbiter_port,
- ),
- name='qtractor',
- **tractor_kwargs,
- )
+ # define tractor entrypoint
+ async def main():
- # guest mode
+ async with tractor.open_root_actor(
+ arbiter_addr=(
+ tractor._root._default_arbiter_host,
+ tractor._root._default_arbiter_port,
+ ),
+ name='qtractor',
+ **tractor_kwargs,
+ ) as a:
+ await func(*(args + (widgets,)))
+
+ # guest mode entry
trio.lowlevel.start_guest_run(
main,
run_sync_soon_threadsafe=run_sync_soon_threadsafe,
diff --git a/piker/ui/_graphics.py b/piker/ui/_graphics.py
deleted file mode 100644
index 6e968060..00000000
--- a/piker/ui/_graphics.py
+++ /dev/null
@@ -1,997 +0,0 @@
-# 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 .
-
-"""
-Chart graphics for displaying a slew of different data types.
-"""
-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(
- "i:{index}
"
- "O:{}
"
- "H:{}
"
- "L:{}
"
- "C:{}
"
- "V:{}
"
- "wap:{}".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
-
- 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)
-
- hl = plot.addLine(y=0, pen=self.lines_pen, movable=False)
- hl.hide()
-
- yl = YAxisLabel(
- parent=plot.getAxis('right'),
- digits=digits or self.digits,
- opacity=_ch_label_opac,
- bg_color='default',
- )
- 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)))
-
- 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) # - first)
-
- # 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()
-
-
-def _mk_lines_array(
- data: List,
- size: int,
- elements_step: int = 6,
-) -> np.ndarray:
- """Create an ndarray to hold lines graphics info.
-
- """
- return np.zeros_like(
- data,
- shape=(int(size), elements_step),
- dtype=object,
- )
-
-
-def lines_from_ohlc(row: np.ndarray, w: float) -> Tuple[QLineF]:
- open, high, low, close, index = row[
- ['open', 'high', 'low', 'close', 'index']]
-
- # high -> low vertical (body) line
- if low != high:
- hl = QLineF(index, low, index, high)
- else:
- # XXX: if we don't do it renders a weird rectangle?
- # see below for filtering this later...
- hl = None
-
- # NOTE: place the x-coord start as "middle" of the drawing range such
- # that the open arm line-graphic is at the left-most-side of
- # the index's range according to the view mapping.
-
- # open line
- o = QLineF(index - w, open, index, open)
- # close line
- c = QLineF(index, close, index + w, close)
-
- return [hl, o, c]
-
-
-@jit(
- # TODO: for now need to construct this manually for readonly arrays, see
- # https://github.com/numba/numba/issues/4511
- # ntypes.Tuple((float64[:], float64[:], float64[:]))(
- # numba_ohlc_dtype[::1], # contiguous
- # int64,
- # optional(float64),
- # ),
- nopython=True,
- nogil=True
-)
-def path_arrays_from_ohlc(
- data: np.ndarray,
- start: int64,
- bar_gap: float64 = 0.43,
-) -> np.ndarray:
- """Generate an array of lines objects from input ohlc data.
-
- """
- size = int(data.shape[0] * 6)
-
- x = np.zeros(
- # data,
- shape=size,
- dtype=float64,
- )
- y, c = x.copy(), x.copy()
-
- # TODO: report bug for assert @
- # /home/goodboy/repos/piker/env/lib/python3.8/site-packages/numba/core/typing/builtins.py:991
- for i, q in enumerate(data[start:], start):
-
- # TODO: ask numba why this doesn't work..
- # open, high, low, close, index = q[
- # ['open', 'high', 'low', 'close', 'index']]
-
- open = q['open']
- high = q['high']
- low = q['low']
- close = q['close']
- index = float64(q['index'])
-
- istart = i * 6
- istop = istart + 6
-
- # x,y detail the 6 points which connect all vertexes of a ohlc bar
- x[istart:istop] = (
- index - bar_gap,
- index,
- index,
- index,
- index,
- index + bar_gap,
- )
- y[istart:istop] = (
- open,
- open,
- low,
- high,
- close,
- close,
- )
-
- # specifies that the first edge is never connected to the
- # prior bars last edge thus providing a small "gap"/"space"
- # between bars determined by ``bar_gap``.
- c[istart:istop] = (0, 1, 1, 1, 1, 1)
-
- return x, y, c
-
-
-# @timeit
-def gen_qpath(
- data,
- start, # XXX: do we need this?
- w,
-) -> QtGui.QPainterPath:
-
- x, y, c = path_arrays_from_ohlc(data, start, bar_gap=w)
-
- # TODO: numba the internals of this!
- return pg.functions.arrayToQPath(x, y, connect=c)
-
-
-class BarItems(pg.GraphicsObject):
- """Price range bars graphics rendered from a OHLC sequence.
- """
- sigPlotChanged = QtCore.Signal(object)
-
- # 0.5 is no overlap between arms, 1.0 is full overlap
- w: float = 0.43
-
- # XXX: for the mega-lulz increasing width here increases draw latency...
- # so probably don't do it until we figure that out.
- bars_pen = pg.mkPen(hcolor('bracket'))
-
- def __init__(
- self,
- # scene: 'QGraphicsScene', # noqa
- plotitem: 'pg.PlotItem', # noqa
- ) -> None:
- super().__init__()
-
- self.last_bar = QtGui.QPicture()
-
- self.path = QtGui.QPainterPath()
- # self._h_path = QtGui.QGraphicsPathItem(self.path)
-
- self._pi = plotitem
-
- self._xrange: Tuple[int, int]
- self._yrange: Tuple[float, float]
-
- # XXX: not sure this actually needs to be an array other
- # then for the old tina mode calcs for up/down bars below?
- # lines container
- # self.lines = _mk_lines_array([], 50e3, 6)
-
- # TODO: don't render the full backing array each time
- # self._path_data = None
- self._last_bar_lines: Optional[Tuple[QLineF, ...]] = None
-
- # track the current length of drawable lines within the larger array
- self.start_index: int = 0
- self.stop_index: int = 0
-
- # @timeit
- def draw_from_data(
- self,
- data: np.ndarray,
- start: int = 0,
- ) -> QtGui.QPainterPath:
- """Draw OHLC datum graphics from a ``np.ndarray``.
-
- This routine is usually only called to draw the initial history.
- """
- self.path = gen_qpath(data, start, self.w)
-
- # save graphics for later reference and keep track
- # of current internal "last index"
- # self.start_index = len(data)
- index = data['index']
- self._xrange = (index[0], index[-1])
- self._yrange = (
- np.nanmax(data['high']),
- np.nanmin(data['low']),
- )
-
- # up to last to avoid double draw of last bar
- self._last_bar_lines = lines_from_ohlc(data[-1], self.w)
-
- # create pics
- # self.draw_history()
- self.draw_last_bar()
-
- # trigger render
- # https://doc.qt.io/qt-5/qgraphicsitem.html#update
- self.update()
-
- return self.path
-
- # def update_ranges(
- # self,
- # xmn: int,
- # xmx: int,
- # ymn: float,
- # ymx: float,
- # ) -> None:
- # ...
-
-
- def draw_last_bar(self) -> None:
- """Currently this draws lines to a cached ``QPicture`` which
- is supposed to speed things up on ``.paint()`` calls (which
- is a call to ``QPainter.drawPicture()`` but I'm not so sure.
-
- """
- p = QtGui.QPainter(self.last_bar)
- p.setPen(self.bars_pen)
- p.drawLines(*tuple(filter(bool, self._last_bar_lines)))
- p.end()
-
- # @timeit
- def update_from_array(
- self,
- array: np.ndarray,
- just_history=False,
- ) -> None:
- """Update the last datum's bar graphic from input data array.
-
- This routine should be interface compatible with
- ``pg.PlotCurveItem.setData()``. Normally this method in
- ``pyqtgraph`` seems to update all the data passed to the
- graphics object, and then update/rerender, but here we're
- assuming the prior graphics havent changed (OHLC history rarely
- does) so this "should" be simpler and faster.
-
- This routine should be made (transitively) as fast as possible.
- """
- # index = self.start_index
- istart, istop = self._xrange
-
- index = array['index']
- first_index, last_index = index[0], index[-1]
-
- # length = len(array)
- prepend_length = istart - first_index
- append_length = last_index - istop
-
- # TODO: allow mapping only a range of lines thus
- # only drawing as many bars as exactly specified.
-
- if prepend_length:
-
- # new history was added and we need to render a new path
- new_bars = array[:prepend_length]
- prepend_path = gen_qpath(new_bars, 0, self.w)
-
- # XXX: SOMETHING IS MAYBE FISHY HERE what with the old_path
- # y value not matching the first value from
- # array[prepend_length + 1] ???
-
- # update path
- old_path = self.path
- self.path = prepend_path
- self.path.addPath(old_path)
-
- if append_length:
- # generate new lines objects for updatable "current bar"
- self._last_bar_lines = lines_from_ohlc(array[-1], self.w)
- self.draw_last_bar()
-
- # generate new graphics to match provided array
- # path appending logic:
- # we need to get the previous "current bar(s)" for the time step
- # and convert it to a sub-path to append to the historical set
- # new_bars = array[istop - 1:istop + append_length - 1]
- new_bars = array[-append_length - 1:-1]
- append_path = gen_qpath(new_bars, 0, self.w)
- self.path.moveTo(float(istop - self.w), float(new_bars[0]['open']))
- self.path.addPath(append_path)
-
- self._xrange = first_index, last_index
-
- if just_history:
- self.update()
- return
-
- # last bar update
- i, o, h, l, last, v = array[-1][
- ['index', 'open', 'high', 'low', 'close', 'volume']
- ]
- # assert i == self.start_index - 1
- assert i == last_index
- body, larm, rarm = self._last_bar_lines
-
- # XXX: is there a faster way to modify this?
- rarm.setLine(rarm.x1(), last, rarm.x2(), last)
- # writer is responsible for changing open on "first" volume of bar
- larm.setLine(larm.x1(), o, larm.x2(), o)
-
- if l != h: # noqa
- if body is None:
- body = self._last_bar_lines[0] = QLineF(i, l, i, h)
- else:
- # update body
- body.setLine(i, l, i, h)
-
- # XXX: pretty sure this is causing an issue where the bar has
- # a large upward move right before the next sample and the body
- # is getting set to None since the next bar is flat but the shm
- # array index update wasn't read by the time this code runs. Iow
- # we're doing this removal of the body for a bar index that is
- # now out of date / from some previous sample. It's weird
- # though because i've seen it do this to bars i - 3 back?
-
- # else:
- # # XXX: h == l -> remove any HL line to avoid render bug
- # if body is not None:
- # body = self.lines[index - 1][0] = None
-
- self.draw_last_bar()
- self.update()
-
- # @timeit
- def paint(self, p, opt, widget):
-
- # profiler = pg.debug.Profiler(disabled=False, delayed=False)
-
- # TODO: use to avoid drawing artefacts?
- # self.prepareGeometryChange()
-
- # p.setCompositionMode(0)
-
- # TODO: one thing we could try here is pictures being drawn of
- # a fixed count of bars such that based on the viewbox indices we
- # only draw the "rounded up" number of "pictures worth" of bars
- # as is necesarry for what's in "view". Not sure if this will
- # lead to any perf gains other then when zoomed in to less bars
- # in view.
- p.drawPicture(0, 0, self.last_bar)
-
- p.setPen(self.bars_pen)
- p.drawPath(self.path)
-
- # @timeit
- def boundingRect(self):
- # Qt docs: https://doc.qt.io/qt-5/qgraphicsitem.html#boundingRect
-
- # TODO: Can we do rect caching to make this faster
- # like `pg.PlotCurveItem` does? In theory it's just
- # computing max/min stuff again like we do in the udpate loop
- # anyway. Not really sure it's necessary since profiling already
- # shows this method is faf.
-
- # boundingRect _must_ indicate the entire area that will be
- # drawn on or else we will get artifacts and possibly crashing.
- # (in this case, QPicture does all the work of computing the
- # bounding rect for us).
-
- # compute aggregate bounding rectangle
- lb = self.last_bar.boundingRect()
- hb = self.path.boundingRect()
-
- return QtCore.QRectF(
- # top left
- QtCore.QPointF(hb.topLeft()),
- # total size
- QtCore.QSizeF(QtCore.QSizeF(lb.size()) + hb.size())
- # QtCore.QSizeF(lb.size() + hb.size())
- )
-
-
-# XXX: when we get back to enabling tina mode for xb
-# class CandlestickItems(BarItems):
-
-# w2 = 0.7
-# line_pen = pg.mkPen('#000000')
-# bull_brush = pg.mkBrush('#00ff00')
-# bear_brush = pg.mkBrush('#ff0000')
-
-# def _generate(self, p):
-# rects = np.array(
-# [
-# QtCore.QRectF(
-# q.id - self.w,
-# q.open,
-# self.w2,
-# q.close - q.open
-# )
-# for q in Quotes
-# ]
-# )
-
-# p.setPen(self.line_pen)
-# p.drawLines(
-# [QtCore.QLineF(q.id, q.low, q.id, q.high)
-# for q in Quotes]
-# )
-
-# p.setBrush(self.bull_brush)
-# p.drawRects(*rects[Quotes.close > Quotes.open])
-
-# p.setBrush(self.bear_brush)
-# p.drawRects(*rects[Quotes.close < Quotes.open])
-
-
-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
diff --git a/piker/ui/_graphics/__init__.py b/piker/ui/_graphics/__init__.py
new file mode 100644
index 00000000..2846367a
--- /dev/null
+++ b/piker/ui/_graphics/__init__.py
@@ -0,0 +1,20 @@
+# 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 .
+
+"""
+Internal custom graphics mostly built for low latency and reuse.
+
+"""
diff --git a/piker/ui/_graphics/_cursor.py b/piker/ui/_graphics/_cursor.py
new file mode 100644
index 00000000..83f0ee96
--- /dev/null
+++ b/piker/ui/_graphics/_cursor.py
@@ -0,0 +1,376 @@
+# 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 .
+"""
+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(
+ "i:{index}
"
+ "O:{}
"
+ "H:{}
"
+ "L:{}
"
+ "C:{}
"
+ "V:{}
"
+ "wap:{}".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
+
+ 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.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)))
+
+ 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()
diff --git a/piker/ui/_graphics/_curve.py b/piker/ui/_graphics/_curve.py
new file mode 100644
index 00000000..7bf39cea
--- /dev/null
+++ b/piker/ui/_graphics/_curve.py
@@ -0,0 +1,158 @@
+# 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 .
+
+"""
+Fast, smooth, sexy curves.
+"""
+from typing import Tuple
+
+import pyqtgraph as pg
+from PyQt5 import QtCore, QtGui, QtWidgets
+
+from ..._profile import pg_profile_enabled
+
+
+# TODO: got a feeling that dropping this inheritance gets us even more speedups
+class FastAppendCurve(pg.PlotCurveItem):
+
+ def __init__(self, *args, **kwargs):
+
+ # TODO: we can probably just dispense with the parent since
+ # we're basically only using the pen setting now...
+ super().__init__(*args, **kwargs)
+
+ self._last_line: QtCore.QLineF = None
+ self._xrange: Tuple[int, int] = self.dataBounds(ax=0)
+
+ # TODO: one question still remaining is if this makes trasform
+ # interactions slower (such as zooming) and if so maybe if/when
+ # we implement a "history" mode for the view we disable this in
+ # that mode?
+ self.setCacheMode(QtGui.QGraphicsItem.DeviceCoordinateCache)
+
+ def update_from_array(
+ self,
+ x,
+ y,
+ ) -> QtGui.QPainterPath:
+
+ profiler = pg.debug.Profiler(disabled=not pg_profile_enabled())
+ flip_cache = False
+
+ # print(f"xrange: {self._xrange}")
+ istart, istop = self._xrange
+
+ prepend_length = istart - x[0]
+ append_length = x[-1] - istop
+
+ if self.path is None or prepend_length:
+ self.path = pg.functions.arrayToQPath(
+ x[:-1],
+ y[:-1],
+ connect='all'
+ )
+ profiler('generate fresh path')
+
+ # TODO: get this working - right now it's giving heck on vwap...
+ # if prepend_length:
+ # breakpoint()
+
+ # prepend_path = pg.functions.arrayToQPath(
+ # x[0:prepend_length],
+ # y[0:prepend_length],
+ # connect='all'
+ # )
+
+ # # swap prepend path in "front"
+ # old_path = self.path
+ # self.path = prepend_path
+ # # self.path.moveTo(new_x[0], new_y[0])
+ # self.path.connectPath(old_path)
+
+ if append_length:
+ # print(f"append_length: {append_length}")
+ new_x = x[-append_length - 2:-1]
+ new_y = y[-append_length - 2:-1]
+ # print((new_x, new_y))
+
+ append_path = pg.functions.arrayToQPath(
+ new_x,
+ new_y,
+ connect='all'
+ )
+ # print(f"append_path br: {append_path.boundingRect()}")
+ # self.path.moveTo(new_x[0], new_y[0])
+ # self.path.connectPath(append_path)
+ self.path.connectPath(append_path)
+
+ # XXX: pretty annoying but, without this there's little
+ # artefacts on the append updates to the curve...
+ self.setCacheMode(QtGui.QGraphicsItem.NoCache)
+ self.prepareGeometryChange()
+ flip_cache = True
+
+ # print(f"update br: {self.path.boundingRect()}")
+
+ # XXX: lol brutal, the internals of `CurvePoint` (inherited by
+ # our `LineDot`) required ``.getData()`` to work..
+ self.xData = x
+ self.yData = y
+
+ self._xrange = x[0], x[-1]
+ self._last_line = QtCore.QLineF(x[-2], y[-2], x[-1], y[-1])
+
+ # trigger redraw of path
+ # do update before reverting to cache mode
+ self.prepareGeometryChange()
+ self.update()
+
+ if flip_cache:
+ self.setCacheMode(QtGui.QGraphicsItem.DeviceCoordinateCache)
+
+ def boundingRect(self):
+ hb = self.path.controlPointRect()
+ hb_size = hb.size()
+ # print(f'hb_size: {hb_size}')
+
+ w = hb_size.width() + 1
+ h = hb_size.height() + 1
+ br = QtCore.QRectF(
+
+ # top left
+ QtCore.QPointF(hb.topLeft()),
+
+ # total size
+ QtCore.QSizeF(w, h)
+ )
+ # print(f'bounding rect: {br}')
+ return br
+
+ def paint(
+ self,
+ p: QtGui.QPainter,
+ opt: QtWidgets.QStyleOptionGraphicsItem,
+ w: QtWidgets.QWidget
+ ) -> None:
+
+ profiler = pg.debug.Profiler(disabled=not pg_profile_enabled())
+ # p.setRenderHint(p.Antialiasing, True)
+
+ p.setPen(self.opts['pen'])
+ p.drawLine(self._last_line)
+ profiler('.drawLine()')
+
+ p.drawPath(self.path)
+ profiler('.drawPath()')
diff --git a/piker/ui/_graphics/_lines.py b/piker/ui/_graphics/_lines.py
new file mode 100644
index 00000000..bd5b9de6
--- /dev/null
+++ b/piker/ui/_graphics/_lines.py
@@ -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 .
+
+"""
+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
diff --git a/piker/ui/_graphics/_ohlc.py b/piker/ui/_graphics/_ohlc.py
new file mode 100644
index 00000000..0be7853f
--- /dev/null
+++ b/piker/ui/_graphics/_ohlc.py
@@ -0,0 +1,432 @@
+# 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 .
+"""
+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 PyQt5 import QtCore, QtGui, QtWidgets
+from PyQt5.QtCore import QLineF, QPointF
+# from numba import types as ntypes
+# from ..data._source import numba_ohlc_dtype
+
+from ..._profile import pg_profile_enabled
+from .._style import hcolor
+
+
+def _mk_lines_array(
+ data: List,
+ size: int,
+ elements_step: int = 6,
+) -> np.ndarray:
+ """Create an ndarray to hold lines graphics info.
+
+ """
+ return np.zeros_like(
+ data,
+ shape=(int(size), elements_step),
+ dtype=object,
+ )
+
+
+def lines_from_ohlc(row: np.ndarray, w: float) -> Tuple[QLineF]:
+ open, high, low, close, index = row[
+ ['open', 'high', 'low', 'close', 'index']]
+
+ # high -> low vertical (body) line
+ if low != high:
+ hl = QLineF(index, low, index, high)
+ else:
+ # XXX: if we don't do it renders a weird rectangle?
+ # see below for filtering this later...
+ hl = None
+
+ # NOTE: place the x-coord start as "middle" of the drawing range such
+ # that the open arm line-graphic is at the left-most-side of
+ # the index's range according to the view mapping.
+
+ # open line
+ o = QLineF(index - w, open, index, open)
+ # close line
+ c = QLineF(index, close, index + w, close)
+
+ return [hl, o, c]
+
+
+@jit(
+ # TODO: for now need to construct this manually for readonly arrays, see
+ # https://github.com/numba/numba/issues/4511
+ # ntypes.Tuple((float64[:], float64[:], float64[:]))(
+ # numba_ohlc_dtype[::1], # contiguous
+ # int64,
+ # optional(float64),
+ # ),
+ nopython=True,
+ nogil=True
+)
+def path_arrays_from_ohlc(
+ data: np.ndarray,
+ start: int64,
+ bar_gap: float64 = 0.43,
+) -> np.ndarray:
+ """Generate an array of lines objects from input ohlc data.
+
+ """
+ size = int(data.shape[0] * 6)
+
+ x = np.zeros(
+ # data,
+ shape=size,
+ dtype=float64,
+ )
+ y, c = x.copy(), x.copy()
+
+ # TODO: report bug for assert @
+ # /home/goodboy/repos/piker/env/lib/python3.8/site-packages/numba/core/typing/builtins.py:991
+ for i, q in enumerate(data[start:], start):
+
+ # TODO: ask numba why this doesn't work..
+ # open, high, low, close, index = q[
+ # ['open', 'high', 'low', 'close', 'index']]
+
+ open = q['open']
+ high = q['high']
+ low = q['low']
+ close = q['close']
+ index = float64(q['index'])
+
+ istart = i * 6
+ istop = istart + 6
+
+ # x,y detail the 6 points which connect all vertexes of a ohlc bar
+ x[istart:istop] = (
+ index - bar_gap,
+ index,
+ index,
+ index,
+ index,
+ index + bar_gap,
+ )
+ y[istart:istop] = (
+ open,
+ open,
+ low,
+ high,
+ close,
+ close,
+ )
+
+ # specifies that the first edge is never connected to the
+ # prior bars last edge thus providing a small "gap"/"space"
+ # between bars determined by ``bar_gap``.
+ c[istart:istop] = (0, 1, 1, 1, 1, 1)
+
+ return x, y, c
+
+
+def gen_qpath(
+ data,
+ start, # XXX: do we need this?
+ w,
+) -> QtGui.QPainterPath:
+
+ profiler = pg.debug.Profiler(disabled=not pg_profile_enabled())
+
+ x, y, c = path_arrays_from_ohlc(data, start, bar_gap=w)
+ profiler("generate stream with numba")
+
+ # TODO: numba the internals of this!
+ path = pg.functions.arrayToQPath(x, y, connect=c)
+ profiler("generate path with arrayToQPath")
+
+ return path
+
+
+class BarItems(pg.GraphicsObject):
+ """Price range bars graphics rendered from a OHLC sequence.
+ """
+ sigPlotChanged = QtCore.Signal(object)
+
+ # 0.5 is no overlap between arms, 1.0 is full overlap
+ w: float = 0.43
+
+ # XXX: for the mega-lulz increasing width here increases draw latency...
+ # so probably don't do it until we figure that out.
+ bars_pen = pg.mkPen(hcolor('bracket'))
+
+ def __init__(
+ self,
+ # scene: 'QGraphicsScene', # noqa
+ plotitem: 'pg.PlotItem', # noqa
+ ) -> None:
+ super().__init__()
+
+ # NOTE: this prevents redraws on mouse interaction which is
+ # a huge boon for avg interaction latency.
+
+ # TODO: one question still remaining is if this makes trasform
+ # interactions slower (such as zooming) and if so maybe if/when
+ # we implement a "history" mode for the view we disable this in
+ # that mode?
+ self.setCacheMode(QtGui.QGraphicsItem.DeviceCoordinateCache)
+
+ # not sure if this is actually impoving anything but figured it
+ # was worth a shot:
+ # self.path.reserve(int(100e3 * 6))
+
+ self.path = QtGui.QPainterPath()
+
+ self._pi = plotitem
+
+ self._xrange: Tuple[int, int]
+ self._yrange: Tuple[float, float]
+
+ # TODO: don't render the full backing array each time
+ # self._path_data = None
+ self._last_bar_lines: Optional[Tuple[QLineF, ...]] = None
+
+ # track the current length of drawable lines within the larger array
+ self.start_index: int = 0
+ self.stop_index: int = 0
+
+ def draw_from_data(
+ self,
+ data: np.ndarray,
+ start: int = 0,
+ ) -> QtGui.QPainterPath:
+ """Draw OHLC datum graphics from a ``np.ndarray``.
+
+ This routine is usually only called to draw the initial history.
+ """
+ self.path = gen_qpath(data, start, self.w)
+
+ # save graphics for later reference and keep track
+ # of current internal "last index"
+ # self.start_index = len(data)
+ index = data['index']
+ self._xrange = (index[0], index[-1])
+ self._yrange = (
+ np.nanmax(data['high']),
+ np.nanmin(data['low']),
+ )
+
+ # up to last to avoid double draw of last bar
+ self._last_bar_lines = lines_from_ohlc(data[-1], self.w)
+
+ # trigger render
+ # https://doc.qt.io/qt-5/qgraphicsitem.html#update
+ self.update()
+
+ return self.path
+
+ def update_from_array(
+ self,
+ array: np.ndarray,
+ just_history=False,
+ ) -> None:
+ """Update the last datum's bar graphic from input data array.
+
+ This routine should be interface compatible with
+ ``pg.PlotCurveItem.setData()``. Normally this method in
+ ``pyqtgraph`` seems to update all the data passed to the
+ graphics object, and then update/rerender, but here we're
+ assuming the prior graphics havent changed (OHLC history rarely
+ does) so this "should" be simpler and faster.
+
+ This routine should be made (transitively) as fast as possible.
+ """
+ # index = self.start_index
+ istart, istop = self._xrange
+
+ index = array['index']
+ first_index, last_index = index[0], index[-1]
+
+ # length = len(array)
+ prepend_length = istart - first_index
+ append_length = last_index - istop
+
+ flip_cache = False
+
+ # TODO: allow mapping only a range of lines thus
+ # only drawing as many bars as exactly specified.
+
+ if prepend_length:
+
+ # new history was added and we need to render a new path
+ new_bars = array[:prepend_length]
+ prepend_path = gen_qpath(new_bars, 0, self.w)
+
+ # XXX: SOMETHING IS MAYBE FISHY HERE what with the old_path
+ # y value not matching the first value from
+ # array[prepend_length + 1] ???
+
+ # update path
+ old_path = self.path
+ self.path = prepend_path
+ self.path.addPath(old_path)
+
+ # trigger redraw despite caching
+ self.prepareGeometryChange()
+
+ if append_length:
+ # generate new lines objects for updatable "current bar"
+ self._last_bar_lines = lines_from_ohlc(array[-1], self.w)
+
+ # generate new graphics to match provided array
+ # path appending logic:
+ # we need to get the previous "current bar(s)" for the time step
+ # and convert it to a sub-path to append to the historical set
+ # new_bars = array[istop - 1:istop + append_length - 1]
+ new_bars = array[-append_length - 1:-1]
+ append_path = gen_qpath(new_bars, 0, self.w)
+ self.path.moveTo(float(istop - self.w), float(new_bars[0]['open']))
+ self.path.addPath(append_path)
+
+ # trigger redraw despite caching
+ self.prepareGeometryChange()
+ self.setCacheMode(QtGui.QGraphicsItem.NoCache)
+ flip_cache = True
+
+ self._xrange = first_index, last_index
+
+ # last bar update
+ i, o, h, l, last, v = array[-1][
+ ['index', 'open', 'high', 'low', 'close', 'volume']
+ ]
+ # assert i == self.start_index - 1
+ assert i == last_index
+ body, larm, rarm = self._last_bar_lines
+
+ # XXX: is there a faster way to modify this?
+ rarm.setLine(rarm.x1(), last, rarm.x2(), last)
+ # writer is responsible for changing open on "first" volume of bar
+ larm.setLine(larm.x1(), o, larm.x2(), o)
+
+ if l != h: # noqa
+ if body is None:
+ body = self._last_bar_lines[0] = QLineF(i, l, i, h)
+ else:
+ # update body
+ body.setLine(i, l, i, h)
+
+ # XXX: pretty sure this is causing an issue where the bar has
+ # a large upward move right before the next sample and the body
+ # is getting set to None since the next bar is flat but the shm
+ # array index update wasn't read by the time this code runs. Iow
+ # we're doing this removal of the body for a bar index that is
+ # now out of date / from some previous sample. It's weird
+ # though because i've seen it do this to bars i - 3 back?
+
+ self.update()
+
+ if flip_cache:
+ self.setCacheMode(QtGui.QGraphicsItem.DeviceCoordinateCache)
+
+ def paint(
+ self,
+ p: QtGui.QPainter,
+ opt: QtWidgets.QStyleOptionGraphicsItem,
+ w: QtWidgets.QWidget
+ ) -> None:
+
+ profiler = pg.debug.Profiler(disabled=not pg_profile_enabled())
+
+ # p.setCompositionMode(0)
+ p.setPen(self.bars_pen)
+
+ # TODO: one thing we could try here is pictures being drawn of
+ # a fixed count of bars such that based on the viewbox indices we
+ # only draw the "rounded up" number of "pictures worth" of bars
+ # as is necesarry for what's in "view". Not sure if this will
+ # lead to any perf gains other then when zoomed in to less bars
+ # in view.
+ p.drawLines(*tuple(filter(bool, self._last_bar_lines)))
+ profiler('draw last bar')
+
+ p.drawPath(self.path)
+ profiler('draw history path')
+
+ def boundingRect(self):
+ # Qt docs: https://doc.qt.io/qt-5/qgraphicsitem.html#boundingRect
+
+ # TODO: Can we do rect caching to make this faster
+ # like `pg.PlotCurveItem` does? In theory it's just
+ # computing max/min stuff again like we do in the udpate loop
+ # anyway. Not really sure it's necessary since profiling already
+ # shows this method is faf.
+
+ # boundingRect _must_ indicate the entire area that will be
+ # drawn on or else we will get artifacts and possibly crashing.
+ # (in this case, QPicture does all the work of computing the
+ # bounding rect for us).
+
+ # apparently this a lot faster says the docs?
+ # https://doc.qt.io/qt-5/qpainterpath.html#controlPointRect
+ hb = self.path.controlPointRect()
+ hb_size = hb.size()
+ # print(f'hb_size: {hb_size}')
+
+ w = hb_size.width() + 1
+ h = hb_size.height() + 1
+
+ br = QtCore.QRectF(
+
+ # top left
+ QPointF(hb.topLeft()),
+
+ # total size
+ QtCore.QSizeF(w, h)
+ )
+ # print(f'bounding rect: {br}')
+ return br
+
+
+# XXX: when we get back to enabling tina mode for xb
+# class CandlestickItems(BarItems):
+
+# w2 = 0.7
+# line_pen = pg.mkPen('#000000')
+# bull_brush = pg.mkBrush('#00ff00')
+# bear_brush = pg.mkBrush('#ff0000')
+
+# def _generate(self, p):
+# rects = np.array(
+# [
+# QtCore.QRectF(
+# q.id - self.w,
+# q.open,
+# self.w2,
+# q.close - q.open
+# )
+# for q in Quotes
+# ]
+# )
+
+# p.setPen(self.line_pen)
+# p.drawLines(
+# [QtCore.QLineF(q.id, q.low, q.id, q.high)
+# for q in Quotes]
+# )
+
+# p.setBrush(self.bull_brush)
+# p.drawRects(*rects[Quotes.close > Quotes.open])
+
+# p.setBrush(self.bear_brush)
+# p.drawRects(*rects[Quotes.close < Quotes.open])
diff --git a/piker/ui/_style.py b/piker/ui/_style.py
index eeeb6c9c..9208e13c 100644
--- a/piker/ui/_style.py
+++ b/piker/ui/_style.py
@@ -112,8 +112,6 @@ CHART_MARGINS = (0, 0, 2, 2)
_min_points_to_show = 6
_bars_from_right_in_follow_mode = int(6**2)
_bars_to_left_in_follow_mode = int(6**3)
-
-
_tina_mode = False
diff --git a/piker/ui/cli.py b/piker/ui/cli.py
index 0b2422da..e14ef3f6 100644
--- a/piker/ui/cli.py
+++ b/piker/ui/cli.py
@@ -84,7 +84,7 @@ def monitor(config, rate, name, dhost, test, tl):
@cli.command()
-@click.option('--tl', is_flag=True, help='Enable tractor logging')
+# @click.option('--tl', is_flag=True, help='Enable tractor logging')
@click.option('--date', '-d', help='Contracts expiry date')
@click.option('--test', '-t', help='Test quote stream file')
@click.option('--rate', '-r', default=1, help='Logging level')
@@ -121,16 +121,25 @@ def optschain(config, symbol, date, tl, rate, test):
@cli.command()
+@click.option(
+ '--profile',
+ is_flag=True,
+ help='Enable pyqtgraph profiling'
+)
@click.option('--date', '-d', help='Contracts expiry date')
@click.option('--test', '-t', help='Test quote stream file')
@click.option('--rate', '-r', default=1, help='Logging level')
@click.argument('symbol', required=True)
@click.pass_obj
-def chart(config, symbol, date, rate, test):
- """Start an option chain UI
+def chart(config, symbol, date, rate, test, profile):
+ """Start a real-time chartng UI
"""
+ from .. import _profile
from ._chart import _main
+ # possibly enable profiling
+ _profile._pg_profile = profile
+
# global opts
brokername = config['broker']
tractorloglevel = config['tractorloglevel']