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.multichartz
							parent
							
								
									066027fe1c
								
							
						
					
					
						commit
						d4f8bb6545
					
				| 
						 | 
				
			
			@ -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