Make chart switching super fast again
This fixes a weird re-render bug/slowdown/artifact that was introduced with the order mode sidepane work. Prior to the sidepane addition, chart switching was immediate with zero noticeable widget rendering steps. The slow down was caused by 2 things: - not yielding back to the Qt loop asap after re-showing/focussing a linked split chart that was already in memory. - pausing/resuming feeds only after a Qt loop render cycle has completed. This now restores the near zero latency UX.py3.10_support
parent
f21c68a672
commit
56b65a1cde
|
@ -18,6 +18,7 @@
|
||||||
High level chart-widget apis.
|
High level chart-widget apis.
|
||||||
|
|
||||||
'''
|
'''
|
||||||
|
from __future__ import annotations
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
from PyQt5 import QtCore, QtWidgets
|
from PyQt5 import QtCore, QtWidgets
|
||||||
|
@ -68,7 +69,7 @@ log = get_logger(__name__)
|
||||||
class GodWidget(QWidget):
|
class GodWidget(QWidget):
|
||||||
'''
|
'''
|
||||||
"Our lord and savior, the holy child of window-shua, there is no
|
"Our lord and savior, the holy child of window-shua, there is no
|
||||||
widget above thee." - 6||6
|
widget above thee." - 6|6
|
||||||
|
|
||||||
The highest level composed widget which contains layouts for
|
The highest level composed widget which contains layouts for
|
||||||
organizing charts as well as other sub-widgets used to control or
|
organizing charts as well as other sub-widgets used to control or
|
||||||
|
@ -104,8 +105,8 @@ class GodWidget(QWidget):
|
||||||
# self.init_strategy_ui()
|
# self.init_strategy_ui()
|
||||||
# self.vbox.addLayout(self.hbox)
|
# self.vbox.addLayout(self.hbox)
|
||||||
|
|
||||||
self._chart_cache = {}
|
self._chart_cache: dict[str, LinkedSplits] = {}
|
||||||
self.linkedsplits: 'LinkedSplits' = None
|
self.linkedsplits: Optional[LinkedSplits] = None
|
||||||
|
|
||||||
# assigned in the startup func `_async_main()`
|
# assigned in the startup func `_async_main()`
|
||||||
self._root_n: trio.Nursery = None
|
self._root_n: trio.Nursery = None
|
||||||
|
@ -135,7 +136,7 @@ class GodWidget(QWidget):
|
||||||
def set_chart_symbol(
|
def set_chart_symbol(
|
||||||
self,
|
self,
|
||||||
symbol_key: str, # of form <fqsn>.<providername>
|
symbol_key: str, # of form <fqsn>.<providername>
|
||||||
linkedsplits: 'LinkedSplits', # type: ignore
|
linkedsplits: LinkedSplits, # type: ignore
|
||||||
|
|
||||||
) -> None:
|
) -> None:
|
||||||
# re-sort org cache symbol list in LIFO order
|
# re-sort org cache symbol list in LIFO order
|
||||||
|
@ -146,20 +147,20 @@ class GodWidget(QWidget):
|
||||||
def get_chart_symbol(
|
def get_chart_symbol(
|
||||||
self,
|
self,
|
||||||
symbol_key: str,
|
symbol_key: str,
|
||||||
) -> 'LinkedSplits': # type: ignore
|
|
||||||
|
) -> LinkedSplits: # type: ignore
|
||||||
return self._chart_cache.get(symbol_key)
|
return self._chart_cache.get(symbol_key)
|
||||||
|
|
||||||
async def load_symbol(
|
async def load_symbol(
|
||||||
self,
|
self,
|
||||||
|
|
||||||
providername: str,
|
providername: str,
|
||||||
symbol_key: str,
|
symbol_key: str,
|
||||||
loglevel: str,
|
loglevel: str,
|
||||||
|
|
||||||
reset: bool = False,
|
reset: bool = False,
|
||||||
|
|
||||||
) -> trio.Event:
|
) -> trio.Event:
|
||||||
'''Load a new contract into the charting app.
|
'''
|
||||||
|
Load a new contract into the charting app.
|
||||||
|
|
||||||
Expects a ``numpy`` structured array containing all the ohlcv fields.
|
Expects a ``numpy`` structured array containing all the ohlcv fields.
|
||||||
|
|
||||||
|
@ -178,6 +179,7 @@ class GodWidget(QWidget):
|
||||||
|
|
||||||
# XXX: this is CRITICAL especially with pixel buffer caching
|
# XXX: this is CRITICAL especially with pixel buffer caching
|
||||||
self.linkedsplits.hide()
|
self.linkedsplits.hide()
|
||||||
|
self.linkedsplits.unfocus()
|
||||||
|
|
||||||
# XXX: pretty sure we don't need this
|
# XXX: pretty sure we don't need this
|
||||||
# remove any existing plots?
|
# remove any existing plots?
|
||||||
|
@ -202,6 +204,11 @@ class GodWidget(QWidget):
|
||||||
)
|
)
|
||||||
|
|
||||||
self.set_chart_symbol(fqsn, linkedsplits)
|
self.set_chart_symbol(fqsn, linkedsplits)
|
||||||
|
self.vbox.addWidget(linkedsplits)
|
||||||
|
|
||||||
|
linkedsplits.show()
|
||||||
|
linkedsplits.focus()
|
||||||
|
await trio.sleep(0)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
# symbol is already loaded and ems ready
|
# symbol is already loaded and ems ready
|
||||||
|
@ -215,21 +222,17 @@ class GodWidget(QWidget):
|
||||||
# also switch it over to the new chart's interal-layout
|
# also switch it over to the new chart's interal-layout
|
||||||
# self.linkedsplits.chart.qframe.hbox.removeWidget(self.pp_pane)
|
# self.linkedsplits.chart.qframe.hbox.removeWidget(self.pp_pane)
|
||||||
chart = linkedsplits.chart
|
chart = linkedsplits.chart
|
||||||
await chart.resume_all_feeds()
|
|
||||||
|
|
||||||
# chart is already in memory so just focus it
|
# chart is already in memory so just focus it
|
||||||
if self.linkedsplits:
|
|
||||||
self.linkedsplits.unfocus()
|
|
||||||
|
|
||||||
self.vbox.addWidget(linkedsplits)
|
|
||||||
|
|
||||||
linkedsplits.show()
|
linkedsplits.show()
|
||||||
linkedsplits.focus()
|
linkedsplits.focus()
|
||||||
|
await trio.sleep(0)
|
||||||
|
|
||||||
|
# resume feeds *after* rendering chart view asap
|
||||||
|
await chart.resume_all_feeds()
|
||||||
|
|
||||||
self.linkedsplits = linkedsplits
|
self.linkedsplits = linkedsplits
|
||||||
|
|
||||||
symbol = linkedsplits.symbol
|
symbol = linkedsplits.symbol
|
||||||
|
|
||||||
if symbol is not None:
|
if symbol is not None:
|
||||||
self.window.setWindowTitle(
|
self.window.setWindowTitle(
|
||||||
f'{symbol.key}@{symbol.brokers} '
|
f'{symbol.key}@{symbol.brokers} '
|
||||||
|
@ -239,7 +242,8 @@ class GodWidget(QWidget):
|
||||||
return order_mode_started
|
return order_mode_started
|
||||||
|
|
||||||
def focus(self) -> None:
|
def focus(self) -> None:
|
||||||
'''Focus the top level widget which in turn focusses the chart
|
'''
|
||||||
|
Focus the top level widget which in turn focusses the chart
|
||||||
ala "view mode".
|
ala "view mode".
|
||||||
|
|
||||||
'''
|
'''
|
||||||
|
@ -247,9 +251,19 @@ class GodWidget(QWidget):
|
||||||
self.clearFocus()
|
self.clearFocus()
|
||||||
self.linkedsplits.chart.setFocus()
|
self.linkedsplits.chart.setFocus()
|
||||||
|
|
||||||
|
def resizeEvent(self, event: QtCore.QEvent) -> None:
|
||||||
|
'''
|
||||||
|
Top level god widget resize handler.
|
||||||
|
|
||||||
|
Where we do UX magic to make things not suck B)
|
||||||
|
|
||||||
|
'''
|
||||||
|
log.debug('god widget resize')
|
||||||
|
|
||||||
|
|
||||||
class ChartnPane(QFrame):
|
class ChartnPane(QFrame):
|
||||||
'''One-off ``QFrame`` composite which pairs a chart
|
'''
|
||||||
|
One-off ``QFrame`` composite which pairs a chart
|
||||||
+ sidepane (often a ``FieldsForm`` + other widgets if
|
+ sidepane (often a ``FieldsForm`` + other widgets if
|
||||||
provided) forming a, sort of, "chart row" with a side panel
|
provided) forming a, sort of, "chart row" with a side panel
|
||||||
for configuration and display of off-chart data.
|
for configuration and display of off-chart data.
|
||||||
|
@ -280,8 +294,6 @@ class ChartnPane(QFrame):
|
||||||
hbox.setContentsMargins(0, 0, 0, 0)
|
hbox.setContentsMargins(0, 0, 0, 0)
|
||||||
hbox.setSpacing(3)
|
hbox.setSpacing(3)
|
||||||
|
|
||||||
# self.setMaximumWidth()
|
|
||||||
|
|
||||||
|
|
||||||
class LinkedSplits(QWidget):
|
class LinkedSplits(QWidget):
|
||||||
'''
|
'''
|
||||||
|
@ -460,7 +472,10 @@ class LinkedSplits(QWidget):
|
||||||
self.xaxis.hide()
|
self.xaxis.hide()
|
||||||
self.xaxis = xaxis
|
self.xaxis = xaxis
|
||||||
|
|
||||||
qframe = ChartnPane(sidepane=sidepane, parent=self.splitter)
|
qframe = ChartnPane(
|
||||||
|
sidepane=sidepane,
|
||||||
|
parent=self.splitter,
|
||||||
|
)
|
||||||
cpw = ChartPlotWidget(
|
cpw = ChartPlotWidget(
|
||||||
|
|
||||||
# this name will be used to register the primary
|
# this name will be used to register the primary
|
||||||
|
@ -554,17 +569,23 @@ class LinkedSplits(QWidget):
|
||||||
else:
|
else:
|
||||||
assert style == 'bar', 'main chart must be OHLC'
|
assert style == 'bar', 'main chart must be OHLC'
|
||||||
|
|
||||||
|
self.resize_sidepanes()
|
||||||
return cpw
|
return cpw
|
||||||
|
|
||||||
def resize_sidepanes(
|
def resize_sidepanes(
|
||||||
self,
|
self,
|
||||||
) -> None:
|
) -> None:
|
||||||
'''Size all sidepanes based on the OHLC "main" plot.
|
'''
|
||||||
|
Size all sidepanes based on the OHLC "main" plot and its
|
||||||
|
sidepane width.
|
||||||
|
|
||||||
'''
|
'''
|
||||||
|
main_chart = self.chart
|
||||||
|
if main_chart:
|
||||||
|
sp_w = main_chart.sidepane.width()
|
||||||
for name, cpw in self.subplots.items():
|
for name, cpw in self.subplots.items():
|
||||||
cpw.sidepane.setMinimumWidth(self.chart.sidepane.width())
|
cpw.sidepane.setMinimumWidth(sp_w)
|
||||||
cpw.sidepane.setMaximumWidth(self.chart.sidepane.width())
|
cpw.sidepane.setMaximumWidth(sp_w)
|
||||||
|
|
||||||
|
|
||||||
class ChartPlotWidget(pg.PlotWidget):
|
class ChartPlotWidget(pg.PlotWidget):
|
||||||
|
@ -672,12 +693,14 @@ class ChartPlotWidget(pg.PlotWidget):
|
||||||
self._vb.sigResized.connect(self._set_yrange)
|
self._vb.sigResized.connect(self._set_yrange)
|
||||||
|
|
||||||
async def resume_all_feeds(self):
|
async def resume_all_feeds(self):
|
||||||
|
async with trio.open_nursery() as n:
|
||||||
for feed in self._feeds.values():
|
for feed in self._feeds.values():
|
||||||
await feed.resume()
|
n.start_soon(feed.resume)
|
||||||
|
|
||||||
async def pause_all_feeds(self):
|
async def pause_all_feeds(self):
|
||||||
|
async with trio.open_nursery() as n:
|
||||||
for feed in self._feeds.values():
|
for feed in self._feeds.values():
|
||||||
await feed.pause()
|
n.start_soon(feed.pause)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def view(self) -> ChartView:
|
def view(self) -> ChartView:
|
||||||
|
|
Loading…
Reference in New Issue