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. | ||||
| """ | ||||
| from typing import Optional, Tuple, Dict, Any | ||||
| from typing import Tuple, Dict, Any | ||||
| import time | ||||
| 
 | ||||
| from PyQt5 import QtCore, QtGui | ||||
|  | @ -172,14 +172,15 @@ class LinkedSplitCharts(QtGui.QWidget): | |||
| 
 | ||||
|         # add crosshairs | ||||
|         self._ch = CrossHair( | ||||
|             parent=self, | ||||
|             linkedsplitcharts=self, | ||||
|             digits=self.digits | ||||
|         ) | ||||
|         self.chart = self.add_plot( | ||||
|             name='main', | ||||
|             name=symbol.key, | ||||
|             array=array, | ||||
|             xaxis=self.xaxis, | ||||
|             ohlc=True, | ||||
|             _is_main=True, | ||||
|         ) | ||||
|         # add crosshair graphic | ||||
|         self.chart.addItem(self._ch) | ||||
|  | @ -195,12 +196,13 @@ class LinkedSplitCharts(QtGui.QWidget): | |||
|         array: np.ndarray, | ||||
|         xaxis: DynamicDateAxis = None, | ||||
|         ohlc: bool = False, | ||||
|         _is_main: bool = False, | ||||
|     ) -> 'ChartPlotWidget': | ||||
|         """Add (sub)plots to chart widget by name. | ||||
| 
 | ||||
|         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( | ||||
|                 "A main plot must be created first with `.plot_main()`") | ||||
| 
 | ||||
|  | @ -230,14 +232,14 @@ class LinkedSplitCharts(QtGui.QWidget): | |||
| 
 | ||||
|         # draw curve graphics | ||||
|         if ohlc: | ||||
|             cpw.draw_ohlc(array) | ||||
|             cpw.draw_ohlc(name, array) | ||||
|         else: | ||||
|             cpw.draw_curve(array) | ||||
|             cpw.draw_curve(name, array) | ||||
| 
 | ||||
|         # add to cross-hair's known plots | ||||
|         self._ch.add_plot(cpw) | ||||
| 
 | ||||
|         if name != "main": | ||||
|         if not _is_main: | ||||
|             # track by name | ||||
|             self.subplots[name] = cpw | ||||
| 
 | ||||
|  | @ -279,13 +281,7 @@ class ChartPlotWidget(pg.PlotWidget): | |||
|         super().__init__(**kwargs) | ||||
|         self._array = array  # readonly view of data | ||||
|         self._graphics = {}  # 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=") | ||||
|         self._labels = {}  # registry of underlying graphics | ||||
| 
 | ||||
