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
							parent
							
								
									cdec4782f0
								
							
						
					
					
						commit
						a5eed8fc1e
					
				|  | @ -49,7 +49,7 @@ class Axis(pg.AxisItem): | |||
|     def __init__( | ||||
|         self, | ||||
|         plotitem: pgo.PlotItem, | ||||
|         typical_max_str: str = '100 000.000', | ||||
|         typical_max_str: str = '100 000.000  ', | ||||
|         text_color: str = 'bracket', | ||||
|         lru_cache_tick_strings: bool = True, | ||||
|         **kwargs | ||||
|  | @ -95,9 +95,10 @@ class Axis(pg.AxisItem): | |||
|         self.setPen(_axis_pen) | ||||
| 
 | ||||
|         # this is the text color | ||||
|         # self.setTextPen(pg.mkPen(hcolor(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) | ||||
| 
 | ||||
|         # size the pertinent axis dimension to a "typical value" | ||||
|  | @ -154,8 +155,8 @@ class Axis(pg.AxisItem): | |||
|         pi: pgo.PlotItem, | ||||
|         name: None | str = None, | ||||
|         digits: None | int = 2, | ||||
|         # axis_name: str = 'right', | ||||
|         bg_color='bracket', | ||||
|         bg_color='default', | ||||
|         fg_color='black', | ||||
| 
 | ||||
|     ) -> YAxisLabel: | ||||
| 
 | ||||
|  | @ -165,22 +166,20 @@ class Axis(pg.AxisItem): | |||
|         digits = digits or 2 | ||||
| 
 | ||||
|         # TODO: ``._ysticks`` should really be an attr on each | ||||
|         # ``PlotItem`` no instead of the (containing because of | ||||
|         # overlays) widget? | ||||
|         # ``PlotItem`` now instead of the containing widget (because of | ||||
|         # overlays) ? | ||||
| 
 | ||||
|         # add y-axis "last" value label | ||||
|         sticky = self._stickies[name] = YAxisLabel( | ||||
|             pi=pi, | ||||
|             parent=self, | ||||
|             # TODO: pass this from symbol data | ||||
|             digits=digits, | ||||
|             opacity=1, | ||||
|             digits=digits,  # TODO: pass this from symbol data | ||||
|             opacity=0.9,  # slight see-through | ||||
|             bg_color=bg_color, | ||||
|             fg_color=fg_color, | ||||
|         ) | ||||
| 
 | ||||
|         pi.sigRangeChanged.connect(sticky.update_on_resize) | ||||
|         # pi.addItem(sticky) | ||||
|         # pi.addItem(last) | ||||
|         return sticky | ||||
| 
 | ||||
| 
 | ||||
|  | @ -244,7 +243,6 @@ class PriceAxis(Axis): | |||
|         self._min_tick = size | ||||
| 
 | ||||
|     def size_to_values(self) -> None: | ||||
|         # self.typical_br = _font._qfm.boundingRect(typical_max_str) | ||||
|         self.setWidth(self.typical_br.width()) | ||||
| 
 | ||||
|     # XXX: drop for now since it just eats up h space | ||||
|  | @ -302,27 +300,44 @@ class DynamicDateAxis(Axis): | |||
|         # XX: ARGGGGG AG:LKSKDJF:LKJSDFD | ||||
|         chart = self.pi.chart_widget | ||||
| 
 | ||||
|         flow = chart._vizs[chart.name] | ||||
|         shm = flow.shm | ||||
|         bars = shm.array | ||||
|         first = shm._first.value | ||||
|         viz = chart._vizs[chart.name] | ||||
|         shm = viz.shm | ||||
|         array = shm.array | ||||
|         times = array['time'] | ||||
|         i_0, i_l = times[0], times[-1] | ||||
| 
 | ||||
|         bars_len = len(bars) | ||||
|         times = bars['time'] | ||||
|         if ( | ||||
|             (indexes[0] < i_0 | ||||
|              and indexes[-1] < i_l) | ||||
|             or | ||||
|             (indexes[0] > i_0 | ||||
|              and indexes[-1] > i_l) | ||||
|         ): | ||||
|             return [] | ||||
| 
 | ||||
