Fix x-axis labelling when using an epoch domain

Previously with array-int indexing we had to map the input x-domain
"indexes" passed to `DynamicDateAxis._indexes_to_timestr()`. In the
epoch-time indexing case we obviously don't need to lookup time stamps
from the underlying shm array and can instead just cast to `int` and
relay the values verbatim.

Further, this patch includes some style adjustments to `AxisLabel` to
better enable multi-feed chart overlays by avoiding L1 label clutter
when multiple y-axes are stacked adjacent:
- adjust the `Axis` typical max string to include a couple spaces suffix
 providing for a bit more margin between side-by-side y-axes.
- make the default label (fill) color the "default" from the global
 color scheme and drop it's opacity to .9
- add some new label placement options and use them in the
 `.boundingRect()` method:
 * `._x/y_br_offset` for relatively shifting the overall label relative
   to it's parent axis.
 * `._y_txt_h_scaling` for increasing the bounding rect's height
   without including more whitespace in the label's text content.
- ensure labels have a high z-value such that by default they are always
 placed "on top" such that when we adjust the l1 labels they can be set
 to a lower value and thus never obscure the last-price label.
epoch_indexing_and_dataviz_layer
Tyler Goodlet 2022-12-23 14:21:55 -05:00
parent cdec4782f0
commit a5eed8fc1e
1 changed files with 86 additions and 48 deletions

View File