|         # show only right side axes | ||||
|         self.hideAxis('left') | ||||
|  | @ -303,6 +299,11 @@ class ChartPlotWidget(pg.PlotWidget): | |||
|         # based on ohlc contents | ||||
|         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( | ||||
|         self, | ||||
|         xfirst: int, | ||||
|  | @ -331,6 +332,7 @@ class ChartPlotWidget(pg.PlotWidget): | |||
| 
 | ||||
|     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, | ||||
|  | @ -345,7 +347,25 @@ class ChartPlotWidget(pg.PlotWidget): | |||
|         graphics.draw_from_data(data) | ||||
|         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 | ||||
|         xlast = data[-1]['index'] | ||||
|  | @ -357,8 +377,8 @@ class ChartPlotWidget(pg.PlotWidget): | |||
| 
 | ||||
|     def draw_curve( | ||||
|         self, | ||||
|         name: str, | ||||
|         data: np.ndarray, | ||||
|         name: Optional[str] = 'line_main', | ||||
|     ) -> pg.PlotDataItem: | ||||
|         # draw the indicator as a plain curve | ||||
|         curve = pg.PlotDataItem( | ||||
|  | @ -371,10 +391,24 @@ class ChartPlotWidget(pg.PlotWidget): | |||
| 
 | ||||
|         # register overlay curve with name | ||||
|         if not self._graphics and name is None: | ||||
|             name = 'line_main' | ||||
|             name = 'a_line_bby' | ||||
| 
 | ||||
|         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" | ||||
|         xlast = len(data) - 1 | ||||
| 
 | ||||
|  | @ -531,29 +565,28 @@ async def add_new_bars(delay_s, linked_charts): | |||
|             ) | ||||
| 
 | ||||
|         # 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 | ||||
|         # since the next quote that arrives should in the | ||||
|         # tick streaming task | ||||
|         await sleep() | ||||
| 
 | ||||
|         # 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}") | ||||
|             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() | ||||
|         # TODO: should we update a graphics again time here? | ||||
|         # Think about race conditions with data update task. | ||||
| 
 | ||||
| 
 | ||||
| async def _async_main( | ||||
|  | @ -639,9 +672,10 @@ async def _async_main( | |||
|                                 last, | ||||
|                             ) | ||||
|                             chart.update_from_array( | ||||
|                                 'ohlc_main', | ||||
|                                 chart.name, | ||||
|                                 chart._array, | ||||
|                             ) | ||||
|                             chart._set_yrange() | ||||
| 
 | ||||
| 
 | ||||
| async def chart_from_fsp( | ||||
|  | @ -676,7 +710,7 @@ async def chart_from_fsp( | |||
|         stream = await portal.result() | ||||
| 
 | ||||
|         # receive processed historical data-array as first message | ||||
|         history = (await stream.__anext__()) | ||||
|         history: np.ndarray = (await stream.__anext__()) | ||||
| 
 | ||||
|         # TODO: enforce type checking here | ||||
|         newbars = np.array(history) | ||||
|  | @ -686,10 +720,19 @@ async def chart_from_fsp( | |||
|             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: | ||||
|             chart._array[-1] = value | ||||
|             chart.update_from_array('line_main', chart._array) | ||||
|             chart.update_from_array(chart.name, chart._array) | ||||
|             chart._set_yrange() | ||||
| 
 | ||||
| 
 | ||||
|  | @ -703,7 +746,7 @@ def _main( | |||
|     # Qt entry point | ||||
|     run_qtractor( | ||||
|        func=_async_main, | ||||
|         args=(sym, brokername), | ||||
|         main_widget=ChartSpace, | ||||
|         tractor_kwargs=tractor_kwargs, | ||||
|        args=(sym, brokername), | ||||
|        main_widget=ChartSpace, | ||||
|        tractor_kwargs=tractor_kwargs, | ||||
|     ) | ||||
|  |  | |||
|  | @ -2,7 +2,6 @@ | |||
| Chart graphics for displaying a slew of different data types. | ||||
| """ | ||||
| from typing import List | ||||
| from enum import Enum | ||||
| from itertools import chain | ||||
| 
 | ||||
| import numpy as np | ||||
|  | @ -23,10 +22,14 @@ _mouse_rate_limit = 30 | |||
| 
 | ||||
| class CrossHair(pg.GraphicsObject): | ||||
| 
 | ||||
|     def __init__(self, parent, digits: int = 0): | ||||
|     def __init__( | ||||
|         self, | ||||
|         linkedsplitcharts: 'LinkedSplitCharts', | ||||
|         digits: int = 0 | ||||
|     ) -> None: | ||||
|         super().__init__() | ||||
|         self.pen = pg.mkPen('#a9a9a9')  # gray? | ||||
|         self.parent = parent | ||||
|         self.lsc = linkedsplitcharts | ||||
|         self.graphics = {} | ||||
|         self.plots = [] | ||||
|         self.active_plot = None | ||||
|  | @ -110,18 +113,27 @@ class CrossHair(pg.GraphicsObject): | |||
|             # mouse was not on active plot | ||||
|             return | ||||
| 
 | ||||
|         self.graphics[self.active_plot]['hl'].setY( | ||||
|             mouse_point.y() | ||||
|         ) | ||||
|         x, y = mouse_point.x(), mouse_point.y() | ||||
| 
 | ||||
|         plot = self.active_plot | ||||
| 
 | ||||
|         self.graphics[plot]['hl'].setY(y) | ||||
| 
 | ||||
|         self.graphics[self.active_plot]['yl'].update_label( | ||||
|             evt_post=pos, point_view=mouse_point | ||||
|         ) | ||||
|         # move the vertical line to the current x coordinate in all charts | ||||
|         for opts in self.graphics.values(): | ||||
|             opts['vl'].setX(mouse_point.x()) | ||||
|         for plot, opts in self.graphics.items(): | ||||
|             # move the vertical line to the current 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 | ||||
|         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): | ||||
|         try: | ||||
|  | @ -149,8 +161,8 @@ def _mk_lines_array(data: List, size: int) -> np.ndarray: | |||
| 
 | ||||
| def bars_from_ohlc( | ||||
|     data: np.ndarray, | ||||
|     w: float, | ||||
|     start: int = 0, | ||||
|     w: float = 0.43, | ||||
| ) -> np.ndarray: | ||||
|     """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']] | ||||
| 
 | ||||
|         # 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 | ||||
|         if low != high: | ||||
|             hl = QLineF(index, low, index, high) | ||||
|             # hl = QLineF(index, low, index, high) | ||||
|             hl = QLineF(index_start, low, index_start, high) | ||||
|         else: | ||||
|             # XXX: if we don't do it renders a weird rectangle? | ||||
|             # see below too for handling this later... | ||||
|  | @ -170,9 +188,9 @@ def bars_from_ohlc( | |||
|             hl._flat = True | ||||
| 
 | ||||
|         # open line | ||||
|         o = QLineF(index - w, open, index, open) | ||||
|         o = QLineF(index_start - w, open, index_start, open) | ||||
|         # 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 | ||||
|         # lines[3*i:3*i+3] = (hl, o, c) | ||||
|  | @ -207,7 +225,7 @@ class BarItems(pg.GraphicsObject): | |||
|     sigPlotChanged = QtCore.Signal(object) | ||||
| 
 | ||||
|     # 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') | ||||
| 
 | ||||
|     # XXX: tina mode, see below | ||||
|  | @ -234,7 +252,7 @@ class BarItems(pg.GraphicsObject): | |||
|     ): | ||||
|         """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 | ||||
|         # of current internal "last index" | ||||
|  | @ -280,7 +298,7 @@ class BarItems(pg.GraphicsObject): | |||
|         if extra > 0: | ||||
|             # generate new graphics to match provided array | ||||
|             new = array[index:index + extra] | ||||
|             lines = bars_from_ohlc(new) | ||||
|             lines = bars_from_ohlc(new, self.w) | ||||
|             bars_added = len(lines) | ||||
|             self.lines[index:index + bars_added] = lines | ||||
|             self.index += bars_added | ||||
|  | @ -311,7 +329,7 @@ class BarItems(pg.GraphicsObject): | |||
|             # if the bar was flat it likely does not have | ||||
|             # the index set correctly due to a rendering bug | ||||
|             # see above | ||||
|             body.setLine(i, low, i, high) | ||||
|             body.setLine(i + self.w, low, i + self.w, high) | ||||
|             body._flat = False | ||||
|         else: | ||||
|             body.setLine(body.x1(), low, body.x2(), high) | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue