2020-11-06 17:23:14 +00:00
|
|
|
# piker: trading gear for hackers
|
2023-02-28 20:01:00 +00:00
|
|
|
# Copyright (C) Tyler Goodlet (in stewardship for pikers)
|
2020-11-06 17:23:14 +00:00
|
|
|
|
|
|
|
# This program is free software: you can redistribute it and/or modify
|
|
|
|
# it under the terms of the GNU Affero General Public License as published by
|
|
|
|
# the Free Software Foundation, either version 3 of the License, or
|
|
|
|
# (at your option) any later version.
|
|
|
|
|
|
|
|
# This program is distributed in the hope that it will be useful,
|
|
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
|
# GNU Affero General Public License for more details.
|
|
|
|
|
|
|
|
# You should have received a copy of the GNU Affero General Public License
|
|
|
|
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
|
|
|
2020-06-15 14:48:00 +00:00
|
|
|
"""
|
|
|
|
Chart axes graphics and behavior.
|
2021-02-11 16:48:19 +00:00
|
|
|
|
2020-06-15 14:48:00 +00:00
|
|
|
"""
|
2022-11-14 20:09:00 +00:00
|
|
|
from __future__ import annotations
|
2022-01-17 23:42:24 +00:00
|
|
|
from functools import lru_cache
|
2023-02-21 14:14:26 +00:00
|
|
|
from typing import Callable
|
2021-03-31 18:24:39 +00:00
|
|
|
from math import floor
|
2020-08-31 21:18:02 +00:00
|
|
|
|
2022-05-15 17:38:22 +00:00
|
|
|
import numpy as np
|
2020-06-15 14:48:00 +00:00
|
|
|
import pyqtgraph as pg
|
2020-12-29 19:34:25 +00:00
|
|
|
from PyQt5 import QtCore, QtGui, QtWidgets
|
2020-08-30 16:27:41 +00:00
|
|
|
from PyQt5.QtCore import QPointF
|
2020-06-15 14:48:00 +00:00
|
|
|
|
2022-11-14 20:09:00 +00:00
|
|
|
from . import _pg_overrides as pgo
|
2023-03-13 21:42:20 +00:00
|
|
|
from ..accounting._mktinfo import float_digits
|
2022-01-17 23:42:24 +00:00
|
|
|
from ._label import Label
|
|
|
|
from ._style import DpiAwareFont, hcolor, _font
|
|
|
|
from ._interaction import ChartView
|
2020-06-15 14:48:00 +00:00
|
|
|
|
2020-10-27 03:34:48 +00:00
|
|
|
_axis_pen = pg.mkPen(hcolor('bracket'))
|
|
|
|
|
2020-06-15 14:48:00 +00:00
|
|
|
|
2020-10-27 14:50:28 +00:00
|
|
|
class Axis(pg.AxisItem):
|
2022-01-09 16:31:15 +00:00
|
|
|
'''
|
|
|
|
A better axis that sizes tick contents considering font size.
|
2020-06-15 14:48:00 +00:00
|
|
|
|
2022-10-31 18:23:29 +00:00
|
|
|
Also includes tick values lru caching originally proposed in but never
|
|
|
|
accepted upstream:
|
|
|
|
https://github.com/pyqtgraph/pyqtgraph/pull/2160
|
|
|
|
|
2022-01-09 16:31:15 +00:00
|
|
|
'''
|
2020-08-27 01:45:13 +00:00
|
|
|
def __init__(
|
|
|
|
self,
|
2022-11-14 20:09:00 +00:00
|
|
|
plotitem: pgo.PlotItem,
|
2022-12-23 19:21:55 +00:00
|
|
|
typical_max_str: str = '100 000.000 ',
|
2022-02-02 13:05:10 +00:00
|
|
|
text_color: str = 'bracket',
|
2022-10-31 18:23:29 +00:00
|
|
|
lru_cache_tick_strings: bool = True,
|
2020-10-27 14:50:28 +00:00
|
|
|
**kwargs
|
|
|
|
|
2022-01-09 16:31:15 +00:00
|
|
|
) -> None:
|
2022-02-02 13:05:10 +00:00
|
|
|
super().__init__(
|
|
|
|
# textPen=textPen,
|
|
|
|
**kwargs
|
|
|
|
)
|
2020-12-29 19:34:25 +00:00
|
|
|
|
2023-02-21 14:14:26 +00:00
|
|
|
# XXX: pretty sure this makes things slower!
|
|
|
|
# no idea why given we only move labels for the most part?
|
2021-07-21 19:50:09 +00:00
|
|
|
# self.setCacheMode(QtWidgets.QGraphicsItem.DeviceCoordinateCache)
|
2020-12-29 19:34:25 +00:00
|
|
|
|
2022-11-14 20:09:00 +00:00
|
|
|
self.pi = plotitem
|
2021-03-31 18:24:39 +00:00
|
|
|
self._dpi_font = _font
|
2020-10-27 14:50:28 +00:00
|
|
|
|
2020-11-02 17:02:05 +00:00
|
|
|
self.setTickFont(_font.font)
|
2021-03-31 18:24:39 +00:00
|
|
|
font_size = self._dpi_font.font.pixelSize()
|
|
|
|
|
2022-11-14 20:09:00 +00:00
|
|
|
style_conf = {
|
|
|
|
'textFillLimits': [(0, 0.5)],
|
|
|
|
'tickFont': self._dpi_font.font,
|
|
|
|
|
|
|
|
}
|
|
|
|
text_offset = None
|
2021-03-31 18:24:39 +00:00
|
|
|
if self.orientation in ('bottom',):
|
|
|
|
text_offset = floor(0.25 * font_size)
|
|
|
|
|
|
|
|
elif self.orientation in ('left', 'right'):
|
|
|
|
text_offset = floor(font_size / 2)
|
|
|
|
|
2022-11-14 20:09:00 +00:00
|
|
|
if text_offset:
|
|
|
|
style_conf.update({
|
|
|
|
# offset of text *away from* axis line in px
|
|
|
|
# use approx. half the font pixel size (height)
|
|
|
|
'tickTextOffset': text_offset,
|
|
|
|
})
|
2020-10-27 14:50:28 +00:00
|
|
|
|
2022-11-14 20:09:00 +00:00
|
|
|
self.setStyle(**style_conf)
|
2020-11-02 17:02:05 +00:00
|
|
|
self.setTickFont(_font.font)
|
2022-02-02 13:05:10 +00:00
|
|
|
|
2022-01-17 23:42:24 +00:00
|
|
|
# NOTE: this is for surrounding "border"
|
2020-10-27 03:34:48 +00:00
|
|
|
self.setPen(_axis_pen)
|
2022-02-02 13:05:10 +00:00
|
|
|
|
2022-01-17 23:42:24 +00:00
|
|
|
# this is the text color
|
2022-02-02 13:05:10 +00:00
|
|
|
self.text_color = text_color
|
|
|
|
|
2022-12-23 19:21:55 +00:00
|
|
|
# generate a bounding rect based on sizing to a "typical"
|
|
|
|
# maximum length-ed string defined as init default.
|
2020-11-02 17:02:05 +00:00
|
|
|
self.typical_br = _font._qfm.boundingRect(typical_max_str)
|
2020-10-27 14:50:28 +00:00
|
|
|
|
|
|
|
# size the pertinent axis dimension to a "typical value"
|
2021-02-11 16:48:19 +00:00
|
|
|
self.size_to_values()
|
|
|
|
|
2022-10-31 18:23:29 +00:00
|
|
|
# NOTE: requires override ``.tickValues()`` method seen below.
|
|
|
|
if lru_cache_tick_strings:
|
|
|
|
self.tickStrings = lru_cache(
|
|
|
|
maxsize=2**20
|
|
|
|
)(self.tickStrings)
|
|
|
|
|
2022-11-14 20:09:00 +00:00
|
|
|
# axis "sticky" labels
|
|
|
|
self._stickies: dict[str, YAxisLabel] = {}
|
|
|
|
|
2022-10-31 18:23:29 +00:00
|
|
|
# NOTE: only overriden to cast tick values entries into tuples
|
|
|
|
# for use with the lru caching.
|
|
|
|
def tickValues(
|
|
|
|
self,
|
|
|
|
minVal: float,
|
|
|
|
maxVal: float,
|
|
|
|
size: int,
|
|
|
|
|
|
|
|
) -> list[tuple[float, tuple[str]]]:
|
|
|
|
'''
|
|
|
|
Repack tick values into tuples for lru caching.
|
|
|
|
|
|
|
|
'''
|
|
|
|
ticks = []
|
|
|
|
for scalar, values in super().tickValues(minVal, maxVal, size):
|
|
|
|
ticks.append((
|
|
|
|
scalar,
|
|
|
|
tuple(values), # this
|
|
|
|
))
|
|
|
|
|
|
|
|
return ticks
|
|
|
|
|
2022-02-02 13:05:10 +00:00
|
|
|
@property
|
|
|
|
def text_color(self) -> str:
|
|
|
|
return self._text_color
|
|
|
|
|
|
|
|
@text_color.setter
|
|
|
|
def text_color(self, text_color: str) -> None:
|
|
|
|
self.setTextPen(pg.mkPen(hcolor(text_color)))
|
|
|
|
self._text_color = text_color
|
|
|
|
|
2021-02-11 16:48:19 +00:00
|
|
|
def size_to_values(self) -> None:
|
|
|
|
pass
|
2020-10-27 14:50:28 +00:00
|
|
|
|
2022-05-15 17:38:22 +00:00
|
|
|
def txt_offsets(self) -> tuple[int, int]:
|
2021-02-11 16:48:19 +00:00
|
|
|
return tuple(self.style['tickTextOffset'])
|
|
|
|
|
2022-11-14 20:09:00 +00:00
|
|
|
def add_sticky(
|
|
|
|
self,
|
|
|
|
pi: pgo.PlotItem,
|
|
|
|
name: None | str = None,
|
|
|
|
digits: None | int = 2,
|
2022-12-23 19:21:55 +00:00
|
|
|
bg_color='default',
|
|
|
|
fg_color='black',
|
2022-11-14 20:09:00 +00:00
|
|
|
|
|
|
|
) -> YAxisLabel:
|
|
|
|
|
|
|
|
# if the sticky is for our symbol
|
|
|
|
# use the tick size precision for display
|
|
|
|
name = name or pi.name
|
|
|
|
digits = digits or 2
|
|
|
|
|
|
|
|
# TODO: ``._ysticks`` should really be an attr on each
|
2022-12-23 19:21:55 +00:00
|
|
|
# ``PlotItem`` now instead of the containing widget (because of
|
|
|
|
# overlays) ?
|
2022-11-14 20:09:00 +00:00
|
|
|
|
|
|
|
# add y-axis "last" value label
|
|
|
|
sticky = self._stickies[name] = YAxisLabel(
|
|
|
|
pi=pi,
|
|
|
|
parent=self,
|
2022-12-23 19:21:55 +00:00
|
|
|
digits=digits, # TODO: pass this from symbol data
|
|
|
|
opacity=0.9, # slight see-through
|
2022-11-14 20:09:00 +00:00
|
|
|
bg_color=bg_color,
|
2022-12-23 19:21:55 +00:00
|
|
|
fg_color=fg_color,
|
2022-11-14 20:09:00 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
pi.sigRangeChanged.connect(sticky.update_on_resize)
|
|
|
|
return sticky
|
|
|
|
|
2020-10-27 14:50:28 +00:00
|
|
|
|
|
|
|
class PriceAxis(Axis):
|
|
|
|
|
2022-01-14 14:14:58 +00:00
|
|
|
def __init__(
|
|
|
|
self,
|
|
|
|
*args,
|
2022-01-16 00:36:23 +00:00
|
|
|
min_tick: int = 2,
|
2022-01-17 23:42:24 +00:00
|
|
|
title: str = '',
|
2023-02-21 14:14:26 +00:00
|
|
|
formatter: Callable[[float], str] | None = None,
|
2022-01-14 14:14:58 +00:00
|
|
|
**kwargs
|
|
|
|
|
|
|
|
) -> None:
|
|
|
|
super().__init__(*args, **kwargs)
|
2022-01-16 00:36:23 +00:00
|
|
|
self.formatter = formatter
|
|
|
|
self._min_tick: int = min_tick
|
2022-01-17 23:42:24 +00:00
|
|
|
self.title = None
|
|
|
|
|
|
|
|
def set_title(
|
|
|
|
self,
|
|
|
|
title: str,
|
2023-02-21 14:14:26 +00:00
|
|
|
view: ChartView | None = None,
|
|
|
|
color: str | None = None,
|
2022-01-17 23:42:24 +00:00
|
|
|
|
|
|
|
) -> Label:
|
|
|
|
'''
|
|
|
|
Set a sane UX label using our built-in ``Label``.
|
|
|
|
|
|
|
|
'''
|
|
|
|
# XXX: built-in labels but they're huge, and placed weird..
|
|
|
|
# self.setLabel(title)
|
|
|
|
# self.showLabel()
|
|
|
|
|
2023-06-02 20:59:37 +00:00
|
|
|
label: Label | None = self.title
|
|
|
|
if label is None:
|
|
|
|
label = self.title = Label(
|
|
|
|
view=view or self.linkedView(),
|
|
|
|
fmt_str=title,
|
|
|
|
color=color or self.text_color,
|
|
|
|
parent=self,
|
|
|
|
# update_on_range_change=False,
|
|
|
|
)
|
|
|
|
else:
|
|
|
|
label.fmt_str: str = title
|
2022-01-17 23:42:24 +00:00
|
|
|
|
|
|
|
def below_axis() -> QPointF:
|
|
|
|
return QPointF(
|
|
|
|
0,
|
|
|
|
self.size().height(),
|
|
|
|
)
|
|
|
|
|
|
|
|
# XXX: doesn't work? have to pass it above
|
|
|
|
# label.txt.setParent(self)
|
|
|
|
label.scene_anchor = below_axis
|
|
|
|
label.render()
|
|
|
|
label.show()
|
|
|
|
label.update()
|
|
|
|
return label
|
2022-01-16 00:36:23 +00:00
|
|
|
|
|
|
|
def set_min_tick(
|
|
|
|
self,
|
|
|
|
size: int
|
|
|
|
) -> None:
|
|
|
|
self._min_tick = size
|
2022-01-14 14:14:58 +00:00
|
|
|
|
2021-02-11 16:48:19 +00:00
|
|
|
def size_to_values(self) -> None:
|
2020-10-27 14:50:28 +00:00
|
|
|
self.setWidth(self.typical_br.width())
|
2020-06-15 14:48:00 +00:00
|
|
|
|
|
|
|
# XXX: drop for now since it just eats up h space
|
|
|
|
|
2022-01-09 16:31:15 +00:00
|
|
|
def tickStrings(
|
|
|
|
self,
|
2022-01-16 00:36:23 +00:00
|
|
|
vals: tuple[float],
|
|
|
|
scale: float,
|
|
|
|
spacing: float,
|
|
|
|
|
|
|
|
) -> list[str]:
|
2022-01-14 14:14:58 +00:00
|
|
|
# TODO: figure out how to enforce min tick spacing by passing it
|
|
|
|
# into the parent type
|
|
|
|
digits = max(
|
|
|
|
float_digits(spacing * scale),
|
|
|
|
self._min_tick,
|
|
|
|
)
|
2022-01-17 23:42:24 +00:00
|
|
|
if self.title:
|
|
|
|
self.title.update()
|
2020-10-23 00:22:21 +00:00
|
|
|
|
|
|
|
# print(f'vals: {vals}\nscale: {scale}\nspacing: {spacing}')
|
|
|
|
# print(f'digits: {digits}')
|
|
|
|
|
2022-01-16 00:36:23 +00:00
|
|
|
if not self.formatter:
|
2022-01-14 14:14:58 +00:00
|
|
|
return [
|
|
|
|
('{value:,.{digits}f}').format(
|
|
|
|
digits=digits,
|
|
|
|
value=v,
|
|
|
|
).replace(',', ' ') for v in vals
|
|
|
|
]
|
|
|
|
else:
|
2022-01-16 00:36:23 +00:00
|
|
|
return list(map(self.formatter, vals))
|
2020-06-15 14:48:00 +00:00
|
|
|
|
|
|
|
|
2020-10-27 14:50:28 +00:00
|
|
|
class DynamicDateAxis(Axis):
|
|
|
|
|
2020-08-31 21:18:02 +00:00
|
|
|
# time formats mapped by seconds between bars
|
|
|
|
tick_tpl = {
|
2020-11-05 17:08:02 +00:00
|
|
|
60 * 60 * 24: '%Y-%b-%d',
|
2020-08-31 21:18:02 +00:00
|
|
|
60: '%H:%M',
|
|
|
|
30: '%H:%M:%S',
|
|
|
|
5: '%H:%M:%S',
|
2020-11-16 22:39:14 +00:00
|
|
|
1: '%H:%M:%S',
|
2020-08-31 21:18:02 +00:00
|
|
|
}
|
2020-06-15 14:48:00 +00:00
|
|
|
|
2021-02-11 16:48:19 +00:00
|
|
|
def size_to_values(self) -> None:
|
2020-11-16 22:39:14 +00:00
|
|
|
self.setHeight(self.typical_br.height() + 1)
|
2020-06-15 14:48:00 +00:00
|
|
|
|
2020-08-31 21:18:02 +00:00
|
|
|
def _indexes_to_timestrs(
|
|
|
|
self,
|
2022-05-15 17:38:22 +00:00
|
|
|
indexes: list[int],
|
2022-01-16 00:36:23 +00:00
|
|
|
|
2022-05-15 17:38:22 +00:00
|
|
|
) -> list[str]:
|
2020-11-02 17:02:05 +00:00
|
|
|
|
2022-11-14 20:09:00 +00:00
|
|
|
# XX: ARGGGGG AG:LKSKDJF:LKJSDFD
|
|
|
|
chart = self.pi.chart_widget
|
|
|
|
|
2022-12-23 19:21:55 +00:00
|
|
|
viz = chart._vizs[chart.name]
|
|
|
|
shm = viz.shm
|
|
|
|
array = shm.array
|
2023-02-28 20:01:00 +00:00
|
|
|
ifield = viz.index_field
|
|
|
|
index = array[ifield]
|
|
|
|
i_0, i_l = index[0], index[-1]
|
2022-12-23 19:21:55 +00:00
|
|
|
|
2022-12-23 20:45:57 +00:00
|
|
|
# edge cases
|
2022-12-23 19:21:55 +00:00
|
|
|
if (
|
2022-12-23 20:45:57 +00:00
|
|
|
not indexes
|
|
|
|
or
|
2022-12-23 19:21:55 +00:00
|
|
|
(indexes[0] < i_0
|
|
|
|
and indexes[-1] < i_l)
|
|
|
|
or
|
|
|
|
(indexes[0] > i_0
|
|
|
|
and indexes[-1] > i_l)
|
|
|
|
):
|
2023-02-28 20:01:00 +00:00
|
|
|
# print(f"x-label indexes empty edge case: {indexes}")
|
2022-12-23 19:21:55 +00:00
|
|
|
return []
|
|
|
|
|
2023-02-28 20:01:00 +00:00
|
|
|
if ifield == 'index':
|
|
|
|
arr_len = index.shape[0]
|
2022-12-23 19:21:55 +00:00
|
|
|
first = shm._first.value
|
2023-02-28 20:01:00 +00:00
|
|
|
times = array['time']
|
2022-12-23 19:21:55 +00:00
|
|
|
epochs = times[
|
|
|
|
list(
|
|
|
|
map(
|
|
|
|
int,
|
|
|
|
filter(
|
|
|
|
lambda i: i > 0 and i < arr_len,
|
|
|
|
(i - first for i in indexes)
|
|
|
|
)
|
|
|
|
)
|
2020-12-14 17:22:05 +00:00
|
|
|
)
|
2022-12-23 19:21:55 +00:00
|
|
|
]
|
|
|
|
else:
|
|
|
|
epochs = list(map(int, indexes))
|
2020-12-14 17:22:05 +00:00
|
|
|
|
2020-08-31 21:18:02 +00:00
|
|
|
# TODO: **don't** have this hard coded shift to EST
|
2022-05-15 17:38:22 +00:00
|
|
|
# delay = times[-1] - times[-2]
|
2022-12-23 19:21:55 +00:00
|
|
|
dts = np.array(
|
|
|
|
epochs,
|
|
|
|
dtype='datetime64[s]',
|
|
|
|
)
|
2022-05-15 17:38:22 +00:00
|
|
|
|
|
|
|
# see units listing:
|
|
|
|
# https://numpy.org/devdocs/reference/arrays.datetime.html#datetime-units
|
|
|
|
return list(np.datetime_as_string(dts))
|
2020-11-02 17:02:05 +00:00
|
|
|
|
2022-05-15 17:38:22 +00:00
|
|
|
# TODO: per timeframe formatting?
|
|
|
|
# - we probably need this based on zoom now right?
|
|
|
|
# prec = self.np_dt_precision[delay]
|
|
|
|
# return dts.strftime(self.tick_tpl[delay])
|
2020-06-18 00:45:47 +00:00
|
|
|
|
2022-01-09 16:31:15 +00:00
|
|
|
def tickStrings(
|
|
|
|
self,
|
|
|
|
values: tuple[float],
|
2022-01-16 00:36:23 +00:00
|
|
|
scale: float,
|
|
|
|
spacing: float,
|
|
|
|
|
|
|
|
) -> list[str]:
|
2022-12-23 19:21:55 +00:00
|
|
|
|
|
|
|
return self._indexes_to_timestrs(values)
|
|
|
|
|
|
|
|
# NOTE: handy for debugging the lru cache
|
2022-01-09 16:31:15 +00:00
|
|
|
# info = self.tickStrings.cache_info()
|
|
|
|
# print(info)
|
2020-06-15 14:48:00 +00:00
|
|
|
|
|
|
|
|
|
|
|
class AxisLabel(pg.GraphicsObject):
|
|
|
|
|
2022-12-23 19:21:55 +00:00
|
|
|
# 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
|
2020-10-27 03:34:48 +00:00
|
|
|
|
2020-06-16 17:32:03 +00:00
|
|
|
def __init__(
|
|
|
|
self,
|
2021-02-11 16:48:19 +00:00
|
|
|
parent: pg.GraphicsItem,
|
2020-10-25 00:04:57 +00:00
|
|
|
digits: int = 2,
|
2021-02-11 16:48:19 +00:00
|
|
|
|
2022-12-23 19:21:55 +00:00
|
|
|
bg_color: str = 'default',
|
2020-10-25 00:04:57 +00:00
|
|
|
fg_color: str = 'black',
|
2022-12-23 19:21:55 +00:00
|
|
|
opacity: int = .8, # XXX: seriously don't set this to 0
|
2021-03-29 20:01:05 +00:00
|
|
|
font_size: str = 'default',
|
2021-02-11 16:48:19 +00:00
|
|
|
|
2021-02-08 11:40:11 +00:00
|
|
|
use_arrow: bool = True,
|
2021-02-11 16:48:19 +00:00
|
|
|
|
2020-12-29 19:34:25 +00:00
|
|
|
) -> None:
|
|
|
|
|
2021-02-11 16:48:19 +00:00
|
|
|
super().__init__()
|
|
|
|
self.setParentItem(parent)
|
|
|
|
|
2020-11-02 20:43:19 +00:00
|
|
|
self.setFlag(self.ItemIgnoresTransformations)
|
2022-12-23 19:21:55 +00:00
|
|
|
self.setZValue(100)
|
2021-01-26 16:27:50 +00:00
|
|
|
|
2020-12-29 19:34:25 +00:00
|
|
|
# XXX: pretty sure this is faster
|
2021-07-21 19:50:09 +00:00
|
|
|
self.setCacheMode(QtWidgets.QGraphicsItem.DeviceCoordinateCache)
|
2020-11-02 20:43:19 +00:00
|
|
|
|
2021-02-11 16:48:19 +00:00
|
|
|
self._parent = parent
|
|
|
|
|
2020-06-15 14:48:00 +00:00
|
|
|
self.opacity = opacity
|
|
|
|
self.label_str = ''
|
|
|
|
self.digits = digits
|
2020-11-02 17:02:05 +00:00
|
|
|
|
2020-10-27 03:34:48 +00:00
|
|
|
self._txt_br: QtCore.QRect = None
|
2020-06-15 14:48:00 +00:00
|
|
|
|
2023-06-19 19:13:01 +00:00
|
|
|
self._dpifont = DpiAwareFont(_font_size_key=font_size)
|
2021-01-26 16:27:50 +00:00
|
|
|
self._dpifont.configure_to_dpi()
|
2020-11-02 20:27:48 +00:00
|
|
|
|
2020-10-25 00:04:57 +00:00
|
|
|
self.bg_color = pg.mkColor(hcolor(bg_color))
|
|
|
|
self.fg_color = pg.mkColor(hcolor(fg_color))
|
|
|
|
|
2021-02-08 11:40:11 +00:00
|
|
|
self._use_arrow = use_arrow
|
|
|
|
|
|
|
|
# create triangle path
|
|
|
|
self.path = None
|
2020-10-27 03:34:48 +00:00
|
|
|
self.rect = None
|
2020-10-25 00:04:57 +00:00
|
|
|
|
2022-01-21 12:32:15 +00:00
|
|
|
self._pw = self.pixelWidth()
|
|
|
|
|
2020-12-29 19:34:25 +00:00
|
|
|
def paint(
|
|
|
|
self,
|
|
|
|
p: QtGui.QPainter,
|
|
|
|
opt: QtWidgets.QStyleOptionGraphicsItem,
|
|
|
|
w: QtWidgets.QWidget
|
2022-12-23 19:21:55 +00:00
|
|
|
|
2020-12-29 19:34:25 +00:00
|
|
|
) -> None:
|
2022-12-23 19:21:55 +00:00
|
|
|
'''
|
|
|
|
Draw a filled rectangle based on the size of ``.label_str`` text.
|
2021-02-11 16:48:19 +00:00
|
|
|
|
|
|
|
Subtypes can customize further by overloading ``.draw()``.
|
|
|
|
|
2022-12-23 19:21:55 +00:00
|
|
|
'''
|
2020-06-15 14:48:00 +00:00
|
|
|
if self.label_str:
|
2020-10-27 03:34:48 +00:00
|
|
|
|
2021-02-08 11:40:11 +00:00
|
|
|
# if not self.rect:
|
|
|
|
self._size_br_from_str(self.label_str)
|
2020-10-27 03:34:48 +00:00
|
|
|
|
2020-11-03 21:21:21 +00:00
|
|
|
# can be overrided in subtype
|
|
|
|
self.draw(p, self.rect)
|
2020-08-31 21:18:02 +00:00
|
|
|
|
2021-02-11 16:48:19 +00:00
|
|
|
p.setFont(self._dpifont.font)
|
|
|
|
p.setPen(self.fg_color)
|
2022-12-23 19:21:55 +00:00
|
|
|
p.drawText(
|
|
|
|
self.rect,
|
|
|
|
self.text_flags,
|
|
|
|
self.label_str,
|
|
|
|
)
|
2020-08-30 16:27:41 +00:00
|
|
|
|
2020-11-03 21:21:21 +00:00
|
|
|
def draw(
|
|
|
|
self,
|
|
|
|
p: QtGui.QPainter,
|
|
|
|
rect: QtCore.QRectF
|
|
|
|
) -> None:
|
|
|
|
|
2022-12-23 19:21:55 +00:00
|
|
|
p.setOpacity(self.opacity)
|
|
|
|
|
2021-02-08 11:40:11 +00:00
|
|
|
if self._use_arrow:
|
|
|
|
if not self.path:
|
|
|
|
self._draw_arrow_path()
|
|
|
|
|
|
|
|
p.drawPath(self.path)
|
|
|
|
p.fillPath(self.path, pg.mkBrush(self.bg_color))
|
|
|
|
|
2021-06-22 11:17:49 +00:00
|
|
|
# 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
|
2022-12-23 19:21:55 +00:00
|
|
|
p.fillRect(
|
|
|
|
self.rect,
|
|
|
|
self.bg_color,
|
|
|
|
)
|
2021-02-08 11:40:11 +00:00
|
|
|
|
2020-08-30 16:27:41 +00:00
|
|
|
def boundingRect(self): # noqa
|
2022-01-17 23:42:24 +00:00
|
|
|
'''
|
|
|
|
Size the graphics space from the text contents.
|
2021-02-11 16:48:19 +00:00
|
|
|
|
2022-01-17 23:42:24 +00:00
|
|
|
'''
|
2021-01-04 19:45:12 +00:00
|
|
|
if self.label_str:
|
|
|
|
self._size_br_from_str(self.label_str)
|
2021-02-08 11:40:11 +00:00
|
|
|
|
|
|
|
# if self.path:
|
|
|
|
# self.tl = self.path.controlPointRect().topLeft()
|
|
|
|
if not self.path:
|
|
|
|
self.tl = self.rect.topLeft()
|
|
|
|
|
|
|
|
return QtCore.QRectF(
|
|
|
|
self.tl,
|
|
|
|
self.rect.bottomRight(),
|
|
|
|
)
|
2020-11-02 17:02:05 +00:00
|
|
|
|
2021-01-04 19:45:12 +00:00
|
|
|
return QtCore.QRectF()
|
2020-11-02 17:02:05 +00:00
|
|
|
|
2022-01-17 23:42:24 +00:00
|
|
|
# TODO: but the input probably needs to be the "len" of
|
|
|
|
# the current text value:
|
|
|
|
@lru_cache
|
|
|
|
def _size_br_from_str(
|
|
|
|
self,
|
|
|
|
value: str
|
2020-10-27 03:34:48 +00:00
|
|
|
|
2022-01-17 23:42:24 +00:00
|
|
|
) -> tuple[float, float]:
|
|
|
|
'''
|
|
|
|
Do our best to render the bounding rect to a set margin
|
2020-10-27 14:50:28 +00:00
|
|
|
around provided string contents.
|
2020-08-30 16:27:41 +00:00
|
|
|
|
2022-01-17 23:42:24 +00:00
|
|
|
'''
|
2020-11-02 17:02:05 +00:00
|
|
|
# size the filled rect to text and/or parent axis
|
2021-01-26 16:27:50 +00:00
|
|
|
# if not self._txt_br:
|
2022-01-17 23:42:24 +00:00
|
|
|
# # XXX: this can't be called until stuff is rendered?
|
2021-01-26 16:27:50 +00:00
|
|
|
# self._txt_br = self._dpifont.boundingRect(value)
|
2020-11-02 17:02:05 +00:00
|
|
|
|
2021-02-08 11:40:11 +00:00
|
|
|
txt_br = self._txt_br = self._dpifont.boundingRect(value)
|
|
|
|
txt_h, txt_w = txt_br.height(), txt_br.width()
|
2022-01-17 23:42:24 +00:00
|
|
|
# print(f'wsw: {self._dpifont.boundingRect(" ")}')
|
2021-02-11 16:48:19 +00:00
|
|
|
|
2022-12-23 19:21:55 +00:00
|
|
|
# allow subtypes to override width and height
|
2020-10-27 14:50:28 +00:00
|
|
|
h, w = self.size_hint()
|
2020-11-02 17:02:05 +00:00
|
|
|
|
2020-10-27 14:50:28 +00:00
|
|
|
self.rect = QtCore.QRectF(
|
2022-12-23 19:21:55 +00:00
|
|
|
|
|
|
|
# relative bounds offsets
|
|
|
|
self._x_br_offset,
|
|
|
|
self._y_br_offset,
|
|
|
|
|
2021-06-22 11:17:49 +00:00
|
|
|
(w or txt_w) + self._x_margin / 2,
|
2022-12-23 19:21:55 +00:00
|
|
|
|
|
|
|
(h or txt_h) * self._y_txt_h_scaling + (self._y_margin / 2),
|
2020-10-27 14:50:28 +00:00
|
|
|
)
|
2021-02-08 11:40:11 +00:00
|
|
|
# print(self.rect)
|
|
|
|
# hb = self.path.controlPointRect()
|
|
|
|
# hb_size = hb.size()
|
2020-08-30 16:27:41 +00:00
|
|
|
|
2022-01-17 23:42:24 +00:00
|
|
|
return (self.rect.width(), self.rect.height())
|
2020-08-30 16:27:41 +00:00
|
|
|
|
|
|
|
# _common_text_flags = (
|
|
|
|
# QtCore.Qt.TextDontClip |
|
|
|
|
# QtCore.Qt.AlignCenter |
|
|
|
|
# QtCore.Qt.AlignTop |
|
|
|
|
# QtCore.Qt.AlignHCenter |
|
|
|
|
# QtCore.Qt.AlignVCenter
|
|
|
|
# )
|
|
|
|
|
2020-06-15 14:48:00 +00:00
|
|
|
|
|
|
|
class XAxisLabel(AxisLabel):
|
2021-02-08 11:40:11 +00:00
|
|
|
_x_margin = 8
|
2020-06-15 14:48:00 +00:00
|
|
|
|
|
|
|
text_flags = (
|
2020-08-30 16:27:41 +00:00
|
|
|
QtCore.Qt.TextDontClip
|
|
|
|
| QtCore.Qt.AlignCenter
|
2020-06-15 14:48:00 +00:00
|
|
|
)
|
|
|
|
|
2022-05-15 17:38:22 +00:00
|
|
|
def size_hint(self) -> tuple[float, float]:
|
2020-10-27 14:50:28 +00:00
|
|
|
# size to parent axis height
|
2021-02-11 16:48:19 +00:00
|
|
|
return self._parent.height(), None
|
2020-10-27 14:50:28 +00:00
|
|
|
|
2020-08-31 21:18:02 +00:00
|
|
|
def update_label(
|
|
|
|
self,
|
|
|
|
abs_pos: QPointF, # scene coords
|
2020-11-05 17:08:02 +00:00
|
|
|
value: float, # data for text
|
2021-02-08 11:40:11 +00:00
|
|
|
offset: int = 0 # if have margins, k?
|
2022-01-21 12:32:15 +00:00
|
|
|
|
2020-08-31 21:18:02 +00:00
|
|
|
) -> None:
|
2020-11-02 17:02:05 +00:00
|
|
|
|
2021-02-11 16:48:19 +00:00
|
|
|
timestrs = self._parent._indexes_to_timestrs([int(value)])
|
2020-10-27 14:50:28 +00:00
|
|
|
|
2022-05-15 17:38:22 +00:00
|
|
|
if not len(timestrs):
|
2020-09-09 14:47:08 +00:00
|
|
|
return
|
2020-10-27 14:50:28 +00:00
|
|
|
|
2021-02-08 11:40:11 +00:00
|
|
|
pad = 1*' '
|
2022-05-15 17:38:22 +00:00
|
|
|
self.label_str = pad + str(timestrs[0]) + pad
|
2021-02-08 11:40:11 +00:00
|
|
|
|
2021-02-11 16:48:19 +00:00
|
|
|
_, y_offset = self._parent.txt_offsets()
|
2020-11-02 17:02:05 +00:00
|
|
|
|
2020-11-02 20:27:48 +00:00
|
|
|
w = self.boundingRect().width()
|
2021-02-11 16:48:19 +00:00
|
|
|
|
2022-01-21 12:32:15 +00:00
|
|
|
self.setPos(
|
|
|
|
QPointF(
|
|
|
|
abs_pos.x() - w/2 - self._pw,
|
|
|
|
y_offset/2,
|
|
|
|
)
|
|
|
|
)
|
2020-11-05 17:08:02 +00:00
|
|
|
self.update()
|
2020-06-15 14:48:00 +00:00
|
|
|
|
2021-02-08 11:40:11 +00:00
|
|
|
def _draw_arrow_path(self):
|
2021-02-11 16:48:19 +00:00
|
|
|
y_offset = self._parent.style['tickTextOffset'][1]
|
2021-07-21 20:16:06 +00:00
|
|
|
path = QtGui.QPainterPath()
|
2021-02-08 11:40:11 +00:00
|
|
|
h, w = self.rect.height(), self.rect.width()
|
2022-01-21 12:32:15 +00:00
|
|
|
middle = w/2 - self._pw * 0.5
|
2021-02-08 11:40:11 +00:00
|
|
|
aw = h/2
|
|
|
|
left = middle - aw
|
|
|
|
right = middle + aw
|
|
|
|
path.moveTo(left, 0)
|
|
|
|
path.lineTo(middle, -y_offset)
|
|
|
|
path.lineTo(right, 0)
|
|
|
|
path.closeSubpath()
|
|
|
|
self.path = path
|
2021-02-11 16:48:19 +00:00
|
|
|
|
|
|
|
# top left point is local origin and tip of the arrow path
|
2021-02-08 11:40:11 +00:00
|
|
|
self.tl = QtCore.QPointF(0, -y_offset)
|
|
|
|
|
2020-06-15 14:48:00 +00:00
|
|
|
|
|
|
|
class YAxisLabel(AxisLabel):
|
2022-11-13 23:23:33 +00:00
|
|
|
_y_margin: int = 4
|
2020-06-15 14:48:00 +00:00
|
|
|
|
|
|
|
text_flags = (
|
2020-12-09 13:53:09 +00:00
|
|
|
QtCore.Qt.AlignLeft
|
|
|
|
# QtCore.Qt.AlignHCenter
|
2020-11-06 20:24:01 +00:00
|
|
|
| QtCore.Qt.AlignVCenter
|
2020-08-30 16:27:41 +00:00
|
|
|
| QtCore.Qt.TextDontClip
|
2020-06-15 14:48:00 +00:00
|
|
|
)
|
|
|
|
|
2021-02-08 11:40:11 +00:00
|
|
|
def __init__(
|
|
|
|
self,
|
2022-11-14 20:09:00 +00:00
|
|
|
pi: pgo.PlotItem,
|
2021-02-08 11:40:11 +00:00
|
|
|
*args,
|
|
|
|
**kwargs
|
|
|
|
) -> None:
|
|
|
|
|
|
|
|
super().__init__(*args, **kwargs)
|
|
|
|
|
2022-11-14 20:09:00 +00:00
|
|
|
self._pi = pi
|
|
|
|
pi.sigRangeChanged.connect(self.update_on_resize)
|
2021-02-11 16:48:19 +00:00
|
|
|
|
2021-02-08 11:40:11 +00:00
|
|
|
self._last_datum = (None, None)
|
|
|
|
|
2022-11-13 23:23:33 +00:00
|
|
|
self.x_offset = 0
|
2021-02-08 11:40:11 +00:00
|
|
|
# pull text offset from axis from parent axis
|
2021-02-11 16:48:19 +00:00
|
|
|
if getattr(self._parent, 'txt_offsets', False):
|
|
|
|
self.x_offset, y_offset = self._parent.txt_offsets()
|
2021-02-08 11:40:11 +00:00
|
|
|
|
2022-05-15 17:38:22 +00:00
|
|
|
def size_hint(self) -> tuple[float, float]:
|
2022-01-17 23:42:24 +00:00
|
|
|
# size to parent axis width(-ish)
|
|
|
|
wsh = self._dpifont.boundingRect(' ').height() / 2
|
|
|
|
return (
|
|
|
|
None,
|
|
|
|
self._parent.size().width() - wsh,
|
|
|
|
)
|
2020-10-27 14:50:28 +00:00
|
|
|
|
2020-08-30 16:27:41 +00:00
|
|
|
def update_label(
|
|
|
|
self,
|
|
|
|
abs_pos: QPointF, # scene coords
|
2020-11-05 17:08:02 +00:00
|
|
|
value: float, # data for text
|
2021-02-08 11:40:11 +00:00
|
|
|
|
|
|
|
# on odd dimension and/or adds nice black line
|
2022-11-13 23:23:33 +00:00
|
|
|
x_offset: int = 0,
|
|
|
|
|
2020-08-30 16:27:41 +00:00
|
|
|
) -> None:
|
2020-11-02 17:02:05 +00:00
|
|
|
|
|
|
|
# this is read inside ``.paint()``
|
2021-02-08 11:40:11 +00:00
|
|
|
self.label_str = '{value:,.{digits}f}'.format(
|
2020-11-05 17:08:02 +00:00
|
|
|
digits=self.digits, value=value).replace(',', ' ')
|
2020-11-02 17:02:05 +00:00
|
|
|
|
2021-02-08 11:40:11 +00:00
|
|
|
# pull text offset from axis from parent axis
|
|
|
|
x_offset = x_offset or self.x_offset
|
|
|
|
|
2020-11-02 17:02:05 +00:00
|
|
|
br = self.boundingRect()
|
|
|
|
h = br.height()
|
2021-02-11 16:48:19 +00:00
|
|
|
|
2022-01-21 12:32:15 +00:00
|
|
|
self.setPos(
|
|
|
|
QPointF(
|
|
|
|
x_offset,
|
|
|
|
abs_pos.y() - h / 2 - self._pw,
|
|
|
|
)
|
|
|
|
)
|
2020-11-05 17:08:02 +00:00
|
|
|
self.update()
|
2020-08-30 16:27:41 +00:00
|
|
|
|
|
|
|
def update_on_resize(self, vr, r):
|
2022-01-17 23:42:24 +00:00
|
|
|
'''
|
|
|
|
This is a ``.sigRangeChanged()`` handler.
|
2020-11-05 17:08:02 +00:00
|
|
|
|
2022-01-17 23:42:24 +00:00
|
|
|
'''
|
2020-11-02 17:02:05 +00:00
|
|
|
index, last = self._last_datum
|
|
|
|
if index is not None:
|
2020-11-05 17:08:02 +00:00
|
|
|
self.update_from_data(index, last)
|
2020-11-02 17:02:05 +00:00
|
|
|
|
2020-08-30 16:27:41 +00:00
|
|
|
def update_from_data(
|
|
|
|
self,
|
|
|
|
index: int,
|
2020-10-19 15:37:28 +00:00
|
|
|
value: float,
|
2021-03-14 02:06:49 +00:00
|
|
|
_save_last: bool = True,
|
2022-01-17 23:42:24 +00:00
|
|
|
|
2020-08-30 16:27:41 +00:00
|
|
|
) -> None:
|
2022-01-17 23:42:24 +00:00
|
|
|
'''
|
|
|
|
Update the label's text contents **and** position from
|
2021-02-11 16:48:19 +00:00
|
|
|
a view box coordinate datum.
|
|
|
|
|
2022-01-17 23:42:24 +00:00
|
|
|
'''
|
2021-03-14 02:06:49 +00:00
|
|
|
if _save_last:
|
|
|
|
self._last_datum = (index, value)
|
|
|
|
|
2020-08-30 16:27:41 +00:00
|
|
|
self.update_label(
|
2022-11-14 20:09:00 +00:00
|
|
|
self._pi.mapFromView(QPointF(index, value)),
|
2020-10-19 15:37:28 +00:00
|
|
|
value
|
2020-08-30 16:27:41 +00:00
|
|
|
)
|
2021-02-08 11:40:11 +00:00
|
|
|
|
|
|
|
def _draw_arrow_path(self):
|
2021-02-11 16:48:19 +00:00
|
|
|
x_offset = self._parent.style['tickTextOffset'][0]
|
2021-07-21 20:16:06 +00:00
|
|
|
path = QtGui.QPainterPath()
|
2021-02-08 11:40:11 +00:00
|
|
|
h = self.rect.height()
|
|
|
|
path.moveTo(0, 0)
|
2022-01-21 12:32:15 +00:00
|
|
|
path.lineTo(-x_offset - h/4, h/2. - self._pw/2)
|
2021-02-08 11:40:11 +00:00
|
|
|
path.lineTo(0, h)
|
|
|
|
path.closeSubpath()
|
|
|
|
self.path = path
|
|
|
|
self.tl = path.controlPointRect().topLeft()
|