@ -95,9 +95,10 @@ class Axis(pg.AxisItem):
self.setPen(_axis_pen) self.setPen(_axis_pen)
# this is the text color # this is the text color
# self.setTextPen(pg.mkPen(hcolor(text_color)))
self.text_color = text_color self.text_color = text_color
# generate a bounding rect based on sizing to a "typical"
# maximum length-ed string defined as init default.
self.typical_br = _font._qfm.boundingRect(typical_max_str) self.typical_br = _font._qfm.boundingRect(typical_max_str)
# size the pertinent axis dimension to a "typical value" # size the pertinent axis dimension to a "typical value"
@ -154,8 +155,8 @@ class Axis(pg.AxisItem):
pi: pgo.PlotItem, pi: pgo.PlotItem,
name: None | str = None, name: None | str = None,
digits: None | int = 2, digits: None | int = 2,
# axis_name: str = 'right', bg_color='default',
bg_color='bracket', fg_color='black',
) -> YAxisLabel: ) -> YAxisLabel:
@ -165,22 +166,20 @@ class Axis(pg.AxisItem):
digits = digits or 2 digits = digits or 2
# TODO: ``._ysticks`` should really be an attr on each # TODO: ``._ysticks`` should really be an attr on each
# ``PlotItem`` no instead of the (containing because of # ``PlotItem`` now instead of the containing widget (because of
# overlays) widget? # overlays) ?
# add y-axis "last" value label # add y-axis "last" value label
sticky = self._stickies[name] = YAxisLabel( sticky = self._stickies[name] = YAxisLabel(
pi=pi, pi=pi,
parent=self, parent=self,
# TODO: pass this from symbol data digits=digits, # TODO: pass this from symbol data
digits=digits, opacity=0.9, # slight see-through
opacity=1,
bg_color=bg_color, bg_color=bg_color,
fg_color=fg_color,
) )
pi.sigRangeChanged.connect(sticky.update_on_resize) pi.sigRangeChanged.connect(sticky.update_on_resize)
# pi.addItem(sticky)
# pi.addItem(last)
return sticky return sticky
@ -244,7 +243,6 @@ class PriceAxis(Axis):
self._min_tick = size self._min_tick = size
def size_to_values(self) -> None: def size_to_values(self) -> None:
# self.typical_br = _font._qfm.boundingRect(typical_max_str)
self.setWidth(self.typical_br.width()) self.setWidth(self.typical_br.width())
# XXX: drop for now since it just eats up h space # XXX: drop for now since it just eats up h space
@ -302,27 +300,44 @@ class DynamicDateAxis(Axis):
# XX: ARGGGGG AG:LKSKDJF:LKJSDFD # XX: ARGGGGG AG:LKSKDJF:LKJSDFD
chart = self.pi.chart_widget chart = self.pi.chart_widget
flow = chart._vizs[chart.name] viz = chart._vizs[chart.name]
shm = flow.shm shm = viz.shm
bars = shm.array array = shm.array
times = array['time']
i_0, i_l = times[0], times[-1]
if (
(indexes[0] < i_0
and indexes[-1] < i_l)
or
(indexes[0] > i_0
and indexes[-1] > i_l)
):
return []
if viz.index_field == 'index':
arr_len = times.shape[0]
first = shm._first.value first = shm._first.value
epochs = times[
bars_len = len(bars) list(
times = bars['time']
epochs = times[list(
map( map(
int, int,
filter( filter(
lambda i: i > 0 and i < bars_len, lambda i: i > 0 and i < arr_len,
(i - first for i in indexes) (i - first for i in indexes)
) )
) )
)] )
]
else:
epochs = list(map(int, indexes))
# TODO: **don't** have this hard coded shift to EST # TODO: **don't** have this hard coded shift to EST
# delay = times[-1] - times[-2] # delay = times[-1] - times[-2]
dts = np.array(epochs, dtype='datetime64[s]') dts = np.array(
epochs,
dtype='datetime64[s]',
)
# see units listing: # see units listing:
# https://numpy.org/devdocs/reference/arrays.datetime.html#datetime-units # https://numpy.org/devdocs/reference/arrays.datetime.html#datetime-units
@ -340,24 +355,39 @@ class DynamicDateAxis(Axis):
spacing: float, spacing: float,
) -> list[str]: ) -> list[str]:
return self._indexes_to_timestrs(values)
# NOTE: handy for debugging the lru cache
# info = self.tickStrings.cache_info() # info = self.tickStrings.cache_info()
# print(info) # print(info)
return self._indexes_to_timestrs(values)
class AxisLabel(pg.GraphicsObject): class AxisLabel(pg.GraphicsObject):
_x_margin = 0 # relative offsets *OF* the bounding rect relative
_y_margin = 0 # to parent graphics object.
# eg. <parent>| => <_x_br_offset> => | <text> |
_x_br_offset: float = 0
_y_br_offset: float = 0
# relative offsets of text *within* bounding rect
# eg. | <_x_margin> => <text> |
_x_margin: float = 0
_y_margin: float = 0
# multiplier of the text content's height in order
# to force a larger (y-dimension) bounding rect.
_y_txt_h_scaling: float = 1
def __init__( def __init__(
self, self,
parent: pg.GraphicsItem, parent: pg.GraphicsItem,
digits: int = 2, digits: int = 2,
bg_color: str = 'bracket', bg_color: str = 'default',
fg_color: str = 'black', fg_color: str = 'black',
opacity: int = 1, # XXX: seriously don't set this to 0 opacity: int = .8, # XXX: seriously don't set this to 0
font_size: str = 'default', font_size: str = 'default',
use_arrow: bool = True, use_arrow: bool = True,
@ -368,6 +398,7 @@ class AxisLabel(pg.GraphicsObject):
self.setParentItem(parent) self.setParentItem(parent)
self.setFlag(self.ItemIgnoresTransformations) self.setFlag(self.ItemIgnoresTransformations)
self.setZValue(100)
# XXX: pretty sure this is faster # XXX: pretty sure this is faster
self.setCacheMode(QtWidgets.QGraphicsItem.DeviceCoordinateCache) self.setCacheMode(QtWidgets.QGraphicsItem.DeviceCoordinateCache)
@ -399,14 +430,14 @@ class AxisLabel(pg.GraphicsObject):
p: QtGui.QPainter, p: QtGui.QPainter,
opt: QtWidgets.QStyleOptionGraphicsItem, opt: QtWidgets.QStyleOptionGraphicsItem,
w: QtWidgets.QWidget w: QtWidgets.QWidget
) -> None: ) -> None:
"""Draw a filled rectangle based on the size of ``.label_str`` text. '''
Draw a filled rectangle based on the size of ``.label_str`` text.
Subtypes can customize further by overloading ``.draw()``. Subtypes can customize further by overloading ``.draw()``.
""" '''
# p.setCompositionMode(QtWidgets.QPainter.CompositionMode_SourceOver)
if self.label_str: if self.label_str:
# if not self.rect: # if not self.rect:
@ -417,7 +448,11 @@ class AxisLabel(pg.GraphicsObject):
p.setFont(self._dpifont.font) p.setFont(self._dpifont.font)
p.setPen(self.fg_color) p.setPen(self.fg_color)
p.drawText(self.rect, self.text_flags, self.label_str) p.drawText(
self.rect,
self.text_flags,
self.label_str,
)
def draw( def draw(
self, self,
@ -425,6 +460,8 @@ class AxisLabel(pg.GraphicsObject):
rect: QtCore.QRectF rect: QtCore.QRectF
) -> None: ) -> None:
p.setOpacity(self.opacity)
if self._use_arrow: if self._use_arrow:
if not self.path: if not self.path:
self._draw_arrow_path() self._draw_arrow_path()
@ -432,15 +469,13 @@ class AxisLabel(pg.GraphicsObject):
p.drawPath(self.path) p.drawPath(self.path)
p.fillPath(self.path, pg.mkBrush(self.bg_color)) p.fillPath(self.path, pg.mkBrush(self.bg_color))
# this adds a nice black outline around the label for some odd
# reason; ok by us
p.setOpacity(self.opacity)
# this cause the L1 labels to glitch out if used in the subtype # this cause the L1 labels to glitch out if used in the subtype
# and it will leave a small black strip with the arrow path if # and it will leave a small black strip with the arrow path if
# done before the above # done before the above
p.fillRect(self.rect, self.bg_color) p.fillRect(
self.rect,
self.bg_color,
)
def boundingRect(self): # noqa def boundingRect(self): # noqa
''' '''
@ -484,15 +519,18 @@ class AxisLabel(pg.GraphicsObject):
txt_h, txt_w = txt_br.height(), txt_br.width() txt_h, txt_w = txt_br.height(), txt_br.width()
# print(f'wsw: {self._dpifont.boundingRect(" ")}') # print(f'wsw: {self._dpifont.boundingRect(" ")}')
# allow subtypes to specify a static width and height # allow subtypes to override width and height
h, w = self.size_hint() h, w = self.size_hint()
# print(f'axis size: {self._parent.size()}')
# print(f'axis geo: {self._parent.geometry()}')
self.rect = QtCore.QRectF( self.rect = QtCore.QRectF(
0, 0,
# relative bounds offsets
self._x_br_offset,
self._y_br_offset,
(w or txt_w) + self._x_margin / 2, (w or txt_w) + self._x_margin / 2,
(h or txt_h) + self._y_margin / 2,
(h or txt_h) * self._y_txt_h_scaling + (self._y_margin / 2),
) )
# print(self.rect) # print(self.rect)
# hb = self.path.controlPointRect() # hb = self.path.controlPointRect()