|         epochs = times[list( | ||||
|             map( | ||||
|                 int, | ||||
|                 filter( | ||||
|                     lambda i: i > 0 and i < bars_len, | ||||
|                     (i-first for i in indexes) | ||||
|         if viz.index_field == 'index': | ||||
|             arr_len = times.shape[0] | ||||
|             first = shm._first.value | ||||
|             epochs = times[ | ||||
|                 list( | ||||
|                     map( | ||||
|                         int, | ||||
|                         filter( | ||||
|                             lambda i: i > 0 and i < arr_len, | ||||
|                             (i - first for i in indexes) | ||||
|                         ) | ||||
|                     ) | ||||
|                 ) | ||||
|             ) | ||||
|         )] | ||||
|             ] | ||||
|         else: | ||||
|             epochs = list(map(int, indexes)) | ||||
| 
 | ||||
|         # TODO: **don't** have this hard coded shift to EST | ||||
|         # delay = times[-1] - times[-2] | ||||
|         dts = np.array(epochs, dtype='datetime64[s]') | ||||
|         dts = np.array( | ||||
|             epochs, | ||||
|             dtype='datetime64[s]', | ||||
|         ) | ||||
| 
 | ||||
|         # see units listing: | ||||
|         # https://numpy.org/devdocs/reference/arrays.datetime.html#datetime-units | ||||
|  | @ -340,24 +355,39 @@ class DynamicDateAxis(Axis): | |||
|         spacing: float, | ||||
| 
 | ||||
|     ) -> list[str]: | ||||
| 
 | ||||
|         return self._indexes_to_timestrs(values) | ||||
| 
 | ||||
|         # NOTE: handy for debugging the lru cache | ||||
|         # info = self.tickStrings.cache_info() | ||||
|         # print(info) | ||||
|         return self._indexes_to_timestrs(values) | ||||
| 
 | ||||
| 
 | ||||
| class AxisLabel(pg.GraphicsObject): | ||||
| 
 | ||||
|     _x_margin = 0 | ||||
|     _y_margin = 0 | ||||
|     # relative offsets *OF* the bounding rect relative | ||||
|     # 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__( | ||||
|         self, | ||||
|         parent: pg.GraphicsItem, | ||||
|         digits: int = 2, | ||||
| 
 | ||||
|         bg_color: str = 'bracket', | ||||
|         bg_color: str = 'default', | ||||
|         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', | ||||
| 
 | ||||
|         use_arrow: bool = True, | ||||
|  | @ -368,6 +398,7 @@ class AxisLabel(pg.GraphicsObject): | |||
|         self.setParentItem(parent) | ||||
| 
 | ||||
|         self.setFlag(self.ItemIgnoresTransformations) | ||||
|         self.setZValue(100) | ||||
| 
 | ||||
|         # XXX: pretty sure this is faster | ||||
|         self.setCacheMode(QtWidgets.QGraphicsItem.DeviceCoordinateCache) | ||||
|  | @ -399,14 +430,14 @@ class AxisLabel(pg.GraphicsObject): | |||
|         p: QtGui.QPainter, | ||||
|         opt: QtWidgets.QStyleOptionGraphicsItem, | ||||
|         w: QtWidgets.QWidget | ||||
| 
 | ||||
|     ) -> 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()``. | ||||
| 
 | ||||
|         """ | ||||
|         # p.setCompositionMode(QtWidgets.QPainter.CompositionMode_SourceOver) | ||||
| 
 | ||||
|         ''' | ||||
|         if self.label_str: | ||||
| 
 | ||||
|             # if not self.rect: | ||||
|  | @ -417,7 +448,11 @@ class AxisLabel(pg.GraphicsObject): | |||
| 
 | ||||
|             p.setFont(self._dpifont.font) | ||||
|             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( | ||||
|         self, | ||||
|  | @ -425,6 +460,8 @@ class AxisLabel(pg.GraphicsObject): | |||
|         rect: QtCore.QRectF | ||||
|     ) -> None: | ||||
| 
 | ||||
|         p.setOpacity(self.opacity) | ||||
| 
 | ||||
|         if self._use_arrow: | ||||
|             if not self.path: | ||||
|                 self._draw_arrow_path() | ||||
|  | @ -432,15 +469,13 @@ class AxisLabel(pg.GraphicsObject): | |||
|             p.drawPath(self.path) | ||||
|             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 | ||||
|         # and it will leave a small black strip with the arrow path if | ||||
|         # done before the above | ||||
|         p.fillRect(self.rect, self.bg_color) | ||||
| 
 | ||||
|         p.fillRect( | ||||
|             self.rect, | ||||
|             self.bg_color, | ||||
|         ) | ||||
| 
 | ||||
|     def boundingRect(self):  # noqa | ||||
|         ''' | ||||
|  | @ -484,15 +519,18 @@ class AxisLabel(pg.GraphicsObject): | |||
|         txt_h, txt_w = txt_br.height(), txt_br.width() | ||||
|         # 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() | ||||
|         # print(f'axis size: {self._parent.size()}') | ||||
|         # print(f'axis geo: {self._parent.geometry()}') | ||||
| 
 | ||||
|         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, | ||||
|             (h or txt_h) + self._y_margin / 2, | ||||
| 
 | ||||
|             (h or txt_h) * self._y_txt_h_scaling + (self._y_margin / 2), | ||||
|         ) | ||||
|         # print(self.rect) | ||||
|         # hb = self.path.controlPointRect() | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue