piker/piker/ui/_style.py

300 lines
8.4 KiB
Python
Raw Normal View History

2020-11-06 17:23:14 +00:00
# piker: trading gear for hackers
# Copyright (C) Tyler Goodlet (in stewardship for piker0)
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/>.
"""
WIP initial draft of FSP subsystem This is a first attempt at a financial signal processing subsystem which utilizes async generators for streaming frames of numpy array data between actors. In this initial attempt the focus is on processing price data and relaying it to the chart app for real-time display. So far this seems to work (with decent latency) but much more work is likely needed around improving the data model for even better latency and less data duplication. Surprisingly (or not?) a lot of simplifications to the charting code came out of this in terms of conducting graphics updates in streaming tasks instead of hiding them inside the obfuscated mess that is the Qt-style-inheritance-OO-90s-trash. The goal from here on wards will be to enforce strict semantics around reading and writing of data such that state is kept outside "object trees" as much as possible and streaming function semantics guide our flow model. Unsurprisingly, this reduction in "instance state" is happening wherever we use `trio` ;) A little summary on the technical changes: - not going to explain the fsp system yet; it's too nascent and probably going to get some heavy editing. - drop any "update" methods from the `LinkedCharts` type since each sub-chart will have it's own update task and thus a separate update loop; further individual graphics (per chart) may eventually require this same design. - delete `ChartView`; moved into separate mod. - add "stream from fsp" task to start our foray into real-time actor processed numpy streaming.
2020-08-19 19:32:09 +00:00
Qt UI styling.
"""
from typing import Optional, Dict
2020-12-04 18:07:03 +00:00
import math
2020-11-06 01:32:35 +00:00
2020-07-15 12:41:29 +00:00
import pyqtgraph as pg
2020-11-02 17:02:05 +00:00
from PyQt5 import QtCore, QtGui
from qdarkstyle import DarkPalette
2020-10-27 19:15:31 +00:00
from ..log import get_logger
log = get_logger(__name__)
_magic_inches = 0.0666 * (1 + 6/16)
2020-12-04 18:07:03 +00:00
# chart-wide fonts specified in inches
_font_sizes: Dict[str, Dict[str, float]] = {
'hi': {
'default': _magic_inches,
'small': 0.9 * _magic_inches,
},
'lo': {
'default': 6.5 / 64,
'small': 6 / 64,
},
}
2020-11-02 17:02:05 +00:00
class DpiAwareFont:
2020-11-02 17:02:05 +00:00
def __init__(
self,
# TODO: move to config
2020-11-02 17:02:05 +00:00
name: str = 'Hack',
font_size: str = 'default',
# size_in_inches: Optional[float] = None,
2020-11-02 17:02:05 +00:00
) -> None:
self.name = name
self._qfont = QtGui.QFont(name)
self._font_size: str = font_size
2020-11-02 17:02:05 +00:00
self._qfm = QtGui.QFontMetrics(self._qfont)
self._font_inches: float = None
2020-11-02 17:02:05 +00:00
self._screen = None
def _set_qfont_px_size(self, px_size: int) -> None:
self._qfont.setPixelSize(px_size)
self._qfm = QtGui.QFontMetrics(self._qfont)
2021-01-09 15:56:35 +00:00
@property
def screen(self) -> QtGui.QScreen:
from ._window import main_window
if self._screen is not None:
try:
self._screen.refreshRate()
except RuntimeError:
self._screen = main_window().current_screen()
else:
self._screen = main_window().current_screen()
return self._screen
2021-01-09 15:56:35 +00:00
2020-11-02 17:02:05 +00:00
@property
def font(self):
return self._qfont
2021-07-23 16:19:07 +00:00
def scale(self) -> float:
screen = self.screen
return screen.logicalDotsPerInch() / screen.physicalDotsPerInch()
2020-11-02 17:02:05 +00:00
@property
2021-04-23 15:14:08 +00:00
def px_size(self) -> int:
2020-11-02 17:02:05 +00:00
return self._qfont.pixelSize()
def configure_to_dpi(self, screen: Optional[QtGui.QScreen] = None):
2020-11-02 17:02:05 +00:00
"""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``.
"""
if screen is None:
screen = self.screen
2020-12-04 18:07:03 +00:00
# take the max since scaling can make things ugly in some cases
pdpi = screen.physicalDotsPerInch()
ldpi = screen.logicalDotsPerInch()
# XXX: this is needed on sway/wayland where you set
# ``QT_WAYLAND_FORCE_DPI=physical``
if ldpi == 0:
ldpi = pdpi
mx_dpi = max(pdpi, ldpi)
mn_dpi = min(pdpi, ldpi)
scale = round(ldpi/pdpi, ndigits=2)
2020-12-04 18:07:03 +00:00
if mx_dpi <= 97: # for low dpi use larger font sizes
inches = _font_sizes['lo'][self._font_size]
else: # hidpi use smaller font sizes
inches = _font_sizes['hi'][self._font_size]
2021-03-19 13:33:47 +00:00
dpi = mn_dpi
# dpi is likely somewhat scaled down so use slightly larger font size
if scale >= 1.1 and self._font_size:
# no idea why
if 1.2 <= scale:
mult = 1.0375
2021-12-03 17:43:54 +00:00
if scale >= 1.5:
mult = 1.375
# TODO: this multiplier should probably be determined from
# relative aspect ratios or something?
inches *= mult
# TODO: we might want to fiddle with incrementing font size by
# +1 for the edge cases above. it seems doing it via scaling is
# always going to hit that error in range mapping from inches:
# float to px size: int.
self._font_inches = inches
font_size = math.floor(inches * dpi)
2022-02-07 13:47:20 +00:00
log.debug(
f"screen:{screen.name()}\n"
2021-09-16 20:36:09 +00:00
f"pDPI: {pdpi}, lDPI: {ldpi}, scale: {scale}\n"
f"\nOur best guess font size is {font_size}\n"
2020-11-02 17:02:05 +00:00
)
# apply the size
2020-11-02 17:02:05 +00:00
self._set_qfont_px_size(font_size)
def boundingRect(self, value: str) -> QtCore.QRectF:
2021-01-09 15:56:35 +00:00
screen = self.screen
2020-11-02 17:02:05 +00:00
if screen is None:
raise RuntimeError("You must call .configure_to_dpi() first!")
unscaled_br = self._qfm.boundingRect(value)
return QtCore.QRectF(
0,
0,
unscaled_br.width(),
unscaled_br.height(),
)
2020-11-06 01:32:35 +00:00
# use inches size to be cross-resolution compatible?
2020-11-02 17:02:05 +00:00
_font = DpiAwareFont()
_font_small = DpiAwareFont(font_size='small')
def _config_fonts_to_screen() -> None:
'configure global DPI aware font sizes'
global _font, _font_small
_font.configure_to_dpi()
_font_small.configure_to_dpi()
2020-09-29 18:18:14 +00:00
2020-11-02 17:02:05 +00:00
# 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
# splitter widget config
_xaxis_at = 'bottom'
WIP initial draft of FSP subsystem This is a first attempt at a financial signal processing subsystem which utilizes async generators for streaming frames of numpy array data between actors. In this initial attempt the focus is on processing price data and relaying it to the chart app for real-time display. So far this seems to work (with decent latency) but much more work is likely needed around improving the data model for even better latency and less data duplication. Surprisingly (or not?) a lot of simplifications to the charting code came out of this in terms of conducting graphics updates in streaming tasks instead of hiding them inside the obfuscated mess that is the Qt-style-inheritance-OO-90s-trash. The goal from here on wards will be to enforce strict semantics around reading and writing of data such that state is kept outside "object trees" as much as possible and streaming function semantics guide our flow model. Unsurprisingly, this reduction in "instance state" is happening wherever we use `trio` ;) A little summary on the technical changes: - not going to explain the fsp system yet; it's too nascent and probably going to get some heavy editing. - drop any "update" methods from the `LinkedCharts` type since each sub-chart will have it's own update task and thus a separate update loop; further individual graphics (per chart) may eventually require this same design. - delete `ChartView`; moved into separate mod. - add "stream from fsp" task to start our foray into real-time actor processed numpy streaming.
2020-08-19 19:32:09 +00:00
# charting config
CHART_MARGINS = (0, 0, 2, 2)
2020-10-23 00:42:46 +00:00
_min_points_to_show = 6
2021-03-31 18:26:32 +00:00
_bars_to_left_in_follow_mode = int(61*6)
_bars_from_right_in_follow_mode = round(0.16 * _bars_to_left_in_follow_mode)
_tina_mode = False
def enable_tina_mode() -> None:
"""Enable "tina mode" to make everything look "conventional"
like your pet hedgehog always wanted.
"""
# white background (for tinas like our pal xb)
pg.setConfigOption('background', 'w')
2020-08-30 16:28:38 +00:00
2020-08-31 21:16:44 +00:00
def hcolor(name: str) -> str:
2020-08-30 16:28:38 +00:00
"""Hex color codes by hipster speak.
2021-03-12 02:41:13 +00:00
This is an internal set of color codes hand picked
for certain purposes.
2020-08-30 16:28:38 +00:00
"""
2020-08-31 21:16:44 +00:00
return {
2020-11-02 17:02:05 +00:00
2020-08-31 21:16:44 +00:00
# lives matter
'black': '#000000',
'erie_black': '#1B1B1B',
'licorice': '#1A1110',
'papas_special': '#06070c',
2020-09-22 19:26:15 +00:00
'svags': '#0a0e14',
2020-08-31 21:16:44 +00:00
# fifty shades
2021-09-20 17:42:54 +00:00
'original': '#a9a9a9',
2020-08-31 21:16:44 +00:00
'gray': '#808080', # like the kick
2021-03-12 02:41:13 +00:00
'grayer': '#4c4c4c',
'grayest': '#3f3f3f',
2020-09-10 20:16:21 +00:00
'cadet': '#91A3B0',
'marengo': '#91A3B0',
'gunmetal': '#91A3B0',
'battleship': '#848482',
2021-09-20 17:42:54 +00:00
# bluish
'charcoal': '#36454F',
# default bars
2020-10-25 00:18:03 +00:00
'bracket': '#666666', # like the logo
2021-09-20 17:42:54 +00:00
# work well for filled polygons which want a 'bracket' feel
# going light to dark
'davies': '#555555',
'i3': '#494D4F',
'jet': '#343434',
2020-08-31 21:16:44 +00:00
# from ``qdarkstyle`` palette
'default_darkest': DarkPalette.COLOR_BACKGROUND_1,
'default_dark': DarkPalette.COLOR_BACKGROUND_2,
'default': DarkPalette.COLOR_BACKGROUND_3,
'default_light': DarkPalette.COLOR_BACKGROUND_4,
'default_lightest': DarkPalette.COLOR_BACKGROUND_5,
'default_spotlight': DarkPalette.COLOR_BACKGROUND_6,
2020-08-31 21:16:44 +00:00
'white': '#ffffff', # for tinas and sunbathers
# blue zone
'dad_blue': '#326693', # like his shirt
'vwap_blue': '#0582fb',
'dodger_blue': '#1e90ff', # like the team?
'panasonic_blue': '#0040be', # from japan
2020-11-02 17:02:05 +00:00
# 'bid_blue': '#0077ea', # like the L1
'bid_blue': '#3094d9', # like the L1
'aquaman': '#39abd0',
2020-08-30 16:28:38 +00:00
# traditional
2020-08-31 21:16:44 +00:00
'tina_green': '#00cc00',
'tina_red': '#fa0000',
2021-01-12 02:22:21 +00:00
'cucumber': '#006400',
'cool_green': '#33b864',
'dull_green': '#74a662',
'hedge_green': '#518360',
2021-01-03 15:39:06 +00:00
# orders and alerts
'alert_yellow': '#e2d083',
2021-01-03 22:23:23 +00:00
'alert_yellow_light': '#ffe366',
2021-01-03 15:39:06 +00:00
2021-01-12 02:22:21 +00:00
# buys
# 'hedge': '#768a75',
# 'hedge': '#41694d',
# 'hedge': '#558964',
# 'hedge_light': '#5e9870',
'80s_neon_green': '#00b677',
2021-01-12 02:22:21 +00:00
# 'buy_green': '#41694d',
'buy_green': '#558964',
'buy_green_light': '#558964',
# sells
# techincally "raspberry"
# 'sell_red': '#990036',
# 'sell_red': '#9A0036',
# brighter then above
# 'sell_red': '#8c0030',
'sell_red': '#b6003f',
# 'sell_red': '#d00048',
2021-01-12 02:22:21 +00:00
'sell_red_light': '#f85462',
2021-01-12 02:22:21 +00:00
# 'sell_red': '#f85462',
# 'sell_red_light': '#ff4d5c',
2020-08-30 16:28:38 +00:00
}[name]