Add a dpi aware font wrapper type
parent
307c501763
commit
9e7aa3f9bf
|
@ -1,21 +1,23 @@
|
|||
"""
|
||||
Chart axes graphics and behavior.
|
||||
"""
|
||||
from typing import List, Tuple
|
||||
from typing import List, Tuple, Optional
|
||||
|
||||
import pandas as pd
|
||||
import pyqtgraph as pg
|
||||
from PyQt5 import QtCore, QtGui
|
||||
from PyQt5.QtCore import QPointF
|
||||
|
||||
from ._style import _font, hcolor
|
||||
from ._style import DpiAwareFont, hcolor, _font
|
||||
from ..data._source import float_digits
|
||||
|
||||
_axis_pen = pg.mkPen(hcolor('bracket'))
|
||||
|
||||
|
||||
class Axis(pg.AxisItem):
|
||||
"""A better axis that sizes to typical tick contents considering font size.
|
||||
|
||||
"""
|
||||
def __init__(
|
||||
self,
|
||||
linked_charts,
|
||||
|
@ -27,10 +29,10 @@ class Axis(pg.AxisItem):
|
|||
|
||||
super().__init__(**kwargs)
|
||||
|
||||
self.setTickFont(_font)
|
||||
self.setTickFont(_font.font)
|
||||
self.setStyle(**{
|
||||
'textFillLimits': [(0, 0.666)],
|
||||
'tickFont': _font,
|
||||
'tickFont': _font.font,
|
||||
# 'tickTextWidth': 100,
|
||||
# 'tickTextHeight': 20,
|
||||
# 'tickTextWidth': 40,
|
||||
|
@ -42,9 +44,9 @@ class Axis(pg.AxisItem):
|
|||
})
|
||||
# self.setLabel(**{'font-size': '10pt'})
|
||||
|
||||
self.setTickFont(_font)
|
||||
self.setTickFont(_font.font)
|
||||
self.setPen(_axis_pen)
|
||||
self.typical_br = _font._fm.boundingRect(typical_max_str)
|
||||
self.typical_br = _font._qfm.boundingRect(typical_max_str)
|
||||
|
||||
# size the pertinent axis dimension to a "typical value"
|
||||
self.resize()
|
||||
|
@ -92,17 +94,18 @@ class DynamicDateAxis(Axis):
|
|||
self,
|
||||
indexes: List[int],
|
||||
) -> List[str]:
|
||||
|
||||
bars = self.linked_charts.chart._array
|
||||
times = bars['time']
|
||||
bars_len = len(bars)
|
||||
# delay = times[-1] - times[times != times[-1]][-1]
|
||||
delay = times[-1] - times[-2]
|
||||
times = bars['time']
|
||||
|
||||
epochs = times[list(
|
||||
map(int, filter(lambda i: i < bars_len, indexes))
|
||||
)]
|
||||
# TODO: **don't** have this hard coded shift to EST
|
||||
dts = pd.to_datetime(epochs, unit='s') # - 4*pd.offsets.Hour()
|
||||
|
||||
delay = times[-1] - times[-2]
|
||||
return dts.strftime(self.tick_tpl[delay])
|
||||
|
||||
def tickStrings(self, values: List[float], scale, spacing):
|
||||
|
@ -111,9 +114,8 @@ class DynamicDateAxis(Axis):
|
|||
|
||||
class AxisLabel(pg.GraphicsObject):
|
||||
|
||||
_font = _font
|
||||
_w_margin = 0
|
||||
_h_margin = 3
|
||||
_h_margin = 0
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
|
@ -122,46 +124,62 @@ class AxisLabel(pg.GraphicsObject):
|
|||
bg_color: str = 'bracket',
|
||||
fg_color: str = 'black',
|
||||
opacity: int = 1,
|
||||
font_size: Optional[int] = None,
|
||||
):
|
||||
super().__init__(parent)
|
||||
self.parent = parent
|
||||
self.opacity = opacity
|
||||
self.label_str = ''
|
||||
self.digits = digits
|
||||
|
||||
self._txt_br: QtCore.QRect = None
|
||||
|
||||
self._dpifont = DpiAwareFont()
|
||||
self._dpifont.configure_to_dpi(_font._screen)
|
||||
if font_size is not None:
|
||||
self._dpifont._set_qfont_px_size(font_size)
|
||||
|
||||
# self._font._fm = QtGui.QFontMetrics(self._font)
|
||||
|
||||
self.bg_color = pg.mkColor(hcolor(bg_color))
|
||||
self.fg_color = pg.mkColor(hcolor(fg_color))
|
||||
|
||||
self.pic = QtGui.QPicture()
|
||||
p = QtGui.QPainter(self.pic)
|
||||
# self.pic = QtGui.QPicture()
|
||||
# p = QtGui.QPainter(self.pic)
|
||||
|
||||
self.rect = None
|
||||
|
||||
p.setPen(self.fg_color)
|
||||
p.setOpacity(self.opacity)
|
||||
# p.setPen(self.fg_color)
|
||||
|
||||
self.setFlag(self.ItemIgnoresTransformations)
|
||||
|
||||
def paint(self, p, option, widget):
|
||||
p.drawPicture(0, 0, self.pic)
|
||||
# p.drawPicture(0, 0, self.pic)
|
||||
p.setCompositionMode(QtGui.QPainter.CompositionMode_SourceOver)
|
||||
|
||||
if self.label_str:
|
||||
|
||||
if not self.rect:
|
||||
self._size_br_from_str(self.label_str)
|
||||
|
||||
p.setFont(_font)
|
||||
p.setFont(self._dpifont.font)
|
||||
p.setPen(self.fg_color)
|
||||
p.setOpacity(self.opacity)
|
||||
p.fillRect(self.rect, self.bg_color)
|
||||
|
||||
# this adds a nice black outline around the label for some odd
|
||||
# reason; ok by us
|
||||
p.drawRect(self.rect)
|
||||
|
||||
p.drawText(option.rect, self.text_flags, self.label_str)
|
||||
p.drawText(self.rect, self.text_flags, self.label_str)
|
||||
|
||||
def boundingRect(self): # noqa
|
||||
# if self.label_str:
|
||||
# self._size_br_from_str(self.label_str)
|
||||
# return self.rect
|
||||
|
||||
# return QtCore.QRectF()
|
||||
|
||||
return self.rect or QtCore.QRectF()
|
||||
|
||||
def _size_br_from_str(self, value: str) -> None:
|
||||
|
@ -169,9 +187,21 @@ class AxisLabel(pg.GraphicsObject):
|
|||
around provided string contents.
|
||||
|
||||
"""
|
||||
txt_br = self._font._fm.boundingRect(value)
|
||||
txt_h, txt_w = txt_br.height(), txt_br.width()
|
||||
# size the filled rect to text and/or parent axis
|
||||
br = self._txt_br = self._dpifont.boundingRect(value)
|
||||
|
||||
# px_per_char = self._font._fm.averageCharWidth()
|
||||
# br = br * 1.88
|
||||
txt_h, txt_w = br.height(), br.width()
|
||||
print(f'orig: {txt_h}')
|
||||
# txt_h = (br.topLeft() - br.bottomRight()).y()
|
||||
# txt_w = len(value) * px_per_char
|
||||
# txt_w *= 1.88
|
||||
# txt_h *= 1.88
|
||||
# print(f'calced: {txt_h}')
|
||||
|
||||
h, w = self.size_hint()
|
||||
|
||||
self.rect = QtCore.QRectF(
|
||||
0, 0,
|
||||
(w or txt_w) + self._w_margin,
|
||||
|
@ -190,15 +220,12 @@ class AxisLabel(pg.GraphicsObject):
|
|||
|
||||
class XAxisLabel(AxisLabel):
|
||||
|
||||
_w_margin = 8
|
||||
_w_margin = 0
|
||||
_h_margin = 0
|
||||
|
||||
text_flags = (
|
||||
QtCore.Qt.TextDontClip
|
||||
| QtCore.Qt.AlignCenter
|
||||
# | QtCore.Qt.AlignTop
|
||||
# | QtCore.Qt.AlignVCenter
|
||||
# | QtCore.Qt.AlignHCenter
|
||||
)
|
||||
|
||||
def size_hint(self) -> Tuple[float, float]:
|
||||
|
@ -211,43 +238,50 @@ class XAxisLabel(AxisLabel):
|
|||
data: float, # data for text
|
||||
offset: int = 1 # if have margins, k?
|
||||
) -> None:
|
||||
|
||||
timestrs = self.parent._indexes_to_timestrs([int(data)])
|
||||
|
||||
if not timestrs.any():
|
||||
return
|
||||
|
||||
self.label_str = timestrs[0]
|
||||
|
||||
width = self.boundingRect().width()
|
||||
new_pos = QPointF(abs_pos.x() - width / 2 - offset, 0)
|
||||
self.setPos(new_pos)
|
||||
self.setPos(QPointF(
|
||||
abs_pos.x() - width / 2, # - offset,
|
||||
0
|
||||
))
|
||||
|
||||
|
||||
class YAxisLabel(AxisLabel):
|
||||
|
||||
text_flags = (
|
||||
QtCore.Qt.AlignLeft
|
||||
# QtCore.Qt.AlignLeft
|
||||
QtCore.Qt.AlignVCenter
|
||||
| QtCore.Qt.TextDontClip
|
||||
| QtCore.Qt.AlignVCenter
|
||||
)
|
||||
|
||||
def size_hint(self) -> Tuple[float, float]:
|
||||
# size to parent axis width
|
||||
return None, self.parent.width()
|
||||
|
||||
def tick_to_string(self, tick_pos):
|
||||
# WTF IS THIS FORMAT?
|
||||
return ('{: ,.%df}' % self.digits).format(tick_pos).replace(',', ' ')
|
||||
|
||||
def update_label(
|
||||
self,
|
||||
abs_pos: QPointF, # scene coords
|
||||
data: float, # data for text
|
||||
offset: int = 1 # if have margins, k?
|
||||
) -> None:
|
||||
self.label_str = self.tick_to_string(data)
|
||||
height = self.boundingRect().height()
|
||||
new_pos = QPointF(0, abs_pos.y() - height / 2 - offset)
|
||||
self.setPos(new_pos)
|
||||
|
||||
# this is read inside ``.paint()``
|
||||
self.label_str = '{data: ,.{digits}f}'.format(
|
||||
digits=self.digits, data=data).replace(',', ' ')
|
||||
|
||||
br = self.boundingRect()
|
||||
h = br.height()
|
||||
self.setPos(QPointF(
|
||||
0,
|
||||
abs_pos.y() - h / 2 #- offset
|
||||
))
|
||||
|
||||
|
||||
class YSticky(YAxisLabel):
|
||||
|
@ -264,29 +298,40 @@ class YSticky(YAxisLabel):
|
|||
|
||||
self._chart = chart
|
||||
chart.sigRangeChanged.connect(self.update_on_resize)
|
||||
self._last_datum = (None, None)
|
||||
|
||||
def update_on_resize(self, vr, r):
|
||||
# TODO: add an `.index` to the array data-buffer layer
|
||||
# and make this way less shitty...
|
||||
chart = self._chart
|
||||
a = chart._array
|
||||
fields = a.dtype.fields
|
||||
if fields and 'close' in fields:
|
||||
index, last = a[-1][['index', 'close']]
|
||||
else:
|
||||
# non-ohlc case
|
||||
index = len(a) - 1
|
||||
last = a[chart.name][-1]
|
||||
index, last = self._last_datum
|
||||
if index is not None:
|
||||
self.update_from_data(
|
||||
index,
|
||||
last,
|
||||
)
|
||||
|
||||
# chart = self._chart
|
||||
# a = chart._array
|
||||
# fields = a.dtype.fields
|
||||
|
||||
# if fields and 'close' in fields:
|
||||
# index, last = a[-1][['index', 'close']]
|
||||
|
||||
# else: # non-ohlc case
|
||||
# index = len(a) - 1
|
||||
# last = a[chart.name][-1]
|
||||
|
||||
# self.update_from_data(
|
||||
# index,
|
||||
# last,
|
||||
# )
|
||||
|
||||
def update_from_data(
|
||||
self,
|
||||
index: int,
|
||||
value: float,
|
||||
) -> None:
|
||||
self._last_datum = (index, value)
|
||||
self.update_label(
|
||||
self._chart.mapFromView(QPointF(index, value)),
|
||||
value
|
||||
|
|
|
@ -2,26 +2,70 @@
|
|||
Qt UI styling.
|
||||
"""
|
||||
import pyqtgraph as pg
|
||||
from PyQt5 import QtGui
|
||||
from PyQt5 import QtCore, QtGui
|
||||
from qdarkstyle.palette import DarkPalette
|
||||
|
||||
from ..log import get_logger
|
||||
|
||||
log = get_logger(__name__)
|
||||
|
||||
|
||||
# def configure_font_to_dpi(screen: QtGui.QScreen):
|
||||
# """Set an appropriately sized font size depending on the screen DPI.
|
||||
|
||||
# If we end up needing to generalize this more here there are resources
|
||||
# listed in the script in ``snippets/qt_screen_info.py``.
|
||||
|
||||
# """
|
||||
# dpi = screen.physicalDotsPerInch()
|
||||
# font_size = round(_font_inches_we_like * dpi)
|
||||
# log.info(
|
||||
# f"\nscreen:{screen.name()} with DPI: {dpi}"
|
||||
# f"\nbest font size is {font_size}\n"
|
||||
# )
|
||||
|
||||
# global _font
|
||||
# _font.setPixelSize(font_size)
|
||||
# _font._fm = QtGui.QFontMetrics(_font)
|
||||
|
||||
# return _font
|
||||
|
||||
|
||||
# chart-wide font
|
||||
# font size 6px / 53 dpi (3x scaled down on 4k hidpi)
|
||||
_font_inches_we_like = 6 / 53
|
||||
|
||||
# use pixel size to be cross-resolution compatible?
|
||||
_font = QtGui.QFont("Hack")
|
||||
_font.setPixelSize(6) # default
|
||||
|
||||
# TODO: re-compute font size when main widget switches screens?
|
||||
# https://forum.qt.io/topic/54136/how-do-i-get-the-qscreen-my-widget-is-on-qapplication-desktop-screen-returns-a-qwidget-and-qobject_cast-qscreen-returns-null/3
|
||||
_font_inches_we_like = 6 / 53 # px / (px / inch) = inch
|
||||
|
||||
|
||||
def configure_font_to_dpi(screen: QtGui.QScreen):
|
||||
class DpiAwareFont:
|
||||
def __init__(
|
||||
self,
|
||||
name: str = 'Hack',
|
||||
) -> None:
|
||||
self.name = name
|
||||
self._qfont = QtGui.QFont(name)
|
||||
self._iwl = _font_inches_we_like
|
||||
self._qfm = QtGui.QFontMetrics(self._qfont)
|
||||
self._font_size = None
|
||||
self._physical_dpi = None
|
||||
self._screen = None
|
||||
|
||||
def _set_qfont_px_size(self, px_size: int) -> None:
|
||||
# self._qfont = QtGui.Qfont(self.name)
|
||||
self._qfont.setPixelSize(px_size)
|
||||
self._qfm = QtGui.QFontMetrics(self._qfont)
|
||||
|
||||
@property
|
||||
def font(self):
|
||||
return self._qfont
|
||||
|
||||
@property
|
||||
def px_size(self):
|
||||
return self._qfont.pixelSize()
|
||||
|
||||
# def set_px_size(self, size: int) -> None:
|
||||
# pass
|
||||
|
||||
def configure_to_dpi(self, screen: QtGui.QScreen):
|
||||
"""Set an appropriately sized font size depending on the screen DPI.
|
||||
|
||||
If we end up needing to generalize this more here there are resources
|
||||
|
@ -29,18 +73,57 @@ def configure_font_to_dpi(screen: QtGui.QScreen):
|
|||
|
||||
"""
|
||||
dpi = screen.physicalDotsPerInch()
|
||||
font_size = round(_font_inches_we_like * dpi)
|
||||
font_size = round(self._iwl * dpi)
|
||||
log.info(
|
||||
f"\nscreen:{screen.name()} with DPI: {dpi}"
|
||||
f"\nbest font size is {font_size}\n"
|
||||
)
|
||||
self._set_qfont_px_size(font_size)
|
||||
self._font_size = font_size
|
||||
self._physical_dpi = dpi
|
||||
self._screen = screen
|
||||
|
||||
global _font
|
||||
_font.setPixelSize(font_size)
|
||||
_font._fm = QtGui.QFontMetrics(_font)
|
||||
def boundingRect(self, value: str) -> QtCore.QRectF:
|
||||
screen = self._screen
|
||||
if screen is None:
|
||||
raise RuntimeError("You must call .configure_to_dpi() first!")
|
||||
|
||||
return _font
|
||||
unscaled_br = self._qfm.boundingRect(value)
|
||||
|
||||
# XXX: for wtv absolutely fucked reason, the scaling only applies
|
||||
# to everything when the current font size **is not** the size
|
||||
# needed to get the original desired text height... :mindblow:
|
||||
if self._font_size != 6:
|
||||
# scalar = self._qfm.fontDpi() / self._physical_dpi
|
||||
scalar = screen.logicalDotsPerInch() / screen.physicalDotsPerInch()
|
||||
# scalar = 100 / screen.physicalDotsPerInch()
|
||||
# assert 0
|
||||
print(f'SCALAR {scalar}')
|
||||
|
||||
|
||||
return QtCore.QRectF(
|
||||
# unscaled_br.x(),
|
||||
# unscaled_br.y(),
|
||||
0,
|
||||
0,
|
||||
unscaled_br.width() * scalar,
|
||||
unscaled_br.height() * scalar,
|
||||
)
|
||||
else:
|
||||
return QtCore.QRectF(
|
||||
# unscaled_br.x(),
|
||||
# unscaled_br.y(),
|
||||
0,
|
||||
0,
|
||||
unscaled_br.width(),
|
||||
unscaled_br.height(),
|
||||
)
|
||||
|
||||
# use pixel size to be cross-resolution compatible?
|
||||
_font = DpiAwareFont()
|
||||
|
||||
# TODO: re-compute font size when main widget switches screens?
|
||||
# https://forum.qt.io/topic/54136/how-do-i-get-the-qscreen-my-widget-is-on-qapplication-desktop-screen-returns-a-qwidget-and-qobject_cast-qscreen-returns-null/3
|
||||
|
||||
# _i3_rgba = QtGui.QColor.fromRgbF(*[0.14]*3 + [1])
|
||||
|
||||
|
@ -69,6 +152,7 @@ def hcolor(name: str) -> str:
|
|||
"""Hex color codes by hipster speak.
|
||||
"""
|
||||
return {
|
||||
|
||||
# lives matter
|
||||
'black': '#000000',
|
||||
'erie_black': '#1B1B1B',
|
||||
|
@ -99,6 +183,9 @@ def hcolor(name: str) -> str:
|
|||
'vwap_blue': '#0582fb',
|
||||
'dodger_blue': '#1e90ff', # like the team?
|
||||
'panasonic_blue': '#0040be', # from japan
|
||||
# 'bid_blue': '#0077ea', # like the L1
|
||||
'bid_blue': '#3094d9', # like the L1
|
||||
'aquaman': '#39abd0',
|
||||
|
||||
# traditional
|
||||
'tina_green': '#00cc00',
|
||||
|
|
Loading…
Reference in New Issue