Add "contents" labels to charts
Add a default "contents label" (eg. OHLC values for bar charts) to each chart and update on crosshair interaction. Few technical changes to make this happen: - adjust bar graphics to have the HL line be in the "middle" of the underlying arrays' "index range" in the containing view. - add a label dict each chart's graphics name to a label + update routine - use symbol names instead of this "main" identifier crap for referring to particular price curves/graphicsbar_select
parent
61e460a422
commit
f46fa99a6e
|
@ -1,7 +1,7 @@
|
||||||
"""
|
"""
|
||||||
High level Qt chart widgets.
|
High level Qt chart widgets.
|
||||||
"""
|
"""
|
||||||
from typing import Optional, Tuple, Dict, Any
|
from typing import Tuple, Dict, Any
|
||||||
import time
|
import time
|
||||||
|
|
||||||
from PyQt5 import QtCore, QtGui
|
from PyQt5 import QtCore, QtGui
|
||||||
|
@ -172,14 +172,15 @@ class LinkedSplitCharts(QtGui.QWidget):
|
||||||
|
|
||||||
# add crosshairs
|
# add crosshairs
|
||||||
self._ch = CrossHair(
|
self._ch = CrossHair(
|
||||||
parent=self,
|
linkedsplitcharts=self,
|
||||||
digits=self.digits
|
digits=self.digits
|
||||||
)
|
)
|
||||||
self.chart = self.add_plot(
|
self.chart = self.add_plot(
|
||||||
name='main',
|
name=symbol.key,
|
||||||
array=array,
|
array=array,
|
||||||
xaxis=self.xaxis,
|
xaxis=self.xaxis,
|
||||||
ohlc=True,
|
ohlc=True,
|
||||||
|
_is_main=True,
|
||||||
)
|
)
|
||||||
# add crosshair graphic
|
# add crosshair graphic
|
||||||
self.chart.addItem(self._ch)
|
self.chart.addItem(self._ch)
|
||||||
|
@ -195,12 +196,13 @@ class LinkedSplitCharts(QtGui.QWidget):
|
||||||
array: np.ndarray,
|
array: np.ndarray,
|
||||||
xaxis: DynamicDateAxis = None,
|
xaxis: DynamicDateAxis = None,
|
||||||
ohlc: bool = False,
|
ohlc: bool = False,
|
||||||
|
_is_main: bool = False,
|
||||||
) -> 'ChartPlotWidget':
|
) -> 'ChartPlotWidget':
|
||||||
"""Add (sub)plots to chart widget by name.
|
"""Add (sub)plots to chart widget by name.
|
||||||
|
|
||||||
If ``name`` == ``"main"`` the chart will be the the primary view.
|
If ``name`` == ``"main"`` the chart will be the the primary view.
|
||||||
"""
|
"""
|
||||||
if self.chart is None and name != 'main':
|
if self.chart is None and not _is_main:
|
||||||
raise RuntimeError(
|
raise RuntimeError(
|
||||||
"A main plot must be created first with `.plot_main()`")
|
"A main plot must be created first with `.plot_main()`")
|
||||||
|
|
||||||
|
@ -230,14 +232,14 @@ class LinkedSplitCharts(QtGui.QWidget):
|
||||||
|
|
||||||
# draw curve graphics
|
# draw curve graphics
|
||||||
if ohlc:
|
if ohlc:
|
||||||
cpw.draw_ohlc(array)
|
cpw.draw_ohlc(name, array)
|
||||||
else:
|
else:
|
||||||
cpw.draw_curve(array)
|
cpw.draw_curve(name, array)
|
||||||
|
|
||||||
# add to cross-hair's known plots
|
# add to cross-hair's known plots
|
||||||
self._ch.add_plot(cpw)
|
self._ch.add_plot(cpw)
|
||||||
|
|
||||||
if name != "main":
|
if not _is_main:
|
||||||
# track by name
|
# track by name
|
||||||
self.subplots[name] = cpw
|
self.subplots[name] = cpw
|
||||||
|
|
||||||
|
@ -279,13 +281,7 @@ class ChartPlotWidget(pg.PlotWidget):
|
||||||
super().__init__(**kwargs)
|
super().__init__(**kwargs)
|
||||||
self._array = array # readonly view of data
|
self._array = array # readonly view of data
|
||||||
self._graphics = {} # registry of underlying graphics
|
self._graphics = {} # registry of underlying graphics
|
||||||
|
self._labels = {} # registry of underlying graphics
|
||||||
# XXX: label setting doesn't seem to work?
|
|
||||||
# likely custom graphics need special handling
|
|
||||||
# label = pg.LabelItem(justify='right')
|
|
||||||
# self.addItem(label)
|
|
||||||
# label.setText("Yo yoyo")
|
|
||||||
# label.setText("<span style='font-size: 12pt'>x=")
|
|
||||||
|
|
||||||
# show only right side axes
|
# show only right side axes
|
||||||
self.hideAxis('left')
|
self.hideAxis('left')
|
||||||
|
@ -303,6 +299,11 @@ class ChartPlotWidget(pg.PlotWidget):
|
||||||
# based on ohlc contents
|
# based on ohlc contents
|
||||||
self.sigXRangeChanged.connect(self._set_yrange)
|
self.sigXRangeChanged.connect(self._set_yrange)
|
||||||
|
|
||||||
|
def _update_contents_label(self, index: int) -> None:
|
||||||
|
if index > 0 and index < len(self._array):
|
||||||
|
for name, (label, update) in self._labels.items():
|
||||||
|
update(index)
|
||||||
|
|
||||||
def _set_xlimits(
|
def _set_xlimits(
|
||||||
self,
|
self,
|
||||||
xfirst: int,
|
xfirst: int,
|
||||||
|
@ -331,6 +332,7 @@ class ChartPlotWidget(pg.PlotWidget):
|
||||||
|
|
||||||
def draw_ohlc(
|
def draw_ohlc(
|
||||||
self,
|
self,
|
||||||
|
name: str,
|
||||||
data: np.ndarray,
|
data: np.ndarray,
|
||||||
# XXX: pretty sure this is dumb and we don't need an Enum
|
# XXX: pretty sure this is dumb and we don't need an Enum
|
||||||
style: pg.GraphicsObject = BarItems,
|
style: pg.GraphicsObject = BarItems,
|
||||||
|
@ -345,7 +347,25 @@ class ChartPlotWidget(pg.PlotWidget):
|
||||||
graphics.draw_from_data(data)
|
graphics.draw_from_data(data)
|
||||||
self.addItem(graphics)
|
self.addItem(graphics)
|
||||||
|
|
||||||
self._graphics['ohlc_main'] = graphics
|
self._graphics[name] = graphics
|
||||||
|
|
||||||
|
# XXX: How to stack labels vertically?
|
||||||
|
label = pg.LabelItem(
|
||||||
|
justify='left',
|
||||||
|
size='5pt',
|
||||||
|
)
|
||||||
|
self.scene().addItem(label)
|
||||||
|
|
||||||
|
def update(index: int) -> None:
|
||||||
|
label.setText(
|
||||||
|
"{name} O:{} H:{} L:{} C:{} V:{}".format(
|
||||||
|
*self._array[index].item()[2:],
|
||||||
|
name=name,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
self._labels[name] = (label, update)
|
||||||
|
self._update_contents_label(index=-1)
|
||||||
|
|
||||||
# set xrange limits
|
# set xrange limits
|
||||||
xlast = data[-1]['index']
|
xlast = data[-1]['index']
|
||||||
|
@ -357,8 +377,8 @@ class ChartPlotWidget(pg.PlotWidget):
|
||||||
|
|
||||||
def draw_curve(
|
def draw_curve(
|
||||||
self,
|
self,
|
||||||
|
name: str,
|
||||||
data: np.ndarray,
|
data: np.ndarray,
|
||||||
name: Optional[str] = 'line_main',
|
|
||||||
) -> pg.PlotDataItem:
|
) -> pg.PlotDataItem:
|
||||||
# draw the indicator as a plain curve
|
# draw the indicator as a plain curve
|
||||||
curve = pg.PlotDataItem(
|
curve = pg.PlotDataItem(
|
||||||
|
@ -371,10 +391,24 @@ class ChartPlotWidget(pg.PlotWidget):
|
||||||
|
|
||||||
# register overlay curve with name
|
# register overlay curve with name
|
||||||
if not self._graphics and name is None:
|
if not self._graphics and name is None:
|
||||||
name = 'line_main'
|
name = 'a_line_bby'
|
||||||
|
|
||||||
self._graphics[name] = curve
|
self._graphics[name] = curve
|
||||||
|
|
||||||
|
# XXX: How to stack labels vertically?
|
||||||
|
label = pg.LabelItem(
|
||||||
|
justify='left',
|
||||||
|
size='5pt',
|
||||||
|
)
|
||||||
|
self.scene().addItem(label)
|
||||||
|
|
||||||
|
def update(index: int) -> None:
|
||||||
|
data = self._array[index]
|
||||||
|
label.setText(f"{name}: {index} {data}")
|
||||||
|
|
||||||
|
self._labels[name] = (label, update)
|
||||||
|
self._update_contents_label(index=-1)
|
||||||
|
|
||||||
# set a "startup view"
|
# set a "startup view"
|
||||||
xlast = len(data) - 1
|
xlast = len(data) - 1
|
||||||
|
|
||||||
|
@ -531,29 +565,28 @@ async def add_new_bars(delay_s, linked_charts):
|
||||||
)
|
)
|
||||||
|
|
||||||
# read value at "open" of bar
|
# read value at "open" of bar
|
||||||
last_quote = ohlc[-1]
|
# last_quote = ohlc[-1]
|
||||||
|
# XXX: If the last bar has not changed print a flat line and
|
||||||
|
# move to the next. This is a "animation" choice that we may not
|
||||||
|
# keep.
|
||||||
|
# if last_quote == ohlc[-1]:
|
||||||
|
# log.debug("Printing flat line for {sym}")
|
||||||
|
|
||||||
|
# update chart graphics and resize view
|
||||||
|
price_chart.update_from_array(price_chart.name, ohlc)
|
||||||
|
price_chart._set_yrange()
|
||||||
|
|
||||||
|
for name, chart in linked_charts.subplots.items():
|
||||||
|
chart.update_from_array(chart.name, chart._array)
|
||||||
|
chart._set_yrange()
|
||||||
|
|
||||||
# We **don't** update the bar right now
|
# We **don't** update the bar right now
|
||||||
# since the next quote that arrives should in the
|
# since the next quote that arrives should in the
|
||||||
# tick streaming task
|
# tick streaming task
|
||||||
await sleep()
|
await sleep()
|
||||||
|
|
||||||
# XXX: If the last bar has not changed print a flat line and
|
# TODO: should we update a graphics again time here?
|
||||||
# move to the next. This is a "animation" choice that we may not
|
# Think about race conditions with data update task.
|
||||||
# keep.
|
|
||||||
if last_quote == ohlc[-1]:
|
|
||||||
log.debug("Printing flat line for {sym}")
|
|
||||||
price_chart.update_from_array('ohlc_main', ohlc)
|
|
||||||
|
|
||||||
# resize view
|
|
||||||
price_chart._set_yrange()
|
|
||||||
|
|
||||||
|
|
||||||
for name, chart in linked_charts.subplots.items():
|
|
||||||
chart.update_from_array('line_main', chart._array)
|
|
||||||
|
|
||||||
# resize view
|
|
||||||
chart._set_yrange()
|
|
||||||
|
|
||||||
|
|
||||||
async def _async_main(
|
async def _async_main(
|
||||||
|
@ -639,9 +672,10 @@ async def _async_main(
|
||||||
last,
|
last,
|
||||||
)
|
)
|
||||||
chart.update_from_array(
|
chart.update_from_array(
|
||||||
'ohlc_main',
|
chart.name,
|
||||||
chart._array,
|
chart._array,
|
||||||
)
|
)
|
||||||
|
chart._set_yrange()
|
||||||
|
|
||||||
|
|
||||||
async def chart_from_fsp(
|
async def chart_from_fsp(
|
||||||
|
@ -676,7 +710,7 @@ async def chart_from_fsp(
|
||||||
stream = await portal.result()
|
stream = await portal.result()
|
||||||
|
|
||||||
# receive processed historical data-array as first message
|
# receive processed historical data-array as first message
|
||||||
history = (await stream.__anext__())
|
history: np.ndarray = (await stream.__anext__())
|
||||||
|
|
||||||
# TODO: enforce type checking here
|
# TODO: enforce type checking here
|
||||||
newbars = np.array(history)
|
newbars = np.array(history)
|
||||||
|
@ -686,10 +720,19 @@ async def chart_from_fsp(
|
||||||
array=newbars,
|
array=newbars,
|
||||||
)
|
)
|
||||||
|
|
||||||
# update sub-plot graphics
|
# check for data length mis-allignment and fill missing values
|
||||||
|
diff = len(chart._array) - len(linked_charts.chart._array)
|
||||||
|
if diff < 0:
|
||||||
|
data = chart._array
|
||||||
|
chart._array = np.append(
|
||||||
|
data,
|
||||||
|
np.full(abs(diff), data[-1], dtype=data.dtype)
|
||||||
|
)
|
||||||
|
|
||||||
|
# update chart graphics
|
||||||
async for value in stream:
|
async for value in stream:
|
||||||
chart._array[-1] = value
|
chart._array[-1] = value
|
||||||
chart.update_from_array('line_main', chart._array)
|
chart.update_from_array(chart.name, chart._array)
|
||||||
chart._set_yrange()
|
chart._set_yrange()
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,6 @@
|
||||||
Chart graphics for displaying a slew of different data types.
|
Chart graphics for displaying a slew of different data types.
|
||||||
"""
|
"""
|
||||||
from typing import List
|
from typing import List
|
||||||
from enum import Enum
|
|
||||||
from itertools import chain
|
from itertools import chain
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
@ -23,10 +22,14 @@ _mouse_rate_limit = 30
|
||||||
|
|
||||||
class CrossHair(pg.GraphicsObject):
|
class CrossHair(pg.GraphicsObject):
|
||||||
|
|
||||||
def __init__(self, parent, digits: int = 0):
|
def __init__(
|
||||||
|
self,
|
||||||
|
linkedsplitcharts: 'LinkedSplitCharts',
|
||||||
|
digits: int = 0
|
||||||
|
) -> None:
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.pen = pg.mkPen('#a9a9a9') # gray?
|
self.pen = pg.mkPen('#a9a9a9') # gray?
|
||||||
self.parent = parent
|
self.lsc = linkedsplitcharts
|
||||||
self.graphics = {}
|
self.graphics = {}
|
||||||
self.plots = []
|
self.plots = []
|
||||||
self.active_plot = None
|
self.active_plot = None
|
||||||
|
@ -110,18 +113,27 @@ class CrossHair(pg.GraphicsObject):
|
||||||
# mouse was not on active plot
|
# mouse was not on active plot
|
||||||
return
|
return
|
||||||
|
|
||||||
self.graphics[self.active_plot]['hl'].setY(
|
x, y = mouse_point.x(), mouse_point.y()
|
||||||
mouse_point.y()
|
|
||||||
)
|
plot = self.active_plot
|
||||||
|
|
||||||
|
self.graphics[plot]['hl'].setY(y)
|
||||||
|
|
||||||
self.graphics[self.active_plot]['yl'].update_label(
|
self.graphics[self.active_plot]['yl'].update_label(
|
||||||
evt_post=pos, point_view=mouse_point
|
evt_post=pos, point_view=mouse_point
|
||||||
)
|
)
|
||||||
# move the vertical line to the current x coordinate in all charts
|
for plot, opts in self.graphics.items():
|
||||||
for opts in self.graphics.values():
|
# move the vertical line to the current x
|
||||||
opts['vl'].setX(mouse_point.x())
|
opts['vl'].setX(x)
|
||||||
|
|
||||||
|
# update the chart's "contents" label
|
||||||
|
plot._update_contents_label(int(x))
|
||||||
|
|
||||||
# update the label on the bottom of the crosshair
|
# update the label on the bottom of the crosshair
|
||||||
self.xaxis_label.update_label(evt_post=pos, point_view=mouse_point)
|
self.xaxis_label.update_label(
|
||||||
|
evt_post=pos,
|
||||||
|
point_view=mouse_point
|
||||||
|
)
|
||||||
|
|
||||||
def boundingRect(self):
|
def boundingRect(self):
|
||||||
try:
|
try:
|
||||||
|
@ -149,8 +161,8 @@ def _mk_lines_array(data: List, size: int) -> np.ndarray:
|
||||||
|
|
||||||
def bars_from_ohlc(
|
def bars_from_ohlc(
|
||||||
data: np.ndarray,
|
data: np.ndarray,
|
||||||
|
w: float,
|
||||||
start: int = 0,
|
start: int = 0,
|
||||||
w: float = 0.43,
|
|
||||||
) -> np.ndarray:
|
) -> np.ndarray:
|
||||||
"""Generate an array of lines objects from input ohlc data.
|
"""Generate an array of lines objects from input ohlc data.
|
||||||
"""
|
"""
|
||||||
|
@ -160,9 +172,15 @@ def bars_from_ohlc(
|
||||||
open, high, low, close, index = q[
|
open, high, low, close, index = q[
|
||||||
['open', 'high', 'low', 'close', 'index']]
|
['open', 'high', 'low', 'close', 'index']]
|
||||||
|
|
||||||
|
# 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 indexe's range according to the view mapping.
|
||||||
|
index_start = index + w
|
||||||
|
|
||||||
# high - low line
|
# high - low line
|
||||||
if low != high:
|
if low != high:
|
||||||
hl = QLineF(index, low, index, high)
|
# hl = QLineF(index, low, index, high)
|
||||||
|
hl = QLineF(index_start, low, index_start, high)
|
||||||
else:
|
else:
|
||||||
# XXX: if we don't do it renders a weird rectangle?
|
# XXX: if we don't do it renders a weird rectangle?
|
||||||
# see below too for handling this later...
|
# see below too for handling this later...
|
||||||
|
@ -170,9 +188,9 @@ def bars_from_ohlc(
|
||||||
hl._flat = True
|
hl._flat = True
|
||||||
|
|
||||||
# open line
|
# open line
|
||||||
o = QLineF(index - w, open, index, open)
|
o = QLineF(index_start - w, open, index_start, open)
|
||||||
# close line
|
# close line
|
||||||
c = QLineF(index + w, close, index, close)
|
c = QLineF(index_start + w, close, index_start, close)
|
||||||
|
|
||||||
# indexing here is as per the below comments
|
# indexing here is as per the below comments
|
||||||
# lines[3*i:3*i+3] = (hl, o, c)
|
# lines[3*i:3*i+3] = (hl, o, c)
|
||||||
|
@ -207,7 +225,7 @@ class BarItems(pg.GraphicsObject):
|
||||||
sigPlotChanged = QtCore.Signal(object)
|
sigPlotChanged = QtCore.Signal(object)
|
||||||
|
|
||||||
# 0.5 is no overlap between arms, 1.0 is full overlap
|
# 0.5 is no overlap between arms, 1.0 is full overlap
|
||||||
w: float = 0.4
|
w: float = 0.43
|
||||||
bull_pen = pg.mkPen('#808080')
|
bull_pen = pg.mkPen('#808080')
|
||||||
|
|
||||||
# XXX: tina mode, see below
|
# XXX: tina mode, see below
|
||||||
|
@ -234,7 +252,7 @@ class BarItems(pg.GraphicsObject):
|
||||||
):
|
):
|
||||||
"""Draw OHLC datum graphics from a ``np.recarray``.
|
"""Draw OHLC datum graphics from a ``np.recarray``.
|
||||||
"""
|
"""
|
||||||
lines = bars_from_ohlc(data, start=start)
|
lines = bars_from_ohlc(data, self.w, start=start)
|
||||||
|
|
||||||
# save graphics for later reference and keep track
|
# save graphics for later reference and keep track
|
||||||
# of current internal "last index"
|
# of current internal "last index"
|
||||||
|
@ -280,7 +298,7 @@ class BarItems(pg.GraphicsObject):
|
||||||
if extra > 0:
|
if extra > 0:
|
||||||
# generate new graphics to match provided array
|
# generate new graphics to match provided array
|
||||||
new = array[index:index + extra]
|
new = array[index:index + extra]
|
||||||
lines = bars_from_ohlc(new)
|
lines = bars_from_ohlc(new, self.w)
|
||||||
bars_added = len(lines)
|
bars_added = len(lines)
|
||||||
self.lines[index:index + bars_added] = lines
|
self.lines[index:index + bars_added] = lines
|
||||||
self.index += bars_added
|
self.index += bars_added
|
||||||
|
@ -311,7 +329,7 @@ class BarItems(pg.GraphicsObject):
|
||||||
# if the bar was flat it likely does not have
|
# if the bar was flat it likely does not have
|
||||||
# the index set correctly due to a rendering bug
|
# the index set correctly due to a rendering bug
|
||||||
# see above
|
# see above
|
||||||
body.setLine(i, low, i, high)
|
body.setLine(i + self.w, low, i + self.w, high)
|
||||||
body._flat = False
|
body._flat = False
|
||||||
else:
|
else:
|
||||||
body.setLine(body.x1(), low, body.x2(), high)
|
body.setLine(body.x1(), low, body.x2(), high)
|
||||||
|
|
Loading…
Reference in New Issue