Add a dpi aware font wrapper type

bar_select
Tyler Goodlet 2020-11-02 12:02:05 -05:00
parent 307c501763
commit 9e7aa3f9bf
2 changed files with 207 additions and 75 deletions

View File

@ -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]
self.update_from_data(
index,
last,
)
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

View File

@ -2,46 +2,129 @@
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
_font_inches_we_like = 6 / 53 # px / (px / inch) = inch
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
listed in the script in ``snippets/qt_screen_info.py``.
"""
dpi = screen.physicalDotsPerInch()
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
def boundingRect(self, value: str) -> QtCore.QRectF:
screen = self._screen
if screen is None:
raise RuntimeError("You must call .configure_to_dpi() first!")
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 = QtGui.QFont("Hack")
_font.setPixelSize(6) # default
_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
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
# _i3_rgba = QtGui.QColor.fromRgbF(*[0.14]*3 + [1])
# splitter widget config
@ -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',