Merge pull request #453 from pikers/overlays_interaction_latency_tuning
Overlays interaction latency tuningbackend_spec
commit
139b8ba0f4
|
@ -257,7 +257,7 @@ async def open_piker_runtime(
|
||||||
# and spawn the service tree distributed per that.
|
# and spawn the service tree distributed per that.
|
||||||
start_method: str = 'trio',
|
start_method: str = 'trio',
|
||||||
|
|
||||||
tractor_kwargs: dict = {},
|
**tractor_kwargs,
|
||||||
|
|
||||||
) -> tuple[
|
) -> tuple[
|
||||||
tractor.Actor,
|
tractor.Actor,
|
||||||
|
|
|
@ -152,9 +152,14 @@ class Profiler(object):
|
||||||
# don't do anything
|
# don't do anything
|
||||||
return cls._disabledProfiler
|
return cls._disabledProfiler
|
||||||
|
|
||||||
# create an actual profiling object
|
|
||||||
cls._depth += 1
|
cls._depth += 1
|
||||||
obj = super(Profiler, cls).__new__(cls)
|
obj = super(Profiler, cls).__new__(cls)
|
||||||
|
obj._msgs = []
|
||||||
|
|
||||||
|
# create an actual profiling object
|
||||||
|
if cls._depth < 1:
|
||||||
|
cls._msgs = []
|
||||||
|
|
||||||
obj._name = msg or func_qualname
|
obj._name = msg or func_qualname
|
||||||
obj._delayed = delayed
|
obj._delayed = delayed
|
||||||
obj._markCount = 0
|
obj._markCount = 0
|
||||||
|
@ -174,8 +179,12 @@ class Profiler(object):
|
||||||
|
|
||||||
self._markCount += 1
|
self._markCount += 1
|
||||||
newTime = perf_counter()
|
newTime = perf_counter()
|
||||||
|
tot_ms = (newTime - self._firstTime) * 1000
|
||||||
ms = (newTime - self._lastTime) * 1000
|
ms = (newTime - self._lastTime) * 1000
|
||||||
self._newMsg(" %s: %0.4f ms", msg, ms)
|
self._newMsg(
|
||||||
|
f" {msg}: {ms:0.4f}, tot:{tot_ms:0.4f}"
|
||||||
|
)
|
||||||
|
|
||||||
self._lastTime = newTime
|
self._lastTime = newTime
|
||||||
|
|
||||||
def mark(self, msg=None):
|
def mark(self, msg=None):
|
||||||
|
|
|
@ -55,6 +55,10 @@ class IncrementalFormatter(msgspec.Struct):
|
||||||
shm: ShmArray
|
shm: ShmArray
|
||||||
viz: Viz
|
viz: Viz
|
||||||
|
|
||||||
|
# the value to be multiplied any any index into the x/y_1d arrays
|
||||||
|
# given the input index is based on the original source data array.
|
||||||
|
flat_index_ratio: float = 1
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def index_field(self) -> 'str':
|
def index_field(self) -> 'str':
|
||||||
'''
|
'''
|
||||||
|
@ -92,8 +96,8 @@ class IncrementalFormatter(msgspec.Struct):
|
||||||
xy_nd_stop: int | None = None
|
xy_nd_stop: int | None = None
|
||||||
|
|
||||||
# TODO: eventually incrementally update 1d-pre-graphics path data?
|
# TODO: eventually incrementally update 1d-pre-graphics path data?
|
||||||
# x_1d: Optional[np.ndarray] = None
|
x_1d: np.ndarray | None = None
|
||||||
# y_1d: Optional[np.ndarray] = None
|
y_1d: np.ndarray | None = None
|
||||||
|
|
||||||
# incremental view-change state(s) tracking
|
# incremental view-change state(s) tracking
|
||||||
_last_vr: tuple[float, float] | None = None
|
_last_vr: tuple[float, float] | None = None
|
||||||
|
@ -107,32 +111,6 @@ class IncrementalFormatter(msgspec.Struct):
|
||||||
'''
|
'''
|
||||||
return self.viz.index_step()
|
return self.viz.index_step()
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
|
||||||
msg = (
|
|
||||||
f'{type(self)}: ->\n\n'
|
|
||||||
f'fqsn={self.viz.name}\n'
|
|
||||||
f'shm_name={self.shm.token["shm_name"]}\n\n'
|
|
||||||
|
|
||||||
f'last_vr={self._last_vr}\n'
|
|
||||||
f'last_ivdr={self._last_ivdr}\n\n'
|
|
||||||
|
|
||||||
f'xy_slice={self.xy_slice}\n'
|
|
||||||
# f'xy_nd_stop={self.xy_nd_stop}\n\n'
|
|
||||||
)
|
|
||||||
|
|
||||||
x_nd_len = 0
|
|
||||||
y_nd_len = 0
|
|
||||||
if self.x_nd is not None:
|
|
||||||
x_nd_len = len(self.x_nd)
|
|
||||||
y_nd_len = len(self.y_nd)
|
|
||||||
|
|
||||||
msg += (
|
|
||||||
f'x_nd_len={x_nd_len}\n'
|
|
||||||
f'y_nd_len={y_nd_len}\n'
|
|
||||||
)
|
|
||||||
|
|
||||||
return msg
|
|
||||||
|
|
||||||
def diff(
|
def diff(
|
||||||
self,
|
self,
|
||||||
new_read: tuple[np.ndarray],
|
new_read: tuple[np.ndarray],
|
||||||
|
@ -180,8 +158,6 @@ class IncrementalFormatter(msgspec.Struct):
|
||||||
# set us in a zero-to-append state
|
# set us in a zero-to-append state
|
||||||
nd_stop = self.xy_nd_stop = src_stop
|
nd_stop = self.xy_nd_stop = src_stop
|
||||||
|
|
||||||
align_index = array[self.index_field]
|
|
||||||
|
|
||||||
# compute the length diffs between the first/last index entry in
|
# compute the length diffs between the first/last index entry in
|
||||||
# the input data and the last indexes we have on record from the
|
# the input data and the last indexes we have on record from the
|
||||||
# last time we updated the curve index.
|
# last time we updated the curve index.
|
||||||
|
@ -334,6 +310,9 @@ class IncrementalFormatter(msgspec.Struct):
|
||||||
array = in_view
|
array = in_view
|
||||||
profiler(f'{self.viz.name} view range slice {view_range}')
|
profiler(f'{self.viz.name} view range slice {view_range}')
|
||||||
|
|
||||||
|
# TODO: we need to check if the last-datum-in-view is true and
|
||||||
|
# if so only slice to the 2nd last datumonly slice to the 2nd
|
||||||
|
# last datum.
|
||||||
# hist = array[:slice_to_head]
|
# hist = array[:slice_to_head]
|
||||||
|
|
||||||
# XXX: WOA WTF TRACTOR DEBUGGING BUGGG
|
# XXX: WOA WTF TRACTOR DEBUGGING BUGGG
|
||||||
|
@ -353,6 +332,11 @@ class IncrementalFormatter(msgspec.Struct):
|
||||||
array_key,
|
array_key,
|
||||||
view_range,
|
view_range,
|
||||||
)
|
)
|
||||||
|
# cache/save last 1d outputs for use by other
|
||||||
|
# readers (eg. `Viz.draw_last_datum()` in the
|
||||||
|
# only-draw-last-uppx case).
|
||||||
|
self.x_1d = x_1d
|
||||||
|
self.y_1d = y_1d
|
||||||
|
|
||||||
# app_tres = None
|
# app_tres = None
|
||||||
# if append_len:
|
# if append_len:
|
||||||
|
@ -376,11 +360,6 @@ class IncrementalFormatter(msgspec.Struct):
|
||||||
# update the last "in view data range"
|
# update the last "in view data range"
|
||||||
if len(x_1d):
|
if len(x_1d):
|
||||||
self._last_ivdr = x_1d[0], x_1d[-1]
|
self._last_ivdr = x_1d[0], x_1d[-1]
|
||||||
if (
|
|
||||||
self.index_field == 'time'
|
|
||||||
and (x_1d[-1] == 0.5).any()
|
|
||||||
):
|
|
||||||
breakpoint()
|
|
||||||
|
|
||||||
profiler('.format_to_1d()')
|
profiler('.format_to_1d()')
|
||||||
|
|
||||||
|
@ -503,14 +482,22 @@ class IncrementalFormatter(msgspec.Struct):
|
||||||
# NOTE: we don't include the very last datum which is filled in
|
# NOTE: we don't include the very last datum which is filled in
|
||||||
# normally by another graphics object.
|
# normally by another graphics object.
|
||||||
x_1d = array[self.index_field][:-1]
|
x_1d = array[self.index_field][:-1]
|
||||||
if (
|
|
||||||
self.index_field == 'time'
|
|
||||||
and x_1d.any()
|
|
||||||
and (x_1d[-1] == 0.5).any()
|
|
||||||
):
|
|
||||||
breakpoint()
|
|
||||||
|
|
||||||
y_1d = array[array_key][:-1]
|
y_1d = array[array_key][:-1]
|
||||||
|
|
||||||
|
# name = self.viz.name
|
||||||
|
# if 'trade_rate' == name:
|
||||||
|
# s = 4
|
||||||
|
# x_nd = list(self.x_nd[self.xy_slice][-s:-1])
|
||||||
|
# y_nd = list(self.y_nd[self.xy_slice][-s:-1])
|
||||||
|
# print(
|
||||||
|
# f'{name}:\n'
|
||||||
|
# f'XY data:\n'
|
||||||
|
# f'x: {x_nd}\n'
|
||||||
|
# f'y: {y_nd}\n\n'
|
||||||
|
# f'x_1d: {list(x_1d[-s:])}\n'
|
||||||
|
# f'y_1d: {list(y_1d[-s:])}\n\n'
|
||||||
|
|
||||||
|
# )
|
||||||
return (
|
return (
|
||||||
x_1d,
|
x_1d,
|
||||||
y_1d,
|
y_1d,
|
||||||
|
@ -532,6 +519,7 @@ class OHLCBarsFmtr(IncrementalFormatter):
|
||||||
fields: list[str] = field(
|
fields: list[str] = field(
|
||||||
default_factory=lambda: ['open', 'high', 'low', 'close']
|
default_factory=lambda: ['open', 'high', 'low', 'close']
|
||||||
)
|
)
|
||||||
|
flat_index_ratio: float = 4
|
||||||
|
|
||||||
def allocate_xy_nd(
|
def allocate_xy_nd(
|
||||||
self,
|
self,
|
||||||
|
@ -627,7 +615,7 @@ class OHLCBarsFmtr(IncrementalFormatter):
|
||||||
|
|
||||||
'''
|
'''
|
||||||
x, y, c = path_arrays_from_ohlc(
|
x, y, c = path_arrays_from_ohlc(
|
||||||
array,
|
array[:-1],
|
||||||
start,
|
start,
|
||||||
bar_w=self.index_step_size,
|
bar_w=self.index_step_size,
|
||||||
bar_gap=w * self.index_step_size,
|
bar_gap=w * self.index_step_size,
|
||||||
|
@ -826,13 +814,6 @@ class StepCurveFmtr(IncrementalFormatter):
|
||||||
x_1d = x_step_iv.reshape(x_step_iv.size)
|
x_1d = x_step_iv.reshape(x_step_iv.size)
|
||||||
y_1d = y_step_iv.reshape(y_step_iv.size)
|
y_1d = y_step_iv.reshape(y_step_iv.size)
|
||||||
|
|
||||||
if (
|
|
||||||
self.index_field == 'time'
|
|
||||||
and x_1d.any()
|
|
||||||
and (x_1d == 0.5).any()
|
|
||||||
):
|
|
||||||
breakpoint()
|
|
||||||
|
|
||||||
# debugging
|
# debugging
|
||||||
# if y_1d.any():
|
# if y_1d.any():
|
||||||
# s = 6
|
# s = 6
|
||||||
|
|
|
@ -91,6 +91,14 @@ def ds_m4(
|
||||||
x_end = x[-1] # x end value/highest in domain
|
x_end = x[-1] # x end value/highest in domain
|
||||||
xrange = (x_end - x_start)
|
xrange = (x_end - x_start)
|
||||||
|
|
||||||
|
if xrange < 0:
|
||||||
|
log.error(f'-VE M4 X-RANGE: {x_start} -> {x_end}')
|
||||||
|
# XXX: broken x-range calc-case, likely the x-end points
|
||||||
|
# are wrong and have some default value set (such as
|
||||||
|
# x_end -> <some epoch float> while x_start -> 0.5).
|
||||||
|
# breakpoint()
|
||||||
|
return None
|
||||||
|
|
||||||
# XXX: always round up on the input pixels
|
# XXX: always round up on the input pixels
|
||||||
# lnx = len(x)
|
# lnx = len(x)
|
||||||
# uppx *= max(4 / (1 + math.log(uppx, 2)), 1)
|
# uppx *= max(4 / (1 + math.log(uppx, 2)), 1)
|
||||||
|
|
|
@ -17,6 +17,11 @@
|
||||||
Super fast ``QPainterPath`` generation related operator routines.
|
Super fast ``QPainterPath`` generation related operator routines.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
from math import (
|
||||||
|
ceil,
|
||||||
|
floor,
|
||||||
|
)
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
from numpy.lib import recfunctions as rfn
|
from numpy.lib import recfunctions as rfn
|
||||||
from numba import (
|
from numba import (
|
||||||
|
@ -58,20 +63,27 @@ def xy_downsample(
|
||||||
# downsample whenever more then 1 pixels per datum can be shown.
|
# downsample whenever more then 1 pixels per datum can be shown.
|
||||||
# always refresh data bounds until we get diffing
|
# always refresh data bounds until we get diffing
|
||||||
# working properly, see above..
|
# working properly, see above..
|
||||||
bins, x, y, ymn, ymx = ds_m4(
|
m4_out = ds_m4(
|
||||||
x,
|
x,
|
||||||
y,
|
y,
|
||||||
uppx,
|
uppx,
|
||||||
)
|
)
|
||||||
|
|
||||||
# flatten output to 1d arrays suitable for path-graphics generation.
|
if m4_out is not None:
|
||||||
x = np.broadcast_to(x[:, None], y.shape)
|
bins, x, y, ymn, ymx = m4_out
|
||||||
x = (x + np.array(
|
# flatten output to 1d arrays suitable for path-graphics generation.
|
||||||
[-x_spacer, 0, 0, x_spacer]
|
x = np.broadcast_to(x[:, None], y.shape)
|
||||||
)).flatten()
|
x = (x + np.array(
|
||||||
y = y.flatten()
|
[-x_spacer, 0, 0, x_spacer]
|
||||||
|
)).flatten()
|
||||||
|
y = y.flatten()
|
||||||
|
|
||||||
return x, y, ymn, ymx
|
return x, y, ymn, ymx
|
||||||
|
|
||||||
|
# XXX: we accept a None output for the case where the input range
|
||||||
|
# to ``ds_m4()`` is bad (-ve) and we want to catch and debug
|
||||||
|
# that (seemingly super rare) circumstance..
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
@njit(
|
@njit(
|
||||||
|
@ -285,10 +297,7 @@ def slice_from_time(
|
||||||
stop_t: float,
|
stop_t: float,
|
||||||
step: int | None = None,
|
step: int | None = None,
|
||||||
|
|
||||||
) -> tuple[
|
) -> slice:
|
||||||
slice,
|
|
||||||
slice,
|
|
||||||
]:
|
|
||||||
'''
|
'''
|
||||||
Calculate array indices mapped from a time range and return them in
|
Calculate array indices mapped from a time range and return them in
|
||||||
a slice.
|
a slice.
|
||||||
|
@ -308,22 +317,32 @@ def slice_from_time(
|
||||||
)
|
)
|
||||||
|
|
||||||
times = arr['time']
|
times = arr['time']
|
||||||
t_first = round(times[0])
|
t_first = floor(times[0])
|
||||||
|
t_last = ceil(times[-1])
|
||||||
|
|
||||||
|
# the greatest index we can return which slices to the
|
||||||
|
# end of the input array.
|
||||||
read_i_max = arr.shape[0]
|
read_i_max = arr.shape[0]
|
||||||
|
|
||||||
|
# TODO: require this is always passed in?
|
||||||
if step is None:
|
if step is None:
|
||||||
step = round(times[-1] - times[-2])
|
step = round(t_last - times[-2])
|
||||||
if step == 0:
|
if step == 0:
|
||||||
# XXX: HOW TF is this happening?
|
|
||||||
step = 1
|
step = 1
|
||||||
|
|
||||||
# compute (presumed) uniform-time-step index offsets
|
# compute (presumed) uniform-time-step index offsets
|
||||||
i_start_t = round(start_t)
|
i_start_t = floor(start_t)
|
||||||
read_i_start = round(((i_start_t - t_first) // step)) - 1
|
read_i_start = floor(((i_start_t - t_first) // step)) - 1
|
||||||
|
|
||||||
i_stop_t = round(stop_t)
|
i_stop_t = ceil(stop_t)
|
||||||
read_i_stop = round((i_stop_t - t_first) // step) + 1
|
|
||||||
|
# XXX: edge case -> always set stop index to last in array whenever
|
||||||
|
# the input stop time is detected to be greater then the equiv time
|
||||||
|
# stamp at that last entry.
|
||||||
|
if i_stop_t >= t_last:
|
||||||
|
read_i_stop = read_i_max
|
||||||
|
else:
|
||||||
|
read_i_stop = ceil((i_stop_t - t_first) // step) + 1
|
||||||
|
|
||||||
# always clip outputs to array support
|
# always clip outputs to array support
|
||||||
# for read start:
|
# for read start:
|
||||||
|
@ -367,7 +386,7 @@ def slice_from_time(
|
||||||
# up_to_arith_start = index[:read_i_start]
|
# up_to_arith_start = index[:read_i_start]
|
||||||
|
|
||||||
if (
|
if (
|
||||||
new_read_i_start < read_i_start
|
new_read_i_start <= read_i_start
|
||||||
):
|
):
|
||||||
# t_diff = t_iv_start - start_t
|
# t_diff = t_iv_start - start_t
|
||||||
# print(
|
# print(
|
||||||
|
@ -391,14 +410,15 @@ def slice_from_time(
|
||||||
# )
|
# )
|
||||||
new_read_i_stop = np.searchsorted(
|
new_read_i_stop = np.searchsorted(
|
||||||
times[read_i_start:],
|
times[read_i_start:],
|
||||||
|
# times,
|
||||||
i_stop_t,
|
i_stop_t,
|
||||||
side='left',
|
side='left',
|
||||||
)
|
)
|
||||||
|
|
||||||
if (
|
if (
|
||||||
new_read_i_stop < read_i_stop
|
new_read_i_stop <= read_i_stop
|
||||||
):
|
):
|
||||||
read_i_stop = read_i_start + new_read_i_stop
|
read_i_stop = read_i_start + new_read_i_stop + 1
|
||||||
|
|
||||||
# sanity checks for range size
|
# sanity checks for range size
|
||||||
# samples = (i_stop_t - i_start_t) // step
|
# samples = (i_stop_t - i_start_t) // step
|
||||||
|
|
|
@ -207,7 +207,7 @@ def get_feed_bus(
|
||||||
|
|
||||||
) -> _FeedsBus:
|
) -> _FeedsBus:
|
||||||
'''
|
'''
|
||||||
Retreive broker-daemon-local data feeds bus from process global
|
Retrieve broker-daemon-local data feeds bus from process global
|
||||||
scope. Serialize task access to lock.
|
scope. Serialize task access to lock.
|
||||||
|
|
||||||
'''
|
'''
|
||||||
|
@ -250,6 +250,7 @@ async def start_backfill(
|
||||||
shm: ShmArray,
|
shm: ShmArray,
|
||||||
timeframe: float,
|
timeframe: float,
|
||||||
sampler_stream: tractor.MsgStream,
|
sampler_stream: tractor.MsgStream,
|
||||||
|
feed_is_live: trio.Event,
|
||||||
|
|
||||||
last_tsdb_dt: Optional[datetime] = None,
|
last_tsdb_dt: Optional[datetime] = None,
|
||||||
storage: Optional[Storage] = None,
|
storage: Optional[Storage] = None,
|
||||||
|
@ -281,7 +282,14 @@ async def start_backfill(
|
||||||
- pendulum.from_timestamp(times[-2])
|
- pendulum.from_timestamp(times[-2])
|
||||||
).seconds
|
).seconds
|
||||||
|
|
||||||
if step_size_s == 60:
|
# if the market is open (aka we have a live feed) but the
|
||||||
|
# history sample step index seems off we report the surrounding
|
||||||
|
# data and drop into a bp. this case shouldn't really ever
|
||||||
|
# happen if we're doing history retrieval correctly.
|
||||||
|
if (
|
||||||
|
step_size_s == 60
|
||||||
|
and feed_is_live.is_set()
|
||||||
|
):
|
||||||
inow = round(time.time())
|
inow = round(time.time())
|
||||||
diff = inow - times[-1]
|
diff = inow - times[-1]
|
||||||
if abs(diff) > 60:
|
if abs(diff) > 60:
|
||||||
|
@ -499,6 +507,7 @@ async def basic_backfill(
|
||||||
bfqsn: str,
|
bfqsn: str,
|
||||||
shms: dict[int, ShmArray],
|
shms: dict[int, ShmArray],
|
||||||
sampler_stream: tractor.MsgStream,
|
sampler_stream: tractor.MsgStream,
|
||||||
|
feed_is_live: trio.Event,
|
||||||
|
|
||||||
) -> None:
|
) -> None:
|
||||||
|
|
||||||
|
@ -518,6 +527,7 @@ async def basic_backfill(
|
||||||
shm,
|
shm,
|
||||||
timeframe,
|
timeframe,
|
||||||
sampler_stream,
|
sampler_stream,
|
||||||
|
feed_is_live,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
except DataUnavailable:
|
except DataUnavailable:
|
||||||
|
@ -534,6 +544,7 @@ async def tsdb_backfill(
|
||||||
bfqsn: str,
|
bfqsn: str,
|
||||||
shms: dict[int, ShmArray],
|
shms: dict[int, ShmArray],
|
||||||
sampler_stream: tractor.MsgStream,
|
sampler_stream: tractor.MsgStream,
|
||||||
|
feed_is_live: trio.Event,
|
||||||
|
|
||||||
task_status: TaskStatus[
|
task_status: TaskStatus[
|
||||||
tuple[ShmArray, ShmArray]
|
tuple[ShmArray, ShmArray]
|
||||||
|
@ -568,6 +579,8 @@ async def tsdb_backfill(
|
||||||
shm,
|
shm,
|
||||||
timeframe,
|
timeframe,
|
||||||
sampler_stream,
|
sampler_stream,
|
||||||
|
feed_is_live,
|
||||||
|
|
||||||
last_tsdb_dt=last_tsdb_dt,
|
last_tsdb_dt=last_tsdb_dt,
|
||||||
tsdb_is_up=True,
|
tsdb_is_up=True,
|
||||||
storage=storage,
|
storage=storage,
|
||||||
|
@ -870,6 +883,7 @@ async def manage_history(
|
||||||
60: hist_shm,
|
60: hist_shm,
|
||||||
},
|
},
|
||||||
sample_stream,
|
sample_stream,
|
||||||
|
feed_is_live,
|
||||||
)
|
)
|
||||||
|
|
||||||
# yield back after client connect with filled shm
|
# yield back after client connect with filled shm
|
||||||
|
@ -904,6 +918,7 @@ async def manage_history(
|
||||||
60: hist_shm,
|
60: hist_shm,
|
||||||
},
|
},
|
||||||
sample_stream,
|
sample_stream,
|
||||||
|
feed_is_live,
|
||||||
)
|
)
|
||||||
task_status.started((
|
task_status.started((
|
||||||
hist_zero_index,
|
hist_zero_index,
|
||||||
|
@ -1065,7 +1080,10 @@ async def allocate_persistent_feed(
|
||||||
# seed the buffer with a history datum - this is most handy
|
# seed the buffer with a history datum - this is most handy
|
||||||
# for many backends which don't sample @ 1s OHLC but do have
|
# for many backends which don't sample @ 1s OHLC but do have
|
||||||
# slower data such as 1m OHLC.
|
# slower data such as 1m OHLC.
|
||||||
if not len(rt_shm.array):
|
if (
|
||||||
|
not len(rt_shm.array)
|
||||||
|
and hist_shm.array.size
|
||||||
|
):
|
||||||
rt_shm.push(hist_shm.array[-3:-1])
|
rt_shm.push(hist_shm.array[-3:-1])
|
||||||
ohlckeys = ['open', 'high', 'low', 'close']
|
ohlckeys = ['open', 'high', 'low', 'close']
|
||||||
rt_shm.array[ohlckeys][-2:] = hist_shm.array['close'][-1]
|
rt_shm.array[ohlckeys][-2:] = hist_shm.array['close'][-1]
|
||||||
|
@ -1076,6 +1094,9 @@ async def allocate_persistent_feed(
|
||||||
rt_shm.array['time'][0] = ts
|
rt_shm.array['time'][0] = ts
|
||||||
rt_shm.array['time'][1] = ts + 1
|
rt_shm.array['time'][1] = ts + 1
|
||||||
|
|
||||||
|
elif hist_shm.array.size == 0:
|
||||||
|
await tractor.breakpoint()
|
||||||
|
|
||||||
# wait the spawning parent task to register its subscriber
|
# wait the spawning parent task to register its subscriber
|
||||||
# send-stream entry before we start the sample loop.
|
# send-stream entry before we start the sample loop.
|
||||||
await sub_registered.wait()
|
await sub_registered.wait()
|
||||||
|
|
|
@ -22,17 +22,11 @@ real-time data processing data-structures.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
from contextlib import asynccontextmanager as acm
|
|
||||||
from functools import partial
|
|
||||||
from typing import (
|
from typing import (
|
||||||
AsyncIterator,
|
|
||||||
TYPE_CHECKING,
|
TYPE_CHECKING,
|
||||||
)
|
)
|
||||||
|
|
||||||
import tractor
|
import tractor
|
||||||
from tractor.trionics import (
|
|
||||||
maybe_open_context,
|
|
||||||
)
|
|
||||||
import pendulum
|
import pendulum
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
||||||
|
@ -45,9 +39,6 @@ from ._sharedmem import (
|
||||||
ShmArray,
|
ShmArray,
|
||||||
_Token,
|
_Token,
|
||||||
)
|
)
|
||||||
from ._sampling import (
|
|
||||||
open_sample_stream,
|
|
||||||
)
|
|
||||||
# from .._profile import (
|
# from .._profile import (
|
||||||
# Profiler,
|
# Profiler,
|
||||||
# pg_profile_enabled,
|
# pg_profile_enabled,
|
||||||
|
@ -151,26 +142,6 @@ class Flume(Struct):
|
||||||
async def receive(self) -> dict:
|
async def receive(self) -> dict:
|
||||||
return await self.stream.receive()
|
return await self.stream.receive()
|
||||||
|
|
||||||
@acm
|
|
||||||
async def index_stream(
|
|
||||||
self,
|
|
||||||
delay_s: float = 1,
|
|
||||||
|
|
||||||
) -> AsyncIterator[int]:
|
|
||||||
|
|
||||||
if not self.feed:
|
|
||||||
raise RuntimeError('This flume is not part of any ``Feed``?')
|
|
||||||
|
|
||||||
# TODO: maybe a public (property) API for this in ``tractor``?
|
|
||||||
portal = self.stream._ctx._portal
|
|
||||||
assert portal
|
|
||||||
|
|
||||||
# XXX: this should be singleton on a host,
|
|
||||||
# a lone broker-daemon per provider should be
|
|
||||||
# created for all practical purposes
|
|
||||||
async with open_sample_stream(float(delay_s)) as stream:
|
|
||||||
yield stream
|
|
||||||
|
|
||||||
def get_ds_info(
|
def get_ds_info(
|
||||||
self,
|
self,
|
||||||
) -> tuple[float, float, float]:
|
) -> tuple[float, float, float]:
|
||||||
|
|
|
@ -54,7 +54,7 @@ def open_trade_ledger(
|
||||||
broker: str,
|
broker: str,
|
||||||
account: str,
|
account: str,
|
||||||
|
|
||||||
) -> str:
|
) -> dict:
|
||||||
'''
|
'''
|
||||||
Indempotently create and read in a trade log file from the
|
Indempotently create and read in a trade log file from the
|
||||||
``<configuration_dir>/ledgers/`` directory.
|
``<configuration_dir>/ledgers/`` directory.
|
||||||
|
|
|
@ -50,7 +50,6 @@ from ._cursor import (
|
||||||
ContentsLabel,
|
ContentsLabel,
|
||||||
)
|
)
|
||||||
from ..data._sharedmem import ShmArray
|
from ..data._sharedmem import ShmArray
|
||||||
from ._l1 import L1Labels
|
|
||||||
from ._ohlc import BarItems
|
from ._ohlc import BarItems
|
||||||
from ._curve import (
|
from ._curve import (
|
||||||
Curve,
|
Curve,
|
||||||
|
@ -70,12 +69,10 @@ from ..data._source import Symbol
|
||||||
from ..log import get_logger
|
from ..log import get_logger
|
||||||
from ._interaction import ChartView
|
from ._interaction import ChartView
|
||||||
from ._forms import FieldsForm
|
from ._forms import FieldsForm
|
||||||
from .._profile import pg_profile_enabled, ms_slower_then
|
|
||||||
from ._overlay import PlotItemOverlay
|
from ._overlay import PlotItemOverlay
|
||||||
from ._dataviz import Viz
|
from ._dataviz import Viz
|
||||||
from ._search import SearchWidget
|
from ._search import SearchWidget
|
||||||
from . import _pg_overrides as pgo
|
from . import _pg_overrides as pgo
|
||||||
from .._profile import Profiler
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from ._display import DisplayState
|
from ._display import DisplayState
|
||||||
|
@ -127,7 +124,10 @@ class GodWidget(QWidget):
|
||||||
# self.init_strategy_ui()
|
# self.init_strategy_ui()
|
||||||
# self.vbox.addLayout(self.hbox)
|
# self.vbox.addLayout(self.hbox)
|
||||||
|
|
||||||
self._chart_cache: dict[str, LinkedSplits] = {}
|
self._chart_cache: dict[
|
||||||
|
str,
|
||||||
|
tuple[LinkedSplits, LinkedSplits],
|
||||||
|
] = {}
|
||||||
|
|
||||||
self.hist_linked: Optional[LinkedSplits] = None
|
self.hist_linked: Optional[LinkedSplits] = None
|
||||||
self.rt_linked: Optional[LinkedSplits] = None
|
self.rt_linked: Optional[LinkedSplits] = None
|
||||||
|
@ -147,23 +147,6 @@ class GodWidget(QWidget):
|
||||||
def linkedsplits(self) -> LinkedSplits:
|
def linkedsplits(self) -> LinkedSplits:
|
||||||
return self.rt_linked
|
return self.rt_linked
|
||||||
|
|
||||||
# def init_timeframes_ui(self):
|
|
||||||
# self.tf_layout = QHBoxLayout()
|
|
||||||
# self.tf_layout.setSpacing(0)
|
|
||||||
# self.tf_layout.setContentsMargins(0, 12, 0, 0)
|
|
||||||
# time_frames = ('1M', '5M', '15M', '30M', '1H', '1D', '1W', 'MN')
|
|
||||||
# btn_prefix = 'TF'
|
|
||||||
|
|
||||||
# for tf in time_frames:
|
|
||||||
# btn_name = ''.join([btn_prefix, tf])
|
|
||||||
# btn = QtWidgets.QPushButton(tf)
|
|
||||||
# # TODO:
|
|
||||||
# btn.setEnabled(False)
|
|
||||||
# setattr(self, btn_name, btn)
|
|
||||||
# self.tf_layout.addWidget(btn)
|
|
||||||
|
|
||||||
# self.toolbar_layout.addLayout(self.tf_layout)
|
|
||||||
|
|
||||||
# XXX: strat loader/saver that we don't need yet.
|
# XXX: strat loader/saver that we don't need yet.
|
||||||
# def init_strategy_ui(self):
|
# def init_strategy_ui(self):
|
||||||
# self.strategy_box = StrategyBoxWidget(self)
|
# self.strategy_box = StrategyBoxWidget(self)
|
||||||
|
@ -545,6 +528,8 @@ class LinkedSplits(QWidget):
|
||||||
|
|
||||||
style: str = 'ohlc_bar',
|
style: str = 'ohlc_bar',
|
||||||
|
|
||||||
|
**add_plot_kwargs,
|
||||||
|
|
||||||
) -> ChartPlotWidget:
|
) -> ChartPlotWidget:
|
||||||
'''
|
'''
|
||||||
Start up and show main (price) chart and all linked subcharts.
|
Start up and show main (price) chart and all linked subcharts.
|
||||||
|
@ -569,6 +554,7 @@ class LinkedSplits(QWidget):
|
||||||
style=style,
|
style=style,
|
||||||
_is_main=True,
|
_is_main=True,
|
||||||
sidepane=sidepane,
|
sidepane=sidepane,
|
||||||
|
**add_plot_kwargs,
|
||||||
)
|
)
|
||||||
# add crosshair graphic
|
# add crosshair graphic
|
||||||
self.chart.addItem(self.cursor)
|
self.chart.addItem(self.cursor)
|
||||||
|
@ -593,6 +579,7 @@ class LinkedSplits(QWidget):
|
||||||
_is_main: bool = False,
|
_is_main: bool = False,
|
||||||
|
|
||||||
sidepane: Optional[QWidget] = None,
|
sidepane: Optional[QWidget] = None,
|
||||||
|
draw_kwargs: dict = {},
|
||||||
|
|
||||||
**cpw_kwargs,
|
**cpw_kwargs,
|
||||||
|
|
||||||
|
@ -650,7 +637,8 @@ class LinkedSplits(QWidget):
|
||||||
cpw.hideAxis('bottom')
|
cpw.hideAxis('bottom')
|
||||||
|
|
||||||
if (
|
if (
|
||||||
_xaxis_at == 'bottom' and (
|
_xaxis_at == 'bottom'
|
||||||
|
and (
|
||||||
self.xaxis_chart
|
self.xaxis_chart
|
||||||
or (
|
or (
|
||||||
not self.subplots
|
not self.subplots
|
||||||
|
@ -658,6 +646,8 @@ class LinkedSplits(QWidget):
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
):
|
):
|
||||||
|
# hide the previous x-axis chart's bottom axis since we're
|
||||||
|
# presumably being appended to the bottom subplot.
|
||||||
if self.xaxis_chart:
|
if self.xaxis_chart:
|
||||||
self.xaxis_chart.hideAxis('bottom')
|
self.xaxis_chart.hideAxis('bottom')
|
||||||
|
|
||||||
|
@ -702,7 +692,12 @@ class LinkedSplits(QWidget):
|
||||||
# link chart x-axis to main chart
|
# link chart x-axis to main chart
|
||||||
# this is 1/2 of where the `Link` in ``LinkedSplit``
|
# this is 1/2 of where the `Link` in ``LinkedSplit``
|
||||||
# comes from ;)
|
# comes from ;)
|
||||||
cpw.setXLink(self.chart)
|
cpw.cv.setXLink(self.chart)
|
||||||
|
|
||||||
|
# NOTE: above is the same as the following,
|
||||||
|
# link this subchart's axes to the main top level chart.
|
||||||
|
# if self.chart:
|
||||||
|
# cpw.cv.linkView(0, self.chart.cv)
|
||||||
|
|
||||||
add_label = False
|
add_label = False
|
||||||
anchor_at = ('top', 'left')
|
anchor_at = ('top', 'left')
|
||||||
|
@ -710,12 +705,12 @@ class LinkedSplits(QWidget):
|
||||||
# draw curve graphics
|
# draw curve graphics
|
||||||
if style == 'ohlc_bar':
|
if style == 'ohlc_bar':
|
||||||
|
|
||||||
# graphics, data_key = cpw.draw_ohlc(
|
|
||||||
viz = cpw.draw_ohlc(
|
viz = cpw.draw_ohlc(
|
||||||
name,
|
name,
|
||||||
shm,
|
shm,
|
||||||
flume=flume,
|
flume=flume,
|
||||||
array_key=array_key
|
array_key=array_key,
|
||||||
|
**draw_kwargs,
|
||||||
)
|
)
|
||||||
self.cursor.contents_labels.add_label(
|
self.cursor.contents_labels.add_label(
|
||||||
cpw,
|
cpw,
|
||||||
|
@ -733,6 +728,7 @@ class LinkedSplits(QWidget):
|
||||||
flume,
|
flume,
|
||||||
array_key=array_key,
|
array_key=array_key,
|
||||||
color='default_light',
|
color='default_light',
|
||||||
|
**draw_kwargs,
|
||||||
)
|
)
|
||||||
|
|
||||||
elif style == 'step':
|
elif style == 'step':
|
||||||
|
@ -746,11 +742,21 @@ class LinkedSplits(QWidget):
|
||||||
step_mode=True,
|
step_mode=True,
|
||||||
color='davies',
|
color='davies',
|
||||||
fill_color='davies',
|
fill_color='davies',
|
||||||
|
**draw_kwargs,
|
||||||
)
|
)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
raise ValueError(f"Chart style {style} is currently unsupported")
|
raise ValueError(f"Chart style {style} is currently unsupported")
|
||||||
|
|
||||||
|
# NOTE: back-link the new sub-chart to trigger y-autoranging in
|
||||||
|
# the (ohlc parent) main chart for this linked set.
|
||||||
|
if self.chart:
|
||||||
|
main_viz = self.chart.get_viz(self.chart.name)
|
||||||
|
self.chart.view.enable_auto_yrange(
|
||||||
|
src_vb=cpw.view,
|
||||||
|
viz=main_viz,
|
||||||
|
)
|
||||||
|
|
||||||
graphics = viz.graphics
|
graphics = viz.graphics
|
||||||
data_key = viz.name
|
data_key = viz.name
|
||||||
|
|
||||||
|
@ -814,7 +820,9 @@ class LinkedSplits(QWidget):
|
||||||
# write our own wrapper around `PlotItem`..
|
# write our own wrapper around `PlotItem`..
|
||||||
class ChartPlotWidget(pg.PlotWidget):
|
class ChartPlotWidget(pg.PlotWidget):
|
||||||
'''
|
'''
|
||||||
``GraphicsView`` subtype containing a single ``PlotItem``.
|
``GraphicsView`` subtype containing a ``.plotItem: PlotItem`` as well
|
||||||
|
as a `.pi_overlay: PlotItemOverlay`` which helps manage and overlay flow
|
||||||
|
graphics view multiple compose view boxes.
|
||||||
|
|
||||||
- The added methods allow for plotting OHLC sequences from
|
- The added methods allow for plotting OHLC sequences from
|
||||||
``np.ndarray``s with appropriate field names.
|
``np.ndarray``s with appropriate field names.
|
||||||
|
@ -871,17 +879,17 @@ class ChartPlotWidget(pg.PlotWidget):
|
||||||
self.sidepane: Optional[FieldsForm] = None
|
self.sidepane: Optional[FieldsForm] = None
|
||||||
|
|
||||||
# source of our custom interactions
|
# source of our custom interactions
|
||||||
self.cv = cv = self.mk_vb(name)
|
self.cv = self.mk_vb(name)
|
||||||
|
|
||||||
pi = pgo.PlotItem(
|
pi = pgo.PlotItem(
|
||||||
viewBox=cv,
|
viewBox=self.cv,
|
||||||
name=name,
|
name=name,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
)
|
)
|
||||||
pi.chart_widget = self
|
pi.chart_widget = self
|
||||||
super().__init__(
|
super().__init__(
|
||||||
background=hcolor(view_color),
|
background=hcolor(view_color),
|
||||||
viewBox=cv,
|
viewBox=self.cv,
|
||||||
# parent=None,
|
# parent=None,
|
||||||
# plotItem=None,
|
# plotItem=None,
|
||||||
# antialias=True,
|
# antialias=True,
|
||||||
|
@ -892,7 +900,9 @@ class ChartPlotWidget(pg.PlotWidget):
|
||||||
# give viewbox as reference to chart
|
# give viewbox as reference to chart
|
||||||
# allowing for kb controls and interactions on **this** widget
|
# allowing for kb controls and interactions on **this** widget
|
||||||
# (see our custom view mode in `._interactions.py`)
|
# (see our custom view mode in `._interactions.py`)
|
||||||
cv.chart = self
|
self.cv.chart = self
|
||||||
|
|
||||||
|
self.pi_overlay: PlotItemOverlay = PlotItemOverlay(self.plotItem)
|
||||||
|
|
||||||
# ensure internal pi matches
|
# ensure internal pi matches
|
||||||
assert self.cv is self.plotItem.vb
|
assert self.cv is self.plotItem.vb
|
||||||
|
@ -921,8 +931,6 @@ class ChartPlotWidget(pg.PlotWidget):
|
||||||
# show background grid
|
# show background grid
|
||||||
self.showGrid(x=False, y=True, alpha=0.3)
|
self.showGrid(x=False, y=True, alpha=0.3)
|
||||||
|
|
||||||
self.pi_overlay: PlotItemOverlay = PlotItemOverlay(self.plotItem)
|
|
||||||
|
|
||||||
# indempotent startup flag for auto-yrange subsys
|
# indempotent startup flag for auto-yrange subsys
|
||||||
# to detect the "first time" y-domain graphics begin
|
# to detect the "first time" y-domain graphics begin
|
||||||
# to be shown in the (main) graphics view.
|
# to be shown in the (main) graphics view.
|
||||||
|
@ -1111,14 +1119,6 @@ class ChartPlotWidget(pg.PlotWidget):
|
||||||
link_axes=(0,),
|
link_axes=(0,),
|
||||||
)
|
)
|
||||||
|
|
||||||
# connect auto-yrange callbacks *from* this new
|
|
||||||
# view **to** this parent and likewise *from* the
|
|
||||||
# main/parent chart back *to* the created overlay.
|
|
||||||
cv.enable_auto_yrange(src_vb=self.view)
|
|
||||||
# makes it so that interaction on the new overlay will reflect
|
|
||||||
# back on the main chart (which overlay was added to).
|
|
||||||
self.view.enable_auto_yrange(src_vb=cv)
|
|
||||||
|
|
||||||
# add axis title
|
# add axis title
|
||||||
# TODO: do we want this API to still work?
|
# TODO: do we want this API to still work?
|
||||||
# raxis = pi.getAxis('right')
|
# raxis = pi.getAxis('right')
|
||||||
|
@ -1158,8 +1158,6 @@ class ChartPlotWidget(pg.PlotWidget):
|
||||||
|
|
||||||
if is_ohlc:
|
if is_ohlc:
|
||||||
graphics = BarItems(
|
graphics = BarItems(
|
||||||
linked=self.linked,
|
|
||||||
plotitem=pi,
|
|
||||||
color=color,
|
color=color,
|
||||||
name=name,
|
name=name,
|
||||||
**graphics_kwargs,
|
**graphics_kwargs,
|
||||||
|
@ -1189,6 +1187,16 @@ class ChartPlotWidget(pg.PlotWidget):
|
||||||
# register curve graphics with this viz
|
# register curve graphics with this viz
|
||||||
graphics=graphics,
|
graphics=graphics,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# connect auto-yrange callbacks *from* this new
|
||||||
|
# view **to** this parent and likewise *from* the
|
||||||
|
# main/parent chart back *to* the created overlay.
|
||||||
|
pi.vb.enable_auto_yrange(
|
||||||
|
src_vb=self.view,
|
||||||
|
viz=viz,
|
||||||
|
)
|
||||||
|
|
||||||
|
pi.viz = viz
|
||||||
assert isinstance(viz.shm, ShmArray)
|
assert isinstance(viz.shm, ShmArray)
|
||||||
|
|
||||||
# TODO: this probably needs its own method?
|
# TODO: this probably needs its own method?
|
||||||
|
@ -1316,13 +1324,6 @@ class ChartPlotWidget(pg.PlotWidget):
|
||||||
If ``bars_range`` is provided use that range.
|
If ``bars_range`` is provided use that range.
|
||||||
|
|
||||||
'''
|
'''
|
||||||
profiler = Profiler(
|
|
||||||
msg=f'`{str(self)}.maxmin(name={name})`: `{self.name}`',
|
|
||||||
disabled=not pg_profile_enabled(),
|
|
||||||
ms_threshold=ms_slower_then,
|
|
||||||
delayed=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
# TODO: here we should instead look up the ``Viz.shm.array``
|
# TODO: here we should instead look up the ``Viz.shm.array``
|
||||||
# and read directly from shm to avoid copying to memory first
|
# and read directly from shm to avoid copying to memory first
|
||||||
# and then reading it again here.
|
# and then reading it again here.
|
||||||
|
@ -1330,36 +1331,21 @@ class ChartPlotWidget(pg.PlotWidget):
|
||||||
viz = self._vizs.get(viz_key)
|
viz = self._vizs.get(viz_key)
|
||||||
if viz is None:
|
if viz is None:
|
||||||
log.error(f"viz {viz_key} doesn't exist in chart {self.name} !?")
|
log.error(f"viz {viz_key} doesn't exist in chart {self.name} !?")
|
||||||
key = res = 0, 0
|
return 0, 0
|
||||||
|
|
||||||
|
res = viz.maxmin()
|
||||||
|
|
||||||
|
if (
|
||||||
|
res is None
|
||||||
|
):
|
||||||
|
mxmn = 0, 0
|
||||||
|
if not self._on_screen:
|
||||||
|
self.default_view(do_ds=False)
|
||||||
|
self._on_screen = True
|
||||||
else:
|
else:
|
||||||
(
|
x_range, read_slc, mxmn = res
|
||||||
l,
|
|
||||||
_,
|
|
||||||
lbar,
|
|
||||||
rbar,
|
|
||||||
_,
|
|
||||||
r,
|
|
||||||
) = bars_range or viz.datums_range()
|
|
||||||
|
|
||||||
profiler(f'{self.name} got bars range')
|
return mxmn
|
||||||
key = lbar, rbar
|
|
||||||
res = viz.maxmin(*key)
|
|
||||||
|
|
||||||
if (
|
|
||||||
res is None
|
|
||||||
):
|
|
||||||
log.warning(
|
|
||||||
f"{viz_key} no mxmn for bars_range => {key} !?"
|
|
||||||
)
|
|
||||||
res = 0, 0
|
|
||||||
if not self._on_screen:
|
|
||||||
self.default_view(do_ds=False)
|
|
||||||
self._on_screen = True
|
|
||||||
|
|
||||||
profiler(f'yrange mxmn: {key} -> {res}')
|
|
||||||
# print(f'{viz_key} yrange mxmn: {key} -> {res}')
|
|
||||||
return res
|
|
||||||
|
|
||||||
def get_viz(
|
def get_viz(
|
||||||
self,
|
self,
|
||||||
|
|
|
@ -198,12 +198,11 @@ class ContentsLabel(pg.LabelItem):
|
||||||
self,
|
self,
|
||||||
|
|
||||||
name: str,
|
name: str,
|
||||||
index: int,
|
ix: int,
|
||||||
array: np.ndarray,
|
array: np.ndarray,
|
||||||
|
|
||||||
) -> None:
|
) -> None:
|
||||||
# this being "html" is the dumbest shit :eyeroll:
|
# this being "html" is the dumbest shit :eyeroll:
|
||||||
first = array[0]['index']
|
|
||||||
|
|
||||||
self.setText(
|
self.setText(
|
||||||
"<b>i</b>:{index}<br/>"
|
"<b>i</b>:{index}<br/>"
|
||||||
|
@ -216,7 +215,7 @@ class ContentsLabel(pg.LabelItem):
|
||||||
"<b>C</b>:{}<br/>"
|
"<b>C</b>:{}<br/>"
|
||||||
"<b>V</b>:{}<br/>"
|
"<b>V</b>:{}<br/>"
|
||||||
"<b>wap</b>:{}".format(
|
"<b>wap</b>:{}".format(
|
||||||
*array[index - first][
|
*array[ix][
|
||||||
[
|
[
|
||||||
'time',
|
'time',
|
||||||
'open',
|
'open',
|
||||||
|
@ -228,7 +227,7 @@ class ContentsLabel(pg.LabelItem):
|
||||||
]
|
]
|
||||||
],
|
],
|
||||||
name=name,
|
name=name,
|
||||||
index=index,
|
index=ix,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -236,15 +235,12 @@ class ContentsLabel(pg.LabelItem):
|
||||||
self,
|
self,
|
||||||
|
|
||||||
name: str,
|
name: str,
|
||||||
index: int,
|
ix: int,
|
||||||
array: np.ndarray,
|
array: np.ndarray,
|
||||||
|
|
||||||
) -> None:
|
) -> None:
|
||||||
|
data = array[ix][name]
|
||||||
first = array[0]['index']
|
self.setText(f"{name}: {data:.2f}")
|
||||||
if index < array[-1]['index'] and index > first:
|
|
||||||
data = array[index - first][name]
|
|
||||||
self.setText(f"{name}: {data:.2f}")
|
|
||||||
|
|
||||||
|
|
||||||
class ContentsLabels:
|
class ContentsLabels:
|
||||||
|
@ -269,17 +265,20 @@ class ContentsLabels:
|
||||||
|
|
||||||
def update_labels(
|
def update_labels(
|
||||||
self,
|
self,
|
||||||
index: int,
|
x_in: int,
|
||||||
|
|
||||||
) -> None:
|
) -> None:
|
||||||
for chart, name, label, update in self._labels:
|
for chart, name, label, update in self._labels:
|
||||||
|
|
||||||
viz = chart.get_viz(name)
|
viz = chart.get_viz(name)
|
||||||
array = viz.shm.array
|
array = viz.shm.array
|
||||||
|
index = array[viz.index_field]
|
||||||
|
start = index[0]
|
||||||
|
stop = index[-1]
|
||||||
|
|
||||||
if not (
|
if not (
|
||||||
index >= 0
|
x_in >= start
|
||||||
and index < array[-1]['index']
|
and x_in <= stop
|
||||||
):
|
):
|
||||||
# out of range
|
# out of range
|
||||||
print('WTF out of range?')
|
print('WTF out of range?')
|
||||||
|
@ -288,7 +287,10 @@ class ContentsLabels:
|
||||||
# call provided update func with data point
|
# call provided update func with data point
|
||||||
try:
|
try:
|
||||||
label.show()
|
label.show()
|
||||||
update(index, array)
|
ix = np.searchsorted(index, x_in)
|
||||||
|
if ix > len(array):
|
||||||
|
breakpoint()
|
||||||
|
update(ix, array)
|
||||||
|
|
||||||
except IndexError:
|
except IndexError:
|
||||||
log.exception(f"Failed to update label: {name}")
|
log.exception(f"Failed to update label: {name}")
|
||||||
|
|
|
@ -60,11 +60,89 @@ class FlowGraphic(pg.GraphicsObject):
|
||||||
|
|
||||||
'''
|
'''
|
||||||
# sub-type customization methods
|
# sub-type customization methods
|
||||||
declare_paintables: Optional[Callable] = None
|
declare_paintables: Callable | None = None
|
||||||
sub_paint: Optional[Callable] = None
|
sub_paint: Callable | None = None
|
||||||
|
|
||||||
# TODO: can we remove this?
|
# XXX-NOTE-XXX: graphics caching B)
|
||||||
# sub_br: Optional[Callable] = None
|
# see explanation for different caching modes:
|
||||||
|
# https://stackoverflow.com/a/39410081
|
||||||
|
cache_mode: int = QGraphicsItem.DeviceCoordinateCache
|
||||||
|
# XXX: WARNING item caching seems to only be useful
|
||||||
|
# if we don't re-generate the entire QPainterPath every time
|
||||||
|
# don't ever use this - it's a colossal nightmare of artefacts
|
||||||
|
# and is disastrous for performance.
|
||||||
|
# QGraphicsItem.ItemCoordinateCache
|
||||||
|
# TODO: still questions todo with coord-cacheing that we should
|
||||||
|
# probably talk to a core dev about:
|
||||||
|
# - if this makes trasform interactions slower (such as zooming)
|
||||||
|
# and if so maybe if/when we implement a "history" mode for the
|
||||||
|
# view we disable this in that mode?
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
*args,
|
||||||
|
name: str | None = None,
|
||||||
|
|
||||||
|
# line styling
|
||||||
|
color: str = 'bracket',
|
||||||
|
last_step_color: str | None = None,
|
||||||
|
fill_color: Optional[str] = None,
|
||||||
|
style: str = 'solid',
|
||||||
|
|
||||||
|
**kwargs
|
||||||
|
|
||||||
|
) -> None:
|
||||||
|
|
||||||
|
self._name = name
|
||||||
|
|
||||||
|
# primary graphics item used for history
|
||||||
|
self.path: QPainterPath = QPainterPath()
|
||||||
|
|
||||||
|
# additional path that can be optionally used for appends which
|
||||||
|
# tries to avoid triggering an update/redraw of the presumably
|
||||||
|
# larger historical ``.path`` above. the flag to enable
|
||||||
|
# this behaviour is found in `Renderer.render()`.
|
||||||
|
self.fast_path: QPainterPath | None = None
|
||||||
|
|
||||||
|
# TODO: evaluating the path capacity stuff and see
|
||||||
|
# if it really makes much diff pre-allocating it.
|
||||||
|
# self._last_cap: int = 0
|
||||||
|
# cap = path.capacity()
|
||||||
|
# if cap != self._last_cap:
|
||||||
|
# print(f'NEW CAPACITY: {self._last_cap} -> {cap}')
|
||||||
|
# self._last_cap = cap
|
||||||
|
|
||||||
|
# all history of curve is drawn in single px thickness
|
||||||
|
self._color: str = color
|
||||||
|
pen = pg.mkPen(hcolor(color), width=1)
|
||||||
|
pen.setStyle(_line_styles[style])
|
||||||
|
|
||||||
|
if 'dash' in style:
|
||||||
|
pen.setDashPattern([8, 3])
|
||||||
|
|
||||||
|
self._pen = pen
|
||||||
|
self._brush = pg.functions.mkBrush(
|
||||||
|
hcolor(fill_color or color)
|
||||||
|
)
|
||||||
|
|
||||||
|
# last segment is drawn in 2px thickness for emphasis
|
||||||
|
if last_step_color:
|
||||||
|
self.last_step_pen = pg.mkPen(
|
||||||
|
hcolor(last_step_color),
|
||||||
|
width=2,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
self.last_step_pen = pg.mkPen(
|
||||||
|
self._pen,
|
||||||
|
width=2,
|
||||||
|
)
|
||||||
|
|
||||||
|
self._last_line: QLineF = QLineF()
|
||||||
|
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
# apply cache mode
|
||||||
|
self.setCacheMode(self.cache_mode)
|
||||||
|
|
||||||
def x_uppx(self) -> int:
|
def x_uppx(self) -> int:
|
||||||
|
|
||||||
|
@ -112,81 +190,32 @@ class Curve(FlowGraphic):
|
||||||
updates don't trigger a full path redraw.
|
updates don't trigger a full path redraw.
|
||||||
|
|
||||||
'''
|
'''
|
||||||
|
# TODO: can we remove this?
|
||||||
|
# sub_br: Optional[Callable] = None
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
*args,
|
*args,
|
||||||
|
|
||||||
step_mode: bool = False,
|
# color: str = 'default_lightest',
|
||||||
color: str = 'default_lightest',
|
# fill_color: Optional[str] = None,
|
||||||
fill_color: Optional[str] = None,
|
# style: str = 'solid',
|
||||||
style: str = 'solid',
|
|
||||||
name: Optional[str] = None,
|
|
||||||
|
|
||||||
**kwargs
|
**kwargs
|
||||||
|
|
||||||
) -> None:
|
) -> None:
|
||||||
|
|
||||||
self._name = name
|
|
||||||
|
|
||||||
# brutaaalll, see comments within..
|
# brutaaalll, see comments within..
|
||||||
self.yData = None
|
self.yData = None
|
||||||
self.xData = None
|
self.xData = None
|
||||||
|
|
||||||
# self._last_cap: int = 0
|
|
||||||
self.path: Optional[QPainterPath] = None
|
|
||||||
|
|
||||||
# additional path that can be optionally used for appends which
|
|
||||||
# tries to avoid triggering an update/redraw of the presumably
|
|
||||||
# larger historical ``.path`` above. the flag to enable
|
|
||||||
# this behaviour is found in `Renderer.render()`.
|
|
||||||
self.fast_path: QPainterPath | None = None
|
|
||||||
|
|
||||||
# TODO: we can probably just dispense with the parent since
|
# TODO: we can probably just dispense with the parent since
|
||||||
# we're basically only using the pen setting now...
|
# we're basically only using the pen setting now...
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
# all history of curve is drawn in single px thickness
|
|
||||||
pen = pg.mkPen(hcolor(color))
|
|
||||||
pen.setStyle(_line_styles[style])
|
|
||||||
|
|
||||||
if 'dash' in style:
|
|
||||||
pen.setDashPattern([8, 3])
|
|
||||||
|
|
||||||
self._pen = pen
|
|
||||||
|
|
||||||
# last segment is drawn in 2px thickness for emphasis
|
|
||||||
# self.last_step_pen = pg.mkPen(hcolor(color), width=2)
|
|
||||||
self.last_step_pen = pg.mkPen(pen, width=2)
|
|
||||||
|
|
||||||
self._last_line: QLineF = QLineF()
|
self._last_line: QLineF = QLineF()
|
||||||
|
|
||||||
# flat-top style histogram-like discrete curve
|
|
||||||
# self._step_mode: bool = step_mode
|
|
||||||
|
|
||||||
# self._fill = True
|
# self._fill = True
|
||||||
self._brush = pg.functions.mkBrush(hcolor(fill_color or color))
|
|
||||||
|
|
||||||
# NOTE: this setting seems to mostly prevent redraws on mouse
|
|
||||||
# interaction which is a huge boon for avg interaction latency.
|
|
||||||
|
|
||||||
# TODO: one question still remaining is if this makes trasform
|
|
||||||
# interactions slower (such as zooming) and if so maybe if/when
|
|
||||||
# we implement a "history" mode for the view we disable this in
|
|
||||||
# that mode?
|
|
||||||
# don't enable caching by default for the case where the
|
|
||||||
# only thing drawn is the "last" line segment which can
|
|
||||||
# have a weird artifact where it won't be fully drawn to its
|
|
||||||
# endpoint (something we saw on trade rate curves)
|
|
||||||
self.setCacheMode(QGraphicsItem.DeviceCoordinateCache)
|
|
||||||
|
|
||||||
# XXX-NOTE-XXX: graphics caching.
|
|
||||||
# see explanation for different caching modes:
|
|
||||||
# https://stackoverflow.com/a/39410081 seems to only be useful
|
|
||||||
# if we don't re-generate the entire QPainterPath every time
|
|
||||||
# don't ever use this - it's a colossal nightmare of artefacts
|
|
||||||
# and is disastrous for performance.
|
|
||||||
# self.setCacheMode(QtWidgets.QGraphicsItem.ItemCoordinateCache)
|
|
||||||
|
|
||||||
# allow sub-type customization
|
# allow sub-type customization
|
||||||
declare = self.declare_paintables
|
declare = self.declare_paintables
|
||||||
|
@ -317,14 +346,10 @@ class Curve(FlowGraphic):
|
||||||
|
|
||||||
p.setPen(self.last_step_pen)
|
p.setPen(self.last_step_pen)
|
||||||
p.drawLine(self._last_line)
|
p.drawLine(self._last_line)
|
||||||
profiler('.drawLine()')
|
profiler('last datum `.drawLine()`')
|
||||||
p.setPen(self._pen)
|
|
||||||
|
|
||||||
|
p.setPen(self._pen)
|
||||||
path = self.path
|
path = self.path
|
||||||
# cap = path.capacity()
|
|
||||||
# if cap != self._last_cap:
|
|
||||||
# print(f'NEW CAPACITY: {self._last_cap} -> {cap}')
|
|
||||||
# self._last_cap = cap
|
|
||||||
|
|
||||||
if path:
|
if path:
|
||||||
p.drawPath(path)
|
p.drawPath(path)
|
||||||
|
@ -369,7 +394,7 @@ class Curve(FlowGraphic):
|
||||||
# from last datum to current such that
|
# from last datum to current such that
|
||||||
# the end of line touches the "beginning"
|
# the end of line touches the "beginning"
|
||||||
# of the current datum step span.
|
# of the current datum step span.
|
||||||
x_2last , y[-2],
|
x_2last, y[-2],
|
||||||
x_last, y[-1],
|
x_last, y[-1],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -382,6 +407,9 @@ class Curve(FlowGraphic):
|
||||||
# (via it's max / min) even when highly zoomed out.
|
# (via it's max / min) even when highly zoomed out.
|
||||||
class FlattenedOHLC(Curve):
|
class FlattenedOHLC(Curve):
|
||||||
|
|
||||||
|
# avoids strange dragging/smearing artifacts when panning..
|
||||||
|
cache_mode: int = QGraphicsItem.NoCache
|
||||||
|
|
||||||
def draw_last_datum(
|
def draw_last_datum(
|
||||||
self,
|
self,
|
||||||
path: QPainterPath,
|
path: QPainterPath,
|
||||||
|
|
|
@ -19,6 +19,10 @@ Data vizualization APIs
|
||||||
|
|
||||||
'''
|
'''
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
from math import (
|
||||||
|
ceil,
|
||||||
|
floor,
|
||||||
|
)
|
||||||
from typing import (
|
from typing import (
|
||||||
Optional,
|
Optional,
|
||||||
Literal,
|
Literal,
|
||||||
|
@ -56,6 +60,7 @@ from ..log import get_logger
|
||||||
from .._profile import (
|
from .._profile import (
|
||||||
Profiler,
|
Profiler,
|
||||||
pg_profile_enabled,
|
pg_profile_enabled,
|
||||||
|
ms_slower_then,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -127,9 +132,9 @@ def render_baritems(
|
||||||
|
|
||||||
# baseline "line" downsampled OHLC curve that should
|
# baseline "line" downsampled OHLC curve that should
|
||||||
# kick on only when we reach a certain uppx threshold.
|
# kick on only when we reach a certain uppx threshold.
|
||||||
self._render_table = (ds_curve_r, curve)
|
self._alt_r = (ds_curve_r, curve)
|
||||||
|
|
||||||
ds_r, curve = self._render_table
|
ds_r, curve = self._alt_r
|
||||||
|
|
||||||
# print(
|
# print(
|
||||||
# f'r: {r.fmtr.xy_slice}\n'
|
# f'r: {r.fmtr.xy_slice}\n'
|
||||||
|
@ -265,14 +270,17 @@ class Viz(msgspec.Struct): # , frozen=True):
|
||||||
_index_step: float | None = None
|
_index_step: float | None = None
|
||||||
|
|
||||||
# map from uppx -> (downsampled data, incremental graphics)
|
# map from uppx -> (downsampled data, incremental graphics)
|
||||||
_src_r: Optional[Renderer] = None
|
_src_r: Renderer | None = None
|
||||||
_render_table: dict[
|
_alt_r: tuple[
|
||||||
Optional[int],
|
Renderer,
|
||||||
tuple[Renderer, pg.GraphicsItem],
|
pg.GraphicsItem
|
||||||
] = (None, None)
|
] | None = None
|
||||||
|
|
||||||
# cache of y-range values per x-range input.
|
# cache of y-range values per x-range input.
|
||||||
_mxmns: dict[tuple[int, int], tuple[float, float]] = {}
|
_mxmns: dict[
|
||||||
|
tuple[int, int],
|
||||||
|
tuple[float, float],
|
||||||
|
] = {}
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def shm(self) -> ShmArray:
|
def shm(self) -> ShmArray:
|
||||||
|
@ -320,59 +328,97 @@ class Viz(msgspec.Struct): # , frozen=True):
|
||||||
|
|
||||||
def maxmin(
|
def maxmin(
|
||||||
self,
|
self,
|
||||||
lbar: int,
|
|
||||||
rbar: int,
|
|
||||||
|
|
||||||
|
x_range: slice | tuple[int, int] | None = None,
|
||||||
|
i_read_range: tuple[int, int] | None = None,
|
||||||
use_caching: bool = True,
|
use_caching: bool = True,
|
||||||
|
|
||||||
) -> Optional[tuple[float, float]]:
|
) -> tuple[float, float] | None:
|
||||||
'''
|
'''
|
||||||
Compute the cached max and min y-range values for a given
|
Compute the cached max and min y-range values for a given
|
||||||
x-range determined by ``lbar`` and ``rbar`` or ``None``
|
x-range determined by ``lbar`` and ``rbar`` or ``None``
|
||||||
if no range can be determined (yet).
|
if no range can be determined (yet).
|
||||||
|
|
||||||
'''
|
'''
|
||||||
# TODO: hash the slice instead maybe?
|
name = self.name
|
||||||
# https://stackoverflow.com/a/29980872
|
profiler = Profiler(
|
||||||
rkey = (round(lbar), round(rbar))
|
msg=f'`Viz[{name}].maxmin()`',
|
||||||
|
disabled=not pg_profile_enabled(),
|
||||||
do_print: bool = False
|
ms_threshold=4,
|
||||||
if use_caching:
|
delayed=True,
|
||||||
cached_result = self._mxmns.get(rkey)
|
)
|
||||||
if cached_result:
|
|
||||||
if do_print:
|
|
||||||
print(
|
|
||||||
f'{self.name} CACHED maxmin\n'
|
|
||||||
f'{rkey} -> {cached_result}'
|
|
||||||
)
|
|
||||||
return cached_result
|
|
||||||
|
|
||||||
shm = self.shm
|
shm = self.shm
|
||||||
if shm is None:
|
if shm is None:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
do_print: bool = False
|
||||||
arr = shm.array
|
arr = shm.array
|
||||||
|
|
||||||
# get relative slice indexes into array
|
if i_read_range is not None:
|
||||||
if self.index_field == 'time':
|
read_slc = slice(*i_read_range)
|
||||||
read_slc = slice_from_time(
|
index = arr[read_slc][self.index_field]
|
||||||
arr,
|
if not index.size:
|
||||||
start_t=lbar,
|
return None
|
||||||
stop_t=rbar,
|
ixrng = (index[0], index[-1])
|
||||||
step=self.index_step(),
|
|
||||||
)
|
|
||||||
|
|
||||||
else:
|
else:
|
||||||
ifirst = arr[0]['index']
|
if x_range is None:
|
||||||
read_slc = slice(
|
(
|
||||||
lbar - ifirst,
|
l,
|
||||||
(rbar - ifirst) + 1
|
_,
|
||||||
)
|
lbar,
|
||||||
|
rbar,
|
||||||
|
_,
|
||||||
|
r,
|
||||||
|
) = self.datums_range()
|
||||||
|
|
||||||
|
profiler(f'{self.name} got bars range')
|
||||||
|
x_range = lbar, rbar
|
||||||
|
|
||||||
|
# TODO: hash the slice instead maybe?
|
||||||
|
# https://stackoverflow.com/a/29980872
|
||||||
|
lbar, rbar = ixrng = round(x_range[0]), round(x_range[1])
|
||||||
|
|
||||||
|
if use_caching:
|
||||||
|
cached_result = self._mxmns.get(ixrng)
|
||||||
|
if cached_result:
|
||||||
|
if do_print:
|
||||||
|
print(
|
||||||
|
f'{self.name} CACHED maxmin\n'
|
||||||
|
f'{ixrng} -> {cached_result}'
|
||||||
|
)
|
||||||
|
read_slc, mxmn = cached_result
|
||||||
|
return (
|
||||||
|
ixrng,
|
||||||
|
read_slc,
|
||||||
|
mxmn,
|
||||||
|
)
|
||||||
|
|
||||||
|
if i_read_range is None:
|
||||||
|
# get relative slice indexes into array
|
||||||
|
if self.index_field == 'time':
|
||||||
|
read_slc = slice_from_time(
|
||||||
|
arr,
|
||||||
|
start_t=lbar,
|
||||||
|
stop_t=rbar,
|
||||||
|
step=self.index_step(),
|
||||||
|
)
|
||||||
|
|
||||||
|
else:
|
||||||
|
ifirst = arr[0]['index']
|
||||||
|
read_slc = slice(
|
||||||
|
lbar - ifirst,
|
||||||
|
(rbar - ifirst) + 1
|
||||||
|
)
|
||||||
|
|
||||||
slice_view = arr[read_slc]
|
slice_view = arr[read_slc]
|
||||||
|
|
||||||
if not slice_view.size:
|
if not slice_view.size:
|
||||||
log.warning(f'{self.name} no maxmin in view?')
|
log.warning(
|
||||||
|
f'{self.name} no maxmin in view?\n'
|
||||||
|
f"{name} no mxmn for bars_range => {ixrng} !?"
|
||||||
|
)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
elif self.yrange:
|
elif self.yrange:
|
||||||
|
@ -380,9 +426,8 @@ class Viz(msgspec.Struct): # , frozen=True):
|
||||||
if do_print:
|
if do_print:
|
||||||
print(
|
print(
|
||||||
f'{self.name} M4 maxmin:\n'
|
f'{self.name} M4 maxmin:\n'
|
||||||
f'{rkey} -> {mxmn}'
|
f'{ixrng} -> {mxmn}'
|
||||||
)
|
)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
if self.is_ohlc:
|
if self.is_ohlc:
|
||||||
ylow = np.min(slice_view['low'])
|
ylow = np.min(slice_view['low'])
|
||||||
|
@ -400,7 +445,7 @@ class Viz(msgspec.Struct): # , frozen=True):
|
||||||
s = 3
|
s = 3
|
||||||
print(
|
print(
|
||||||
f'{self.name} MANUAL ohlc={self.is_ohlc} maxmin:\n'
|
f'{self.name} MANUAL ohlc={self.is_ohlc} maxmin:\n'
|
||||||
f'{rkey} -> {mxmn}\n'
|
f'{ixrng} -> {mxmn}\n'
|
||||||
f'read_slc: {read_slc}\n'
|
f'read_slc: {read_slc}\n'
|
||||||
# f'abs_slc: {slice_view["index"]}\n'
|
# f'abs_slc: {slice_view["index"]}\n'
|
||||||
f'first {s}:\n{slice_view[:s]}\n'
|
f'first {s}:\n{slice_view[:s]}\n'
|
||||||
|
@ -409,9 +454,13 @@ class Viz(msgspec.Struct): # , frozen=True):
|
||||||
|
|
||||||
# cache result for input range
|
# cache result for input range
|
||||||
assert mxmn
|
assert mxmn
|
||||||
self._mxmns[rkey] = mxmn
|
self._mxmns[ixrng] = (read_slc, mxmn)
|
||||||
|
profiler(f'yrange mxmn cacheing: {x_range} -> {mxmn}')
|
||||||
return mxmn
|
return (
|
||||||
|
ixrng,
|
||||||
|
read_slc,
|
||||||
|
mxmn,
|
||||||
|
)
|
||||||
|
|
||||||
def view_range(self) -> tuple[int, int]:
|
def view_range(self) -> tuple[int, int]:
|
||||||
'''
|
'''
|
||||||
|
@ -456,13 +505,13 @@ class Viz(msgspec.Struct): # , frozen=True):
|
||||||
array = self.shm.array
|
array = self.shm.array
|
||||||
|
|
||||||
index = array[index_field]
|
index = array[index_field]
|
||||||
first = round(index[0])
|
first = floor(index[0])
|
||||||
last = round(index[-1])
|
last = ceil(index[-1])
|
||||||
|
|
||||||
# first and last datums in view determined by
|
# first and last datums in view determined by
|
||||||
# l / r view range.
|
# l / r view range.
|
||||||
leftmost = round(l)
|
leftmost = floor(l)
|
||||||
rightmost = round(r)
|
rightmost = ceil(r)
|
||||||
|
|
||||||
# invalid view state
|
# invalid view state
|
||||||
if (
|
if (
|
||||||
|
@ -609,7 +658,11 @@ class Viz(msgspec.Struct): # , frozen=True):
|
||||||
|
|
||||||
**kwargs,
|
**kwargs,
|
||||||
|
|
||||||
) -> pg.GraphicsObject:
|
) -> tuple[
|
||||||
|
bool,
|
||||||
|
tuple[int, int],
|
||||||
|
pg.GraphicsObject,
|
||||||
|
]:
|
||||||
'''
|
'''
|
||||||
Read latest datums from shm and render to (incrementally)
|
Read latest datums from shm and render to (incrementally)
|
||||||
render to graphics.
|
render to graphics.
|
||||||
|
@ -618,13 +671,17 @@ class Viz(msgspec.Struct): # , frozen=True):
|
||||||
profiler = Profiler(
|
profiler = Profiler(
|
||||||
msg=f'Viz.update_graphics() for {self.name}',
|
msg=f'Viz.update_graphics() for {self.name}',
|
||||||
disabled=not pg_profile_enabled(),
|
disabled=not pg_profile_enabled(),
|
||||||
ms_threshold=4,
|
ms_threshold=ms_slower_then,
|
||||||
# ms_threshold=ms_slower_then,
|
# ms_threshold=4,
|
||||||
)
|
)
|
||||||
# shm read and slice to view
|
# shm read and slice to view
|
||||||
read = (
|
read = (
|
||||||
xfirst, xlast, src_array,
|
xfirst,
|
||||||
ivl, ivr, in_view,
|
xlast,
|
||||||
|
src_array,
|
||||||
|
ivl,
|
||||||
|
ivr,
|
||||||
|
in_view,
|
||||||
) = self.read(profiler=profiler)
|
) = self.read(profiler=profiler)
|
||||||
|
|
||||||
profiler('read src shm data')
|
profiler('read src shm data')
|
||||||
|
@ -635,8 +692,12 @@ class Viz(msgspec.Struct): # , frozen=True):
|
||||||
not in_view.size
|
not in_view.size
|
||||||
or not render
|
or not render
|
||||||
):
|
):
|
||||||
# print('exiting early')
|
# print(f'{self.name} not in view (exiting early)')
|
||||||
return graphics
|
return (
|
||||||
|
False,
|
||||||
|
(ivl, ivr),
|
||||||
|
graphics,
|
||||||
|
)
|
||||||
|
|
||||||
should_redraw: bool = False
|
should_redraw: bool = False
|
||||||
ds_allowed: bool = True # guard for m4 activation
|
ds_allowed: bool = True # guard for m4 activation
|
||||||
|
@ -753,13 +814,18 @@ class Viz(msgspec.Struct): # , frozen=True):
|
||||||
|
|
||||||
if not out:
|
if not out:
|
||||||
log.warning(f'{self.name} failed to render!?')
|
log.warning(f'{self.name} failed to render!?')
|
||||||
return graphics
|
return (
|
||||||
|
False,
|
||||||
|
(ivl, ivr),
|
||||||
|
graphics,
|
||||||
|
)
|
||||||
|
|
||||||
path, reset_cache = out
|
path, reset_cache = out
|
||||||
|
|
||||||
# XXX: SUPER UGGGHHH... without this we get stale cache
|
# XXX: SUPER UGGGHHH... without this we get stale cache
|
||||||
# graphics that "smear" across the view horizontally
|
# graphics that "smear" across the view horizontally
|
||||||
# when panning and the first datum is out of view..
|
# when panning and the first datum is out of view..
|
||||||
|
reset_cache = False
|
||||||
if (
|
if (
|
||||||
reset_cache
|
reset_cache
|
||||||
):
|
):
|
||||||
|
@ -768,36 +834,53 @@ class Viz(msgspec.Struct): # , frozen=True):
|
||||||
with graphics.reset_cache():
|
with graphics.reset_cache():
|
||||||
graphics.path = r.path
|
graphics.path = r.path
|
||||||
graphics.fast_path = r.fast_path
|
graphics.fast_path = r.fast_path
|
||||||
|
|
||||||
|
self.draw_last(
|
||||||
|
array_key=array_key,
|
||||||
|
last_read=read,
|
||||||
|
reset_cache=reset_cache,
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
# assign output paths to graphicis obj
|
# assign output paths to graphicis obj
|
||||||
graphics.path = r.path
|
graphics.path = r.path
|
||||||
graphics.fast_path = r.fast_path
|
graphics.fast_path = r.fast_path
|
||||||
|
|
||||||
graphics.draw_last_datum(
|
self.draw_last(
|
||||||
path,
|
array_key=array_key,
|
||||||
src_array,
|
last_read=read,
|
||||||
reset_cache,
|
reset_cache=reset_cache,
|
||||||
array_key,
|
)
|
||||||
index_field=self.index_field,
|
# graphics.draw_last_datum(
|
||||||
)
|
# path,
|
||||||
graphics.update()
|
# src_array,
|
||||||
profiler('.update()')
|
# reset_cache,
|
||||||
|
# array_key,
|
||||||
|
# index_field=self.index_field,
|
||||||
|
# )
|
||||||
# TODO: does this actuallly help us in any way (prolly should
|
# TODO: does this actuallly help us in any way (prolly should
|
||||||
# look at the source / ask ogi). I think it avoid artifacts on
|
# look at the source / ask ogi). I think it avoid artifacts on
|
||||||
# wheel-scroll downsampling curve updates?
|
# wheel-scroll downsampling curve updates?
|
||||||
# TODO: is this ever better?
|
# TODO: is this ever better?
|
||||||
# graphics.prepareGeometryChange()
|
graphics.prepareGeometryChange()
|
||||||
# profiler('.prepareGeometryChange()')
|
profiler('.prepareGeometryChange()')
|
||||||
|
|
||||||
|
graphics.update()
|
||||||
|
profiler('.update()')
|
||||||
|
|
||||||
# track downsampled state
|
# track downsampled state
|
||||||
self._in_ds = r._in_ds
|
self._in_ds = r._in_ds
|
||||||
|
|
||||||
return graphics
|
return (
|
||||||
|
True,
|
||||||
|
(ivl, ivr),
|
||||||
|
graphics,
|
||||||
|
)
|
||||||
|
|
||||||
def draw_last(
|
def draw_last(
|
||||||
self,
|
self,
|
||||||
array_key: Optional[str] = None,
|
array_key: str | None = None,
|
||||||
|
last_read: tuple | None = None,
|
||||||
|
reset_cache: bool = False,
|
||||||
only_last_uppx: bool = False,
|
only_last_uppx: bool = False,
|
||||||
|
|
||||||
) -> None:
|
) -> None:
|
||||||
|
@ -806,17 +889,11 @@ class Viz(msgspec.Struct): # , frozen=True):
|
||||||
(
|
(
|
||||||
xfirst, xlast, src_array,
|
xfirst, xlast, src_array,
|
||||||
ivl, ivr, in_view,
|
ivl, ivr, in_view,
|
||||||
) = self.read()
|
) = last_read or self.read()
|
||||||
|
|
||||||
g = self.graphics
|
|
||||||
array_key = array_key or self.name
|
array_key = array_key or self.name
|
||||||
x, y = g.draw_last_datum(
|
|
||||||
g.path,
|
gfx = self.graphics
|
||||||
src_array,
|
|
||||||
False, # never reset path
|
|
||||||
array_key,
|
|
||||||
self.index_field,
|
|
||||||
)
|
|
||||||
|
|
||||||
# the renderer is downsampling we choose
|
# the renderer is downsampling we choose
|
||||||
# to always try and update a single (interpolating)
|
# to always try and update a single (interpolating)
|
||||||
|
@ -826,36 +903,55 @@ class Viz(msgspec.Struct): # , frozen=True):
|
||||||
# worth of data since that's all the screen
|
# worth of data since that's all the screen
|
||||||
# can represent on the last column where
|
# can represent on the last column where
|
||||||
# the most recent datum is being drawn.
|
# the most recent datum is being drawn.
|
||||||
if (
|
uppx = ceil(gfx.x_uppx())
|
||||||
self._in_ds
|
|
||||||
or only_last_uppx
|
|
||||||
):
|
|
||||||
dsg = self.ds_graphics or self.graphics
|
|
||||||
|
|
||||||
# XXX: pretty sure we don't need this?
|
if (
|
||||||
# if isinstance(g, Curve):
|
(self._in_ds or only_last_uppx)
|
||||||
# with dsg.reset_cache():
|
and uppx > 0
|
||||||
uppx = round(self._last_uppx)
|
):
|
||||||
y = y[-uppx:]
|
alt_renderer = self._alt_r
|
||||||
|
if alt_renderer:
|
||||||
|
renderer, gfx = alt_renderer
|
||||||
|
else:
|
||||||
|
renderer = self._src_r
|
||||||
|
|
||||||
|
fmtr = renderer.fmtr
|
||||||
|
x = fmtr.x_1d
|
||||||
|
y = fmtr.y_1d
|
||||||
|
|
||||||
|
iuppx = ceil(uppx)
|
||||||
|
if alt_renderer:
|
||||||
|
iuppx = ceil(uppx / fmtr.flat_index_ratio)
|
||||||
|
|
||||||
|
y = y[-iuppx:]
|
||||||
ymn, ymx = y.min(), y.max()
|
ymn, ymx = y.min(), y.max()
|
||||||
# print(f'drawing uppx={uppx} mxmn line: {ymn}, {ymx}')
|
|
||||||
try:
|
try:
|
||||||
iuppx = x[-uppx]
|
x_start = x[-iuppx]
|
||||||
except IndexError:
|
except IndexError:
|
||||||
# we're less then an x-px wide so just grab the start
|
# we're less then an x-px wide so just grab the start
|
||||||
# datum index.
|
# datum index.
|
||||||
iuppx = x[0]
|
x_start = x[0]
|
||||||
|
|
||||||
dsg._last_line = QLineF(
|
gfx._last_line = QLineF(
|
||||||
iuppx, ymn,
|
x_start, ymn,
|
||||||
x[-1], ymx,
|
x[-1], ymx,
|
||||||
)
|
)
|
||||||
# print(f'updating DS curve {self.name}')
|
# print(
|
||||||
dsg.update()
|
# f'updating DS curve {self.name}@{time_step}s\n'
|
||||||
|
# f'drawing uppx={uppx} mxmn line: {ymn}, {ymx}'
|
||||||
|
# )
|
||||||
|
|
||||||
else:
|
else:
|
||||||
|
x, y = gfx.draw_last_datum(
|
||||||
|
gfx.path,
|
||||||
|
src_array,
|
||||||
|
reset_cache, # never reset path
|
||||||
|
array_key,
|
||||||
|
self.index_field,
|
||||||
|
)
|
||||||
# print(f'updating NOT DS curve {self.name}')
|
# print(f'updating NOT DS curve {self.name}')
|
||||||
g.update()
|
|
||||||
|
gfx.update()
|
||||||
|
|
||||||
def default_view(
|
def default_view(
|
||||||
self,
|
self,
|
||||||
|
@ -964,7 +1060,9 @@ class Viz(msgspec.Struct): # , frozen=True):
|
||||||
l_reset = r_reset - rl_diff
|
l_reset = r_reset - rl_diff
|
||||||
|
|
||||||
else:
|
else:
|
||||||
raise RuntimeError(f'Unknown view state {vl} -> {vr}')
|
log.warning(f'Unknown view state {vl} -> {vr}')
|
||||||
|
return
|
||||||
|
# raise RuntimeError(f'Unknown view state {vl} -> {vr}')
|
||||||
|
|
||||||
else:
|
else:
|
||||||
# maintain the l->r view distance
|
# maintain the l->r view distance
|
||||||
|
@ -981,11 +1079,9 @@ class Viz(msgspec.Struct): # , frozen=True):
|
||||||
)
|
)
|
||||||
|
|
||||||
if do_ds:
|
if do_ds:
|
||||||
|
# view.interaction_graphics_cycle()
|
||||||
view.maybe_downsample_graphics()
|
view.maybe_downsample_graphics()
|
||||||
view._set_yrange()
|
view._set_yrange(viz=self)
|
||||||
|
|
||||||
# caller should do this!
|
|
||||||
# self.linked.graphics_cycle()
|
|
||||||
|
|
||||||
def incr_info(
|
def incr_info(
|
||||||
self,
|
self,
|
||||||
|
@ -994,8 +1090,46 @@ class Viz(msgspec.Struct): # , frozen=True):
|
||||||
is_1m: bool = False,
|
is_1m: bool = False,
|
||||||
|
|
||||||
) -> tuple:
|
) -> tuple:
|
||||||
|
'''
|
||||||
|
Return a slew of graphics related data-flow metrics to do with
|
||||||
|
incrementally updating a data view.
|
||||||
|
|
||||||
_, _, _, r = self.bars_range() # most recent right datum index in-view
|
Output info includes,
|
||||||
|
----------------------
|
||||||
|
|
||||||
|
uppx: float
|
||||||
|
x-domain units-per-pixel.
|
||||||
|
|
||||||
|
liv: bool
|
||||||
|
telling if the "last datum" is in vie"last datum" is in
|
||||||
|
view.
|
||||||
|
|
||||||
|
do_px_step: bool
|
||||||
|
recent data append(s) are enough that the next physical
|
||||||
|
pixel-column should be used for drawing.
|
||||||
|
|
||||||
|
i_diff_t: float
|
||||||
|
the difference between the last globally recorded time stamp
|
||||||
|
aand the current one.
|
||||||
|
|
||||||
|
append_diff: int
|
||||||
|
diff between last recorded "append index" (the index at whic
|
||||||
|
`do_px_step` was last returned `True`) and the current index.
|
||||||
|
|
||||||
|
do_rt_update: bool
|
||||||
|
`True` only when the uppx is less then some threshold
|
||||||
|
defined by `update_uppx`.
|
||||||
|
|
||||||
|
should_tread: bool
|
||||||
|
determines the first step, globally across all callers, that
|
||||||
|
the a set of data views should be "treaded", shifted in the
|
||||||
|
x-domain such that the last datum in view is always in the
|
||||||
|
same spot in non-view/scene (aka GUI coord) terms.
|
||||||
|
|
||||||
|
|
||||||
|
'''
|
||||||
|
# get most recent right datum index in-view
|
||||||
|
l, start, datum_start, datum_stop, stop, r = self.datums_range()
|
||||||
lasts = self.shm.array[-1]
|
lasts = self.shm.array[-1]
|
||||||
i_step = lasts['index'] # last index-specific step.
|
i_step = lasts['index'] # last index-specific step.
|
||||||
i_step_t = lasts['time'] # last time step.
|
i_step_t = lasts['time'] # last time step.
|
||||||
|
@ -1044,9 +1178,9 @@ class Viz(msgspec.Struct): # , frozen=True):
|
||||||
# is such that a datum(s) update to graphics wouldn't span
|
# is such that a datum(s) update to graphics wouldn't span
|
||||||
# to a new pixel, we don't update yet.
|
# to a new pixel, we don't update yet.
|
||||||
i_last_append = varz['i_last_append']
|
i_last_append = varz['i_last_append']
|
||||||
append_diff = i_step - i_last_append
|
append_diff: int = i_step - i_last_append
|
||||||
|
|
||||||
do_px_step = append_diff >= uppx
|
do_px_step = (append_diff * self.index_step()) >= uppx
|
||||||
do_rt_update = (uppx < update_uppx)
|
do_rt_update = (uppx < update_uppx)
|
||||||
|
|
||||||
if (
|
if (
|
||||||
|
|
|
@ -25,7 +25,10 @@ from functools import partial
|
||||||
import itertools
|
import itertools
|
||||||
from math import floor
|
from math import floor
|
||||||
import time
|
import time
|
||||||
from typing import Optional, Any, Callable
|
from typing import (
|
||||||
|
Optional,
|
||||||
|
Any,
|
||||||
|
)
|
||||||
|
|
||||||
import tractor
|
import tractor
|
||||||
import trio
|
import trio
|
||||||
|
@ -43,7 +46,10 @@ from ..data.types import Struct
|
||||||
from ..data._sharedmem import (
|
from ..data._sharedmem import (
|
||||||
ShmArray,
|
ShmArray,
|
||||||
)
|
)
|
||||||
from ..data._sampling import _tick_groups
|
from ..data._sampling import (
|
||||||
|
_tick_groups,
|
||||||
|
open_sample_stream,
|
||||||
|
)
|
||||||
from ._axes import YAxisLabel
|
from ._axes import YAxisLabel
|
||||||
from ._chart import (
|
from ._chart import (
|
||||||
ChartPlotWidget,
|
ChartPlotWidget,
|
||||||
|
@ -84,11 +90,11 @@ log = get_logger(__name__)
|
||||||
# approach, likely with ``numba``:
|
# approach, likely with ``numba``:
|
||||||
# https://arxiv.org/abs/cs/0610046
|
# https://arxiv.org/abs/cs/0610046
|
||||||
# https://github.com/lemire/pythonmaxmin
|
# https://github.com/lemire/pythonmaxmin
|
||||||
def chart_maxmin(
|
def multi_maxmin(
|
||||||
chart: ChartPlotWidget,
|
i_read_range: tuple[int, int] | None,
|
||||||
fqsn: str,
|
fast_viz: Viz,
|
||||||
# ohlcv_shm: ShmArray,
|
vlm_viz: Viz | None = None,
|
||||||
vlm_chart: ChartPlotWidget | None = None,
|
profiler: Profiler = None,
|
||||||
|
|
||||||
) -> tuple[
|
) -> tuple[
|
||||||
|
|
||||||
|
@ -102,30 +108,51 @@ def chart_maxmin(
|
||||||
Compute max and min datums "in view" for range limits.
|
Compute max and min datums "in view" for range limits.
|
||||||
|
|
||||||
'''
|
'''
|
||||||
main_viz = chart.get_viz(chart.name)
|
out = fast_viz.maxmin(
|
||||||
last_bars_range = main_viz.bars_range()
|
i_read_range=i_read_range,
|
||||||
out = chart.maxmin(name=fqsn)
|
)
|
||||||
|
|
||||||
if out is None:
|
if out is None:
|
||||||
return (last_bars_range, 0, 0, 0)
|
# log.warning(f'No yrange provided for {name}!?')
|
||||||
|
return (0, 0, 0)
|
||||||
|
|
||||||
mn, mx = out
|
(
|
||||||
|
ixrng,
|
||||||
|
read_slc,
|
||||||
|
yrange,
|
||||||
|
) = out
|
||||||
|
|
||||||
mx_vlm_in_view = 0
|
if profiler:
|
||||||
|
profiler(f'fast_viz.maxmin({read_slc})')
|
||||||
|
|
||||||
|
mn, mx = yrange
|
||||||
|
|
||||||
# TODO: we need to NOT call this to avoid a manual
|
# TODO: we need to NOT call this to avoid a manual
|
||||||
# np.max/min trigger and especially on the vlm_chart
|
# np.max/min trigger and especially on the vlm_chart
|
||||||
# vizs which aren't shown.. like vlm?
|
# vizs which aren't shown.. like vlm?
|
||||||
if vlm_chart:
|
mx_vlm_in_view = 0
|
||||||
out = vlm_chart.maxmin()
|
if vlm_viz:
|
||||||
|
out = vlm_viz.maxmin(
|
||||||
|
i_read_range=i_read_range,
|
||||||
|
)
|
||||||
if out:
|
if out:
|
||||||
_, mx_vlm_in_view = out
|
(
|
||||||
|
ixrng,
|
||||||
|
read_slc,
|
||||||
|
mxmn,
|
||||||
|
) = out
|
||||||
|
mx_vlm_in_view = mxmn[1]
|
||||||
|
|
||||||
|
if profiler:
|
||||||
|
profiler(f'vlm_viz.maxmin({read_slc})')
|
||||||
|
|
||||||
return (
|
return (
|
||||||
last_bars_range,
|
|
||||||
mx,
|
mx,
|
||||||
max(mn, 0), # presuming price can't be negative?
|
|
||||||
mx_vlm_in_view,
|
# enforcing price can't be negative?
|
||||||
|
# TODO: do we even need this?
|
||||||
|
max(mn, 0),
|
||||||
|
|
||||||
|
mx_vlm_in_view, # vlm max
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -134,10 +161,10 @@ class DisplayState(Struct):
|
||||||
Chart-local real-time graphics state container.
|
Chart-local real-time graphics state container.
|
||||||
|
|
||||||
'''
|
'''
|
||||||
|
fqsn: str
|
||||||
godwidget: GodWidget
|
godwidget: GodWidget
|
||||||
quotes: dict[str, Any]
|
quotes: dict[str, Any]
|
||||||
|
|
||||||
maxmin: Callable
|
|
||||||
flume: Flume
|
flume: Flume
|
||||||
|
|
||||||
# high level chart handles and underlying ``Viz``
|
# high level chart handles and underlying ``Viz``
|
||||||
|
@ -151,6 +178,8 @@ class DisplayState(Struct):
|
||||||
last_price_sticky: YAxisLabel
|
last_price_sticky: YAxisLabel
|
||||||
hist_last_price_sticky: YAxisLabel
|
hist_last_price_sticky: YAxisLabel
|
||||||
|
|
||||||
|
vlm_viz: Viz
|
||||||
|
|
||||||
# misc state tracking
|
# misc state tracking
|
||||||
vars: dict[str, Any] = field(
|
vars: dict[str, Any] = field(
|
||||||
default_factory=lambda: {
|
default_factory=lambda: {
|
||||||
|
@ -194,9 +223,17 @@ async def increment_history_view(
|
||||||
# wakeups/ctx switches verus logic checks (as normal)
|
# wakeups/ctx switches verus logic checks (as normal)
|
||||||
# - we need increment logic that only does the view shift
|
# - we need increment logic that only does the view shift
|
||||||
# call when the uppx permits/needs it
|
# call when the uppx permits/needs it
|
||||||
async with hist_viz.flume.index_stream(int(1)) as istream:
|
async with open_sample_stream(1.) as istream:
|
||||||
async for msg in istream:
|
async for msg in istream:
|
||||||
|
|
||||||
|
profiler = Profiler(
|
||||||
|
msg=f'History chart cycle for: `{ds.fqsn}`',
|
||||||
|
delayed=True,
|
||||||
|
disabled=not pg_profile_enabled(),
|
||||||
|
ms_threshold=ms_slower_then,
|
||||||
|
# ms_threshold=4,
|
||||||
|
)
|
||||||
|
|
||||||
# l3 = ds.viz.shm.array[-3:]
|
# l3 = ds.viz.shm.array[-3:]
|
||||||
# print(
|
# print(
|
||||||
# f'fast step for {ds.flume.symbol.fqsn}:\n'
|
# f'fast step for {ds.flume.symbol.fqsn}:\n'
|
||||||
|
@ -208,7 +245,7 @@ async def increment_history_view(
|
||||||
(
|
(
|
||||||
uppx,
|
uppx,
|
||||||
liv,
|
liv,
|
||||||
do_append,
|
do_px_step,
|
||||||
i_diff_t,
|
i_diff_t,
|
||||||
append_diff,
|
append_diff,
|
||||||
do_rt_update,
|
do_rt_update,
|
||||||
|
@ -219,15 +256,22 @@ async def increment_history_view(
|
||||||
is_1m=True,
|
is_1m=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
if (
|
if do_px_step:
|
||||||
do_append
|
hist_viz.update_graphics()
|
||||||
and liv
|
profiler('`hist Viz.update_graphics()` call')
|
||||||
):
|
|
||||||
hist_viz.plot.vb._set_yrange()
|
|
||||||
|
|
||||||
# check if tread-in-place x-shift is needed
|
if liv:
|
||||||
|
hist_viz.plot.vb._set_yrange(viz=hist_viz)
|
||||||
|
profiler('hist chart yrange view')
|
||||||
|
|
||||||
|
# check if tread-in-place view x-shift is needed
|
||||||
if should_tread:
|
if should_tread:
|
||||||
|
# ensure path graphics append is shown on treads since
|
||||||
|
# the main rt loop does not call this.
|
||||||
hist_chart.increment_view(datums=append_diff)
|
hist_chart.increment_view(datums=append_diff)
|
||||||
|
profiler('hist tread view')
|
||||||
|
|
||||||
|
profiler.finish()
|
||||||
|
|
||||||
|
|
||||||
async def graphics_update_loop(
|
async def graphics_update_loop(
|
||||||
|
@ -304,19 +348,17 @@ async def graphics_update_loop(
|
||||||
)
|
)
|
||||||
|
|
||||||
vlm_chart = vlm_charts[fqsn]
|
vlm_chart = vlm_charts[fqsn]
|
||||||
maxmin = partial(
|
vlm_viz = vlm_chart._vizs.get('volume') if vlm_chart else None
|
||||||
chart_maxmin,
|
|
||||||
fast_chart,
|
|
||||||
fqsn,
|
|
||||||
vlm_chart,
|
|
||||||
)
|
|
||||||
last_bars_range: tuple[float, float]
|
|
||||||
(
|
(
|
||||||
last_bars_range,
|
|
||||||
last_mx,
|
last_mx,
|
||||||
last_mn,
|
last_mn,
|
||||||
last_mx_vlm,
|
last_mx_vlm,
|
||||||
) = maxmin()
|
) = multi_maxmin(
|
||||||
|
None,
|
||||||
|
fast_viz,
|
||||||
|
vlm_viz,
|
||||||
|
)
|
||||||
|
|
||||||
last, volume = ohlcv.array[-1][['close', 'volume']]
|
last, volume = ohlcv.array[-1][['close', 'volume']]
|
||||||
|
|
||||||
|
@ -344,9 +386,10 @@ async def graphics_update_loop(
|
||||||
last_quote_s = time.time()
|
last_quote_s = time.time()
|
||||||
|
|
||||||
dss[fqsn] = ds = linked.display_state = DisplayState(**{
|
dss[fqsn] = ds = linked.display_state = DisplayState(**{
|
||||||
|
'fqsn': fqsn,
|
||||||
'godwidget': godwidget,
|
'godwidget': godwidget,
|
||||||
'quotes': {},
|
'quotes': {},
|
||||||
'maxmin': maxmin,
|
# 'maxmin': maxmin,
|
||||||
|
|
||||||
'flume': flume,
|
'flume': flume,
|
||||||
|
|
||||||
|
@ -358,6 +401,8 @@ async def graphics_update_loop(
|
||||||
'hist_viz': hist_viz,
|
'hist_viz': hist_viz,
|
||||||
'hist_last_price_sticky': hist_last_price_sticky,
|
'hist_last_price_sticky': hist_last_price_sticky,
|
||||||
|
|
||||||
|
'vlm_viz': vlm_viz,
|
||||||
|
|
||||||
'l1': l1,
|
'l1': l1,
|
||||||
|
|
||||||
'vars': {
|
'vars': {
|
||||||
|
@ -372,7 +417,7 @@ async def graphics_update_loop(
|
||||||
})
|
})
|
||||||
|
|
||||||
if vlm_chart:
|
if vlm_chart:
|
||||||
vlm_pi = vlm_chart._vizs['volume'].plot
|
vlm_pi = vlm_viz.plot
|
||||||
vlm_sticky = vlm_pi.getAxis('right')._stickies['volume']
|
vlm_sticky = vlm_pi.getAxis('right')._stickies['volume']
|
||||||
ds.vlm_chart = vlm_chart
|
ds.vlm_chart = vlm_chart
|
||||||
ds.vlm_sticky = vlm_sticky
|
ds.vlm_sticky = vlm_sticky
|
||||||
|
@ -408,7 +453,8 @@ async def graphics_update_loop(
|
||||||
# and quote_rate >= _quote_throttle_rate * 2
|
# and quote_rate >= _quote_throttle_rate * 2
|
||||||
and quote_rate >= display_rate
|
and quote_rate >= display_rate
|
||||||
):
|
):
|
||||||
log.warning(f'High quote rate {symbol.key}: {quote_rate}')
|
pass
|
||||||
|
# log.warning(f'High quote rate {symbol.key}: {quote_rate}')
|
||||||
|
|
||||||
last_quote_s = time.time()
|
last_quote_s = time.time()
|
||||||
|
|
||||||
|
@ -452,105 +498,99 @@ def graphics_update_cycle(
|
||||||
|
|
||||||
) -> None:
|
) -> None:
|
||||||
|
|
||||||
|
profiler = Profiler(
|
||||||
|
msg=f'Graphics loop cycle for: `{ds.fqsn}`',
|
||||||
|
disabled=not pg_profile_enabled(),
|
||||||
|
ms_threshold=ms_slower_then,
|
||||||
|
delayed=True,
|
||||||
|
# ms_threshold=4,
|
||||||
|
)
|
||||||
|
|
||||||
# TODO: SPEEDing this all up..
|
# TODO: SPEEDing this all up..
|
||||||
# - optimize this whole graphics stack with ``numba`` hopefully
|
# - optimize this whole graphics stack with ``numba`` hopefully
|
||||||
# or at least a little `mypyc` B)
|
# or at least a little `mypyc` B)
|
||||||
# - pass more direct refs as input to avoid so many attr accesses?
|
# - pass more direct refs as input to avoid so many attr accesses?
|
||||||
# - use a streaming minmax algo and drop the use of the
|
# - use a streaming minmax algo and drop the use of the
|
||||||
# state-tracking ``chart_maxmin()`` routine from above?
|
# state-tracking ``multi_maxmin()`` routine from above?
|
||||||
|
|
||||||
|
fqsn = ds.fqsn
|
||||||
chart = ds.chart
|
chart = ds.chart
|
||||||
hist_chart = ds.godwidget.hist_linked.chart
|
|
||||||
|
|
||||||
flume = ds.flume
|
|
||||||
sym = flume.symbol
|
|
||||||
fqsn = sym.fqsn
|
|
||||||
main_viz = chart._vizs[fqsn]
|
|
||||||
index_field = main_viz.index_field
|
|
||||||
|
|
||||||
profiler = Profiler(
|
|
||||||
msg=f'Graphics loop cycle for: `{chart.name}`',
|
|
||||||
delayed=True,
|
|
||||||
disabled=not pg_profile_enabled(),
|
|
||||||
ms_threshold=ms_slower_then,
|
|
||||||
)
|
|
||||||
|
|
||||||
# unpack multi-referenced components
|
|
||||||
vlm_chart = ds.vlm_chart
|
vlm_chart = ds.vlm_chart
|
||||||
|
|
||||||
# rt "HFT" chart
|
varz = ds.vars
|
||||||
l1 = ds.l1
|
l1 = ds.l1
|
||||||
|
flume = ds.flume
|
||||||
ohlcv = flume.rt_shm
|
ohlcv = flume.rt_shm
|
||||||
array = ohlcv.array
|
array = ohlcv.array
|
||||||
|
|
||||||
varz = ds.vars
|
hist_viz = ds.hist_viz
|
||||||
|
main_viz = ds.viz
|
||||||
|
index_field = main_viz.index_field
|
||||||
|
|
||||||
tick_margin = varz['tick_margin']
|
tick_margin = varz['tick_margin']
|
||||||
|
|
||||||
(
|
(
|
||||||
uppx,
|
uppx,
|
||||||
liv,
|
liv,
|
||||||
do_append,
|
do_px_step,
|
||||||
i_diff_t,
|
i_diff_t,
|
||||||
append_diff,
|
append_diff,
|
||||||
do_rt_update,
|
do_rt_update,
|
||||||
should_tread,
|
should_tread,
|
||||||
) = main_viz.incr_info(ds=ds)
|
) = main_viz.incr_info(ds=ds)
|
||||||
|
profiler('`.incr_info()`')
|
||||||
|
|
||||||
# TODO: we should only run mxmn when we know
|
# TODO: we should only run mxmn when we know
|
||||||
# an update is due via ``do_append`` above.
|
# an update is due via ``do_px_step`` above.
|
||||||
(
|
|
||||||
brange,
|
|
||||||
mx_in_view,
|
|
||||||
mn_in_view,
|
|
||||||
mx_vlm_in_view,
|
|
||||||
) = ds.maxmin()
|
|
||||||
l, lbar, rbar, r = brange
|
|
||||||
mx = mx_in_view + tick_margin
|
|
||||||
mn = mn_in_view - tick_margin
|
|
||||||
profiler('`ds.maxmin()` call')
|
|
||||||
|
|
||||||
# TODO: eventually we want to separate out the dark vlm and show
|
# TODO: eventually we want to separate out the dark vlm and show
|
||||||
# them as an additional graphic.
|
# them as an additional graphic.
|
||||||
clear_types = _tick_groups['clears']
|
clear_types = _tick_groups['clears']
|
||||||
|
|
||||||
|
mx = varz['last_mx']
|
||||||
|
mn = varz['last_mn']
|
||||||
|
mx_vlm_in_view = varz['last_mx_vlm']
|
||||||
|
|
||||||
# update ohlc sampled price bars
|
# update ohlc sampled price bars
|
||||||
if (
|
if (
|
||||||
do_rt_update
|
# do_rt_update
|
||||||
or do_append
|
# or do_px_step
|
||||||
|
(liv and do_px_step)
|
||||||
or trigger_all
|
or trigger_all
|
||||||
):
|
):
|
||||||
chart.update_graphics_from_flow(
|
_, i_read_range, _ = main_viz.update_graphics()
|
||||||
fqsn,
|
profiler('`Viz.update_graphics()` call')
|
||||||
# chart.name,
|
|
||||||
# do_append=do_append,
|
|
||||||
)
|
|
||||||
main_viz.draw_last(array_key=fqsn)
|
|
||||||
|
|
||||||
hist_chart.update_graphics_from_flow(
|
|
||||||
fqsn,
|
|
||||||
# chart.name,
|
|
||||||
# do_append=do_append,
|
|
||||||
)
|
|
||||||
|
|
||||||
# don't real-time "shift" the curve to the
|
|
||||||
# left unless we get one of the following:
|
|
||||||
if (
|
|
||||||
(
|
(
|
||||||
should_tread
|
mx_in_view,
|
||||||
and do_append
|
mn_in_view,
|
||||||
and liv
|
mx_vlm_in_view,
|
||||||
|
) = multi_maxmin(
|
||||||
|
i_read_range,
|
||||||
|
main_viz,
|
||||||
|
ds.vlm_viz,
|
||||||
|
profiler,
|
||||||
)
|
)
|
||||||
or trigger_all
|
|
||||||
):
|
|
||||||
chart.increment_view(datums=append_diff)
|
|
||||||
main_viz.plot.vb._set_yrange()
|
|
||||||
|
|
||||||
# NOTE: since vlm and ohlc charts are axis linked now we don't
|
mx = mx_in_view + tick_margin
|
||||||
# need the double increment request?
|
mn = mn_in_view - tick_margin
|
||||||
# if vlm_chart:
|
profiler('{fqsdn} `multi_maxmin()` call')
|
||||||
# vlm_chart.increment_view(datums=append_diff)
|
|
||||||
|
|
||||||
profiler('view incremented')
|
# don't real-time "shift" the curve to the
|
||||||
|
# left unless we get one of the following:
|
||||||
|
if (
|
||||||
|
should_tread
|
||||||
|
or trigger_all
|
||||||
|
):
|
||||||
|
chart.increment_view(datums=append_diff)
|
||||||
|
# main_viz.plot.vb._set_yrange(viz=main_viz)
|
||||||
|
|
||||||
|
# NOTE: since vlm and ohlc charts are axis linked now we don't
|
||||||
|
# need the double increment request?
|
||||||
|
# if vlm_chart:
|
||||||
|
# vlm_chart.increment_view(datums=append_diff)
|
||||||
|
|
||||||
|
profiler('view incremented')
|
||||||
|
|
||||||
# iterate frames of ticks-by-type such that we only update graphics
|
# iterate frames of ticks-by-type such that we only update graphics
|
||||||
# using the last update per type where possible.
|
# using the last update per type where possible.
|
||||||
|
@ -589,9 +629,14 @@ def graphics_update_cycle(
|
||||||
ds.last_price_sticky.update_from_data(*end_ic)
|
ds.last_price_sticky.update_from_data(*end_ic)
|
||||||
ds.hist_last_price_sticky.update_from_data(*end_ic)
|
ds.hist_last_price_sticky.update_from_data(*end_ic)
|
||||||
|
|
||||||
if wap_in_history:
|
# update vwap overlay line
|
||||||
# update vwap overlay line
|
# if wap_in_history:
|
||||||
chart.update_graphics_from_flow('bar_wap')
|
# chart.get_viz('bar_wap').update_graphics()
|
||||||
|
|
||||||
|
# update OHLC chart last bars
|
||||||
|
# TODO: fix the only last uppx stuff....
|
||||||
|
main_viz.draw_last() # only_last_uppx=True)
|
||||||
|
hist_viz.draw_last() # only_last_uppx=True)
|
||||||
|
|
||||||
# L1 book label-line updates
|
# L1 book label-line updates
|
||||||
if typ in ('last',):
|
if typ in ('last',):
|
||||||
|
@ -628,7 +673,10 @@ def graphics_update_cycle(
|
||||||
):
|
):
|
||||||
l1.bid_label.update_fields({'level': price, 'size': size})
|
l1.bid_label.update_fields({'level': price, 'size': size})
|
||||||
|
|
||||||
# check for y-autorange re-size
|
profiler('L1 labels updates')
|
||||||
|
|
||||||
|
# Y-autoranging: adjust y-axis limits based on state tracking
|
||||||
|
# of previous "last" L1 values which are in view.
|
||||||
lmx = varz['last_mx']
|
lmx = varz['last_mx']
|
||||||
lmn = varz['last_mn']
|
lmn = varz['last_mn']
|
||||||
mx_diff = mx - lmx
|
mx_diff = mx - lmx
|
||||||
|
@ -638,6 +686,8 @@ def graphics_update_cycle(
|
||||||
mx_diff
|
mx_diff
|
||||||
or mn_diff
|
or mn_diff
|
||||||
):
|
):
|
||||||
|
# complain about out-of-range outliers which can show up
|
||||||
|
# in certain annoying feeds (like ib)..
|
||||||
if (
|
if (
|
||||||
abs(mx_diff) > .25 * lmx
|
abs(mx_diff) > .25 * lmx
|
||||||
or
|
or
|
||||||
|
@ -652,19 +702,21 @@ def graphics_update_cycle(
|
||||||
f'mx_diff: {mx_diff}\n'
|
f'mx_diff: {mx_diff}\n'
|
||||||
f'mn_diff: {mn_diff}\n'
|
f'mn_diff: {mn_diff}\n'
|
||||||
)
|
)
|
||||||
# fast chart resize case
|
|
||||||
|
# FAST CHART resize case
|
||||||
elif (
|
elif (
|
||||||
liv
|
liv
|
||||||
and not chart._static_yrange == 'axis'
|
and not chart._static_yrange == 'axis'
|
||||||
):
|
):
|
||||||
main_vb = chart._vizs[fqsn].plot.vb
|
main_vb = main_viz.plot.vb
|
||||||
|
|
||||||
if (
|
if (
|
||||||
main_vb._ic is None
|
main_vb._ic is None
|
||||||
or not main_vb._ic.is_set()
|
or not main_vb._ic.is_set()
|
||||||
):
|
):
|
||||||
yr = (mn, mx)
|
yr = (mn, mx)
|
||||||
# print(
|
# print(
|
||||||
# f'updating y-range due to mxmn\n'
|
# f'MAIN VIZ yrange update\n'
|
||||||
# f'{fqsn}: {yr}'
|
# f'{fqsn}: {yr}'
|
||||||
# )
|
# )
|
||||||
|
|
||||||
|
@ -677,9 +729,9 @@ def graphics_update_cycle(
|
||||||
# range_margin=0.1,
|
# range_margin=0.1,
|
||||||
yrange=yr
|
yrange=yr
|
||||||
)
|
)
|
||||||
|
profiler('main vb y-autorange')
|
||||||
|
|
||||||
# check if slow chart needs a resize
|
# SLOW CHART resize case
|
||||||
hist_viz = hist_chart._vizs[fqsn]
|
|
||||||
(
|
(
|
||||||
_,
|
_,
|
||||||
hist_liv,
|
hist_liv,
|
||||||
|
@ -692,48 +744,74 @@ def graphics_update_cycle(
|
||||||
ds=ds,
|
ds=ds,
|
||||||
is_1m=True,
|
is_1m=True,
|
||||||
)
|
)
|
||||||
if hist_liv:
|
profiler('hist `Viz.incr_info()`')
|
||||||
hist_viz.plot.vb._set_yrange()
|
|
||||||
|
|
||||||
# XXX: update this every draw cycle to make
|
# TODO: track local liv maxmin without doing a recompute all the
|
||||||
|
# time..plut, just generally the user is more likely to be
|
||||||
|
# zoomed out enough on the slow chart that this is never an
|
||||||
|
# issue (the last datum going out of y-range).
|
||||||
|
# hist_chart = ds.hist_chart
|
||||||
|
# if (
|
||||||
|
# hist_liv
|
||||||
|
# and not hist_chart._static_yrange == 'axis'
|
||||||
|
# ):
|
||||||
|
# hist_viz.plot.vb._set_yrange(
|
||||||
|
# viz=hist_viz,
|
||||||
|
# # yrange=yr, # this is the rt range, not hist.. XD
|
||||||
|
# )
|
||||||
|
# profiler('hist vb y-autorange')
|
||||||
|
|
||||||
|
# XXX: update this every draw cycle to ensure y-axis auto-ranging
|
||||||
|
# only adjusts when the in-view data co-domain actually expands or
|
||||||
|
# contracts.
|
||||||
varz['last_mx'], varz['last_mn'] = mx, mn
|
varz['last_mx'], varz['last_mn'] = mx, mn
|
||||||
|
|
||||||
# run synchronous update on all linked viz
|
# TODO: a similar, only-update-full-path-on-px-step approach for all
|
||||||
# TODO: should the "main" (aka source) viz be special?
|
# fsp overlays and vlm stuff..
|
||||||
|
|
||||||
|
# run synchronous update on all `Viz` overlays
|
||||||
for curve_name, viz in chart._vizs.items():
|
for curve_name, viz in chart._vizs.items():
|
||||||
|
|
||||||
# update any overlayed fsp flows
|
# update any overlayed fsp flows
|
||||||
if (
|
if (
|
||||||
# curve_name != chart.data_key
|
|
||||||
curve_name != fqsn
|
curve_name != fqsn
|
||||||
and not viz.is_ohlc
|
and not viz.is_ohlc
|
||||||
):
|
):
|
||||||
update_fsp_chart(
|
update_fsp_chart(
|
||||||
chart,
|
|
||||||
viz,
|
viz,
|
||||||
curve_name,
|
curve_name,
|
||||||
array_key=curve_name,
|
array_key=curve_name,
|
||||||
)
|
)
|
||||||
|
|
||||||
# even if we're downsampled bigly
|
# even if we're downsampled bigly
|
||||||
# draw the last datum in the final
|
# draw the last datum in the final
|
||||||
# px column to give the user the mx/mn
|
# px column to give the user the mx/mn
|
||||||
# range of that set.
|
# range of that set.
|
||||||
if (
|
if (
|
||||||
liv
|
curve_name != fqsn
|
||||||
# and not do_append
|
and liv
|
||||||
# and not do_rt_update
|
# and not do_px_step
|
||||||
):
|
# and not do_rt_update
|
||||||
viz.draw_last(
|
):
|
||||||
array_key=curve_name,
|
viz.draw_last(
|
||||||
only_last_uppx=True,
|
array_key=curve_name,
|
||||||
)
|
|
||||||
|
# TODO: XXX this is currently broken for the
|
||||||
|
# `FlattenedOHLC` case since we aren't returning the
|
||||||
|
# full x/y uppx's worth of src-data from
|
||||||
|
# `draw_last_datum()` ..
|
||||||
|
only_last_uppx=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
profiler('overlays updates')
|
||||||
|
|
||||||
# volume chart logic..
|
# volume chart logic..
|
||||||
# TODO: can we unify this with the above loop?
|
# TODO: can we unify this with the above loop?
|
||||||
if vlm_chart:
|
if vlm_chart:
|
||||||
# print(f"DOING VLM {fqsn}")
|
|
||||||
vlm_vizs = vlm_chart._vizs
|
vlm_vizs = vlm_chart._vizs
|
||||||
|
|
||||||
|
main_vlm_viz = vlm_vizs['volume']
|
||||||
|
|
||||||
# always update y-label
|
# always update y-label
|
||||||
ds.vlm_sticky.update_from_data(
|
ds.vlm_sticky.update_from_data(
|
||||||
*array[-1][[
|
*array[-1][[
|
||||||
|
@ -745,19 +823,20 @@ def graphics_update_cycle(
|
||||||
if (
|
if (
|
||||||
(
|
(
|
||||||
do_rt_update
|
do_rt_update
|
||||||
or do_append
|
or do_px_step
|
||||||
and liv
|
and liv
|
||||||
)
|
)
|
||||||
or trigger_all
|
or trigger_all
|
||||||
):
|
):
|
||||||
# TODO: make it so this doesn't have to be called
|
# TODO: make it so this doesn't have to be called
|
||||||
# once the $vlm is up?
|
# once the $vlm is up?
|
||||||
vlm_chart.update_graphics_from_flow(
|
main_vlm_viz.update_graphics(
|
||||||
'volume',
|
|
||||||
# UGGGh, see ``maxmin()`` impl in `._fsp` for
|
# UGGGh, see ``maxmin()`` impl in `._fsp` for
|
||||||
# the overlayed plotitems... we need a better
|
# the overlayed plotitems... we need a better
|
||||||
# bay to invoke a maxmin per overlay..
|
# bay to invoke a maxmin per overlay..
|
||||||
render=False,
|
render=False,
|
||||||
|
|
||||||
# XXX: ^^^^ THIS IS SUPER IMPORTANT! ^^^^
|
# XXX: ^^^^ THIS IS SUPER IMPORTANT! ^^^^
|
||||||
# without this, since we disable the
|
# without this, since we disable the
|
||||||
# 'volume' (units) chart after the $vlm starts
|
# 'volume' (units) chart after the $vlm starts
|
||||||
|
@ -766,60 +845,65 @@ def graphics_update_cycle(
|
||||||
# connected to update accompanying overlay
|
# connected to update accompanying overlay
|
||||||
# graphics..
|
# graphics..
|
||||||
)
|
)
|
||||||
profiler('`vlm_chart.update_graphics_from_flow()`')
|
profiler('`main_vlm_viz.update_graphics()`')
|
||||||
|
|
||||||
if (
|
if (
|
||||||
mx_vlm_in_view != varz['last_mx_vlm']
|
mx_vlm_in_view != varz['last_mx_vlm']
|
||||||
):
|
):
|
||||||
vlm_yr = (0, mx_vlm_in_view * 1.375)
|
|
||||||
vlm_chart.view._set_yrange(yrange=vlm_yr)
|
|
||||||
profiler('`vlm_chart.view._set_yrange()`')
|
|
||||||
# print(f'mx vlm: {last_mx_vlm} -> {mx_vlm_in_view}')
|
|
||||||
varz['last_mx_vlm'] = mx_vlm_in_view
|
varz['last_mx_vlm'] = mx_vlm_in_view
|
||||||
|
# vlm_yr = (0, mx_vlm_in_view * 1.375)
|
||||||
|
# vlm_chart.view._set_yrange(yrange=vlm_yr)
|
||||||
|
# profiler('`vlm_chart.view._set_yrange()`')
|
||||||
|
|
||||||
# update all downstream FSPs
|
# update all downstream FSPs
|
||||||
for curve_name, viz in vlm_vizs.items():
|
for curve_name, viz in vlm_vizs.items():
|
||||||
|
|
||||||
|
if curve_name == 'volume':
|
||||||
|
continue
|
||||||
|
|
||||||
if (
|
if (
|
||||||
curve_name not in {'volume', fqsn}
|
viz.render
|
||||||
and viz.render
|
|
||||||
and (
|
and (
|
||||||
liv and do_rt_update
|
liv and do_rt_update
|
||||||
or do_append
|
or do_px_step
|
||||||
)
|
)
|
||||||
# and not viz.is_ohlc
|
and curve_name not in {fqsn}
|
||||||
# and curve_name != fqsn
|
|
||||||
):
|
):
|
||||||
update_fsp_chart(
|
update_fsp_chart(
|
||||||
vlm_chart,
|
|
||||||
viz,
|
viz,
|
||||||
curve_name,
|
curve_name,
|
||||||
array_key=curve_name,
|
array_key=curve_name,
|
||||||
# do_append=uppx < update_uppx,
|
|
||||||
# do_append=do_append,
|
|
||||||
)
|
)
|
||||||
|
profiler(f'vlm `Viz[{viz.name}].update_graphics()`')
|
||||||
|
|
||||||
# is this even doing anything?
|
# is this even doing anything?
|
||||||
# (pretty sure it's the real-time
|
# (pretty sure it's the real-time
|
||||||
# resizing from last quote?)
|
# resizing from last quote?)
|
||||||
fvb = viz.plot.vb
|
# XXX: without this we get completely
|
||||||
fvb._set_yrange(
|
# mangled/empty vlm display subchart..
|
||||||
name=curve_name,
|
# fvb = viz.plot.vb
|
||||||
)
|
# fvb._set_yrange(
|
||||||
|
# viz=viz,
|
||||||
|
# )
|
||||||
|
profiler(f'vlm `Viz[{viz.name}].plot.vb._set_yrange()`')
|
||||||
|
|
||||||
|
# even if we're downsampled bigly
|
||||||
|
# draw the last datum in the final
|
||||||
|
# px column to give the user the mx/mn
|
||||||
|
# range of that set.
|
||||||
elif (
|
elif (
|
||||||
curve_name != 'volume'
|
not do_px_step
|
||||||
and not do_append
|
|
||||||
and liv
|
and liv
|
||||||
and uppx >= 1
|
and uppx >= 1
|
||||||
# even if we're downsampled bigly
|
|
||||||
# draw the last datum in the final
|
|
||||||
# px column to give the user the mx/mn
|
|
||||||
# range of that set.
|
|
||||||
):
|
):
|
||||||
# always update the last datum-element
|
# always update the last datum-element
|
||||||
# graphic for all vizs
|
# graphic for all vizs
|
||||||
# print(f'drawing last {viz.name}')
|
|
||||||
viz.draw_last(array_key=curve_name)
|
viz.draw_last(array_key=curve_name)
|
||||||
|
profiler(f'vlm `Viz[{viz.name}].draw_last()`')
|
||||||
|
|
||||||
|
profiler('vlm Viz all updates complete')
|
||||||
|
|
||||||
|
profiler.finish()
|
||||||
|
|
||||||
|
|
||||||
async def link_views_with_region(
|
async def link_views_with_region(
|
||||||
|
@ -989,32 +1073,6 @@ async def link_views_with_region(
|
||||||
# region.sigRegionChangeFinished.connect(update_pi_from_region)
|
# region.sigRegionChangeFinished.connect(update_pi_from_region)
|
||||||
|
|
||||||
|
|
||||||
# force 0 to always be in view
|
|
||||||
def multi_maxmin(
|
|
||||||
chart: ChartPlotWidget,
|
|
||||||
names: list[str],
|
|
||||||
|
|
||||||
) -> tuple[float, float]:
|
|
||||||
'''
|
|
||||||
Viz "group" maxmin loop; assumes all named vizs
|
|
||||||
are in the same co-domain and thus can be sorted
|
|
||||||
as one set.
|
|
||||||
|
|
||||||
Iterates all the named vizs and calls the chart
|
|
||||||
api to find their range values and return.
|
|
||||||
|
|
||||||
TODO: really we should probably have a more built-in API
|
|
||||||
for this?
|
|
||||||
|
|
||||||
'''
|
|
||||||
mx = 0
|
|
||||||
for name in names:
|
|
||||||
ymn, ymx = chart.maxmin(name=name)
|
|
||||||
mx = max(mx, ymx)
|
|
||||||
|
|
||||||
return 0, mx
|
|
||||||
|
|
||||||
|
|
||||||
_quote_throttle_rate: int = 60 - 6
|
_quote_throttle_rate: int = 60 - 6
|
||||||
|
|
||||||
|
|
||||||
|
@ -1061,17 +1119,28 @@ async def display_symbol_data(
|
||||||
display_rate = main_window().current_screen().refreshRate()
|
display_rate = main_window().current_screen().refreshRate()
|
||||||
_quote_throttle_rate = floor(display_rate) - 6
|
_quote_throttle_rate = floor(display_rate) - 6
|
||||||
|
|
||||||
|
# TODO: we should be able to increase this if we use some
|
||||||
|
# `mypyc` speedups elsewhere? 22ish seems to be the sweet
|
||||||
|
# spot for single-feed chart.
|
||||||
|
num_of_feeds = len(fqsns)
|
||||||
|
mx: int = 22
|
||||||
|
if num_of_feeds > 1:
|
||||||
|
# there will be more ctx switches with more than 1 feed so we
|
||||||
|
# max throttle down a bit more.
|
||||||
|
mx = 16
|
||||||
|
|
||||||
|
# limit to at least display's FPS
|
||||||
|
# avoiding needless Qt-in-guest-mode context switches
|
||||||
|
cycles_per_feed = min(
|
||||||
|
round(_quote_throttle_rate/num_of_feeds),
|
||||||
|
mx,
|
||||||
|
)
|
||||||
|
|
||||||
feed: Feed
|
feed: Feed
|
||||||
async with open_feed(
|
async with open_feed(
|
||||||
fqsns,
|
fqsns,
|
||||||
loglevel=loglevel,
|
loglevel=loglevel,
|
||||||
|
tick_throttle=cycles_per_feed,
|
||||||
# limit to at least display's FPS
|
|
||||||
# avoiding needless Qt-in-guest-mode context switches
|
|
||||||
tick_throttle=min(
|
|
||||||
round(_quote_throttle_rate/len(fqsns)),
|
|
||||||
22, # aka 6 + 16
|
|
||||||
),
|
|
||||||
|
|
||||||
) as feed:
|
) as feed:
|
||||||
|
|
||||||
|
@ -1159,10 +1228,8 @@ async def display_symbol_data(
|
||||||
|
|
||||||
# ensure the last datum graphic is generated
|
# ensure the last datum graphic is generated
|
||||||
# for zoom-interaction purposes.
|
# for zoom-interaction purposes.
|
||||||
hist_chart.get_viz(fqsn).draw_last(
|
hist_viz = hist_chart.get_viz(fqsn)
|
||||||
array_key=fqsn,
|
hist_viz.draw_last(array_key=fqsn)
|
||||||
# only_last_uppx=True,
|
|
||||||
)
|
|
||||||
pis.setdefault(fqsn, [None, None])[1] = hist_chart.plotItem
|
pis.setdefault(fqsn, [None, None])[1] = hist_chart.plotItem
|
||||||
|
|
||||||
# don't show when not focussed
|
# don't show when not focussed
|
||||||
|
@ -1176,6 +1243,7 @@ async def display_symbol_data(
|
||||||
# to avoid internal pane creation.
|
# to avoid internal pane creation.
|
||||||
sidepane=pp_pane,
|
sidepane=pp_pane,
|
||||||
)
|
)
|
||||||
|
rt_viz = rt_chart.get_viz(fqsn)
|
||||||
pis.setdefault(fqsn, [None, None])[0] = rt_chart.plotItem
|
pis.setdefault(fqsn, [None, None])[0] = rt_chart.plotItem
|
||||||
|
|
||||||
# for pause/resume on mouse interaction
|
# for pause/resume on mouse interaction
|
||||||
|
@ -1192,7 +1260,7 @@ async def display_symbol_data(
|
||||||
and has_vlm(ohlcv)
|
and has_vlm(ohlcv)
|
||||||
and vlm_chart is None
|
and vlm_chart is None
|
||||||
):
|
):
|
||||||
vlm_charts[fqsn] = await ln.start(
|
vlm_chart = vlm_charts[fqsn] = await ln.start(
|
||||||
open_vlm_displays,
|
open_vlm_displays,
|
||||||
rt_linked,
|
rt_linked,
|
||||||
flume,
|
flume,
|
||||||
|
@ -1223,6 +1291,9 @@ async def display_symbol_data(
|
||||||
# add_label=False,
|
# add_label=False,
|
||||||
# )
|
# )
|
||||||
|
|
||||||
|
godwidget.resize_all()
|
||||||
|
await trio.sleep(0)
|
||||||
|
|
||||||
for fqsn, flume in fitems[1:]:
|
for fqsn, flume in fitems[1:]:
|
||||||
# get a new color from the palette
|
# get a new color from the palette
|
||||||
bg_chart_color, bg_last_bar_color = next(palette)
|
bg_chart_color, bg_last_bar_color = next(palette)
|
||||||
|
@ -1245,7 +1316,7 @@ async def display_symbol_data(
|
||||||
# are none?
|
# are none?
|
||||||
hist_pi.hideAxis('left')
|
hist_pi.hideAxis('left')
|
||||||
|
|
||||||
viz = hist_chart.draw_curve(
|
hist_viz = hist_chart.draw_curve(
|
||||||
fqsn,
|
fqsn,
|
||||||
hist_ohlcv,
|
hist_ohlcv,
|
||||||
flume,
|
flume,
|
||||||
|
@ -1255,15 +1326,12 @@ async def display_symbol_data(
|
||||||
is_ohlc=True,
|
is_ohlc=True,
|
||||||
|
|
||||||
color=bg_chart_color,
|
color=bg_chart_color,
|
||||||
last_bar_color=bg_last_bar_color,
|
last_step_color=bg_last_bar_color,
|
||||||
)
|
)
|
||||||
|
|
||||||
# ensure the last datum graphic is generated
|
# ensure the last datum graphic is generated
|
||||||
# for zoom-interaction purposes.
|
# for zoom-interaction purposes.
|
||||||
viz.draw_last(
|
hist_viz.draw_last(array_key=fqsn)
|
||||||
array_key=fqsn,
|
|
||||||
# only_last_uppx=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
hist_pi.vb.maxmin = partial(
|
hist_pi.vb.maxmin = partial(
|
||||||
hist_chart.maxmin,
|
hist_chart.maxmin,
|
||||||
|
@ -1273,8 +1341,8 @@ async def display_symbol_data(
|
||||||
# specially store ref to shm for lookup in display loop
|
# specially store ref to shm for lookup in display loop
|
||||||
# since only a placeholder of `None` is entered in
|
# since only a placeholder of `None` is entered in
|
||||||
# ``.draw_curve()``.
|
# ``.draw_curve()``.
|
||||||
viz = hist_chart._vizs[fqsn]
|
hist_viz = hist_chart._vizs[fqsn]
|
||||||
assert viz.plot is hist_pi
|
assert hist_viz.plot is hist_pi
|
||||||
pis.setdefault(fqsn, [None, None])[1] = hist_pi
|
pis.setdefault(fqsn, [None, None])[1] = hist_pi
|
||||||
|
|
||||||
rt_pi = rt_chart.overlay_plotitem(
|
rt_pi = rt_chart.overlay_plotitem(
|
||||||
|
@ -1285,7 +1353,7 @@ async def display_symbol_data(
|
||||||
rt_pi.hideAxis('left')
|
rt_pi.hideAxis('left')
|
||||||
rt_pi.hideAxis('bottom')
|
rt_pi.hideAxis('bottom')
|
||||||
|
|
||||||
viz = rt_chart.draw_curve(
|
rt_viz = rt_chart.draw_curve(
|
||||||
fqsn,
|
fqsn,
|
||||||
ohlcv,
|
ohlcv,
|
||||||
flume,
|
flume,
|
||||||
|
@ -1295,7 +1363,7 @@ async def display_symbol_data(
|
||||||
is_ohlc=True,
|
is_ohlc=True,
|
||||||
|
|
||||||
color=bg_chart_color,
|
color=bg_chart_color,
|
||||||
last_bar_color=bg_last_bar_color,
|
last_step_color=bg_last_bar_color,
|
||||||
)
|
)
|
||||||
rt_pi.vb.maxmin = partial(
|
rt_pi.vb.maxmin = partial(
|
||||||
rt_chart.maxmin,
|
rt_chart.maxmin,
|
||||||
|
@ -1306,8 +1374,8 @@ async def display_symbol_data(
|
||||||
# specially store ref to shm for lookup in display loop
|
# specially store ref to shm for lookup in display loop
|
||||||
# since only a placeholder of `None` is entered in
|
# since only a placeholder of `None` is entered in
|
||||||
# ``.draw_curve()``.
|
# ``.draw_curve()``.
|
||||||
viz = rt_chart._vizs[fqsn]
|
rt_viz = rt_chart._vizs[fqsn]
|
||||||
assert viz.plot is rt_pi
|
assert rt_viz.plot is rt_pi
|
||||||
pis.setdefault(fqsn, [None, None])[0] = rt_pi
|
pis.setdefault(fqsn, [None, None])[0] = rt_pi
|
||||||
|
|
||||||
rt_chart.setFocus()
|
rt_chart.setFocus()
|
||||||
|
@ -1375,16 +1443,17 @@ async def display_symbol_data(
|
||||||
|
|
||||||
rt_linked.mode = mode
|
rt_linked.mode = mode
|
||||||
|
|
||||||
viz = rt_chart.get_viz(order_ctl_symbol)
|
rt_viz = rt_chart.get_viz(order_ctl_symbol)
|
||||||
viz.plot.setFocus()
|
rt_viz.plot.setFocus()
|
||||||
|
|
||||||
# default view adjuments and sidepane alignment
|
# default view adjuments and sidepane alignment
|
||||||
# as final default UX touch.
|
# as final default UX touch.
|
||||||
rt_chart.default_view()
|
rt_chart.default_view()
|
||||||
rt_chart.view.enable_auto_yrange()
|
await trio.sleep(0)
|
||||||
|
|
||||||
hist_chart.default_view()
|
hist_chart.default_view()
|
||||||
hist_chart.view.enable_auto_yrange()
|
hist_viz = hist_chart.get_viz(fqsn)
|
||||||
|
await trio.sleep(0)
|
||||||
|
|
||||||
godwidget.resize_all()
|
godwidget.resize_all()
|
||||||
|
|
||||||
|
|
|
@ -78,7 +78,6 @@ def has_vlm(ohlcv: ShmArray) -> bool:
|
||||||
|
|
||||||
|
|
||||||
def update_fsp_chart(
|
def update_fsp_chart(
|
||||||
chart: ChartPlotWidget,
|
|
||||||
viz,
|
viz,
|
||||||
graphics_name: str,
|
graphics_name: str,
|
||||||
array_key: Optional[str],
|
array_key: Optional[str],
|
||||||
|
@ -101,18 +100,14 @@ def update_fsp_chart(
|
||||||
# update graphics
|
# update graphics
|
||||||
# NOTE: this does a length check internally which allows it
|
# NOTE: this does a length check internally which allows it
|
||||||
# staying above the last row check below..
|
# staying above the last row check below..
|
||||||
chart.update_graphics_from_flow(
|
viz.update_graphics()
|
||||||
graphics_name,
|
|
||||||
array_key=array_key or graphics_name,
|
|
||||||
**kwargs,
|
|
||||||
)
|
|
||||||
|
|
||||||
# XXX: re: ``array_key``: fsp func names must be unique meaning we
|
# XXX: re: ``array_key``: fsp func names must be unique meaning we
|
||||||
# can't have duplicates of the underlying data even if multiple
|
# can't have duplicates of the underlying data even if multiple
|
||||||
# sub-charts reference it under different 'named charts'.
|
# sub-charts reference it under different 'named charts'.
|
||||||
|
|
||||||
# read from last calculated value and update any label
|
# read from last calculated value and update any label
|
||||||
last_val_sticky = chart.plotItem.getAxis(
|
last_val_sticky = viz.plot.getAxis(
|
||||||
'right')._stickies.get(graphics_name)
|
'right')._stickies.get(graphics_name)
|
||||||
if last_val_sticky:
|
if last_val_sticky:
|
||||||
last = last_row[array_key]
|
last = last_row[array_key]
|
||||||
|
@ -287,9 +282,10 @@ async def run_fsp_ui(
|
||||||
# profiler(f'fsp:{name} chart created')
|
# profiler(f'fsp:{name} chart created')
|
||||||
|
|
||||||
# first UI update, usually from shm pushed history
|
# first UI update, usually from shm pushed history
|
||||||
|
viz = chart.get_viz(array_key)
|
||||||
update_fsp_chart(
|
update_fsp_chart(
|
||||||
chart,
|
chart,
|
||||||
chart.get_viz(array_key),
|
viz,
|
||||||
name,
|
name,
|
||||||
array_key=array_key,
|
array_key=array_key,
|
||||||
)
|
)
|
||||||
|
@ -316,7 +312,7 @@ async def run_fsp_ui(
|
||||||
# level_line(chart, 70, orient_v='bottom')
|
# level_line(chart, 70, orient_v='bottom')
|
||||||
# level_line(chart, 80, orient_v='top')
|
# level_line(chart, 80, orient_v='top')
|
||||||
|
|
||||||
chart.view._set_yrange()
|
chart.view._set_yrange(viz=viz)
|
||||||
# done() # status updates
|
# done() # status updates
|
||||||
|
|
||||||
# profiler(f'fsp:{func_name} starting update loop')
|
# profiler(f'fsp:{func_name} starting update loop')
|
||||||
|
@ -670,7 +666,7 @@ async def open_vlm_displays(
|
||||||
# built-in vlm which we plot ASAP since it's
|
# built-in vlm which we plot ASAP since it's
|
||||||
# usually data provided directly with OHLC history.
|
# usually data provided directly with OHLC history.
|
||||||
shm = ohlcv
|
shm = ohlcv
|
||||||
ohlc_chart = linked.chart
|
# ohlc_chart = linked.chart
|
||||||
|
|
||||||
vlm_chart = linked.add_plot(
|
vlm_chart = linked.add_plot(
|
||||||
name='volume',
|
name='volume',
|
||||||
|
@ -688,37 +684,7 @@ async def open_vlm_displays(
|
||||||
# the curve item internals are pretty convoluted.
|
# the curve item internals are pretty convoluted.
|
||||||
style='step',
|
style='step',
|
||||||
)
|
)
|
||||||
vlm_chart.view.enable_auto_yrange()
|
vlm_viz = vlm_chart._vizs['volume']
|
||||||
|
|
||||||
# back-link the volume chart to trigger y-autoranging
|
|
||||||
# in the ohlc (parent) chart.
|
|
||||||
ohlc_chart.view.enable_auto_yrange(
|
|
||||||
src_vb=vlm_chart.view,
|
|
||||||
)
|
|
||||||
|
|
||||||
# force 0 to always be in view
|
|
||||||
def multi_maxmin(
|
|
||||||
names: list[str],
|
|
||||||
|
|
||||||
) -> tuple[float, float]:
|
|
||||||
'''
|
|
||||||
Viz "group" maxmin loop; assumes all named flows
|
|
||||||
are in the same co-domain and thus can be sorted
|
|
||||||
as one set.
|
|
||||||
|
|
||||||
Iterates all the named flows and calls the chart
|
|
||||||
api to find their range values and return.
|
|
||||||
|
|
||||||
TODO: really we should probably have a more built-in API
|
|
||||||
for this?
|
|
||||||
|
|
||||||
'''
|
|
||||||
mx = 0
|
|
||||||
for name in names:
|
|
||||||
ymn, ymx = vlm_chart.maxmin(name=name)
|
|
||||||
mx = max(mx, ymx)
|
|
||||||
|
|
||||||
return 0, mx
|
|
||||||
|
|
||||||
# TODO: fix the x-axis label issue where if you put
|
# TODO: fix the x-axis label issue where if you put
|
||||||
# the axis on the left it's totally not lined up...
|
# the axis on the left it's totally not lined up...
|
||||||
|
@ -741,12 +707,14 @@ async def open_vlm_displays(
|
||||||
|
|
||||||
last_val_sticky.update_from_data(-1, value)
|
last_val_sticky.update_from_data(-1, value)
|
||||||
|
|
||||||
vlm_curve = vlm_chart.update_graphics_from_flow(
|
_, _, vlm_curve = vlm_chart.update_graphics_from_flow(
|
||||||
'volume',
|
'volume',
|
||||||
)
|
)
|
||||||
|
|
||||||
# size view to data once at outset
|
# size view to data once at outset
|
||||||
vlm_chart.view._set_yrange()
|
vlm_chart.view._set_yrange(
|
||||||
|
viz=vlm_viz
|
||||||
|
)
|
||||||
|
|
||||||
# add axis title
|
# add axis title
|
||||||
axis = vlm_chart.getAxis('right')
|
axis = vlm_chart.getAxis('right')
|
||||||
|
@ -761,7 +729,7 @@ async def open_vlm_displays(
|
||||||
|
|
||||||
{ # fsp engine conf
|
{ # fsp engine conf
|
||||||
'func_name': 'dolla_vlm',
|
'func_name': 'dolla_vlm',
|
||||||
'zero_on_step': False,
|
'zero_on_step': True,
|
||||||
'params': {
|
'params': {
|
||||||
'price_func': {
|
'price_func': {
|
||||||
'default_value': 'chl3',
|
'default_value': 'chl3',
|
||||||
|
@ -811,7 +779,7 @@ async def open_vlm_displays(
|
||||||
dvlm_pi.hideAxis('bottom')
|
dvlm_pi.hideAxis('bottom')
|
||||||
|
|
||||||
# all to be overlayed curve names
|
# all to be overlayed curve names
|
||||||
fields = [
|
dvlm_fields = [
|
||||||
'dolla_vlm',
|
'dolla_vlm',
|
||||||
'dark_vlm',
|
'dark_vlm',
|
||||||
]
|
]
|
||||||
|
@ -824,16 +792,6 @@ async def open_vlm_displays(
|
||||||
'dark_trade_rate',
|
'dark_trade_rate',
|
||||||
]
|
]
|
||||||
|
|
||||||
group_mxmn = partial(
|
|
||||||
multi_maxmin,
|
|
||||||
# keep both regular and dark vlm in view
|
|
||||||
names=fields,
|
|
||||||
# names=fields + dvlm_rate_fields,
|
|
||||||
)
|
|
||||||
|
|
||||||
# add custom auto range handler
|
|
||||||
dvlm_pi.vb._maxmin = group_mxmn
|
|
||||||
|
|
||||||
# add dvlm (step) curves to common view
|
# add dvlm (step) curves to common view
|
||||||
def chart_curves(
|
def chart_curves(
|
||||||
names: list[str],
|
names: list[str],
|
||||||
|
@ -870,7 +828,7 @@ async def open_vlm_displays(
|
||||||
assert viz.plot is pi
|
assert viz.plot is pi
|
||||||
|
|
||||||
chart_curves(
|
chart_curves(
|
||||||
fields,
|
dvlm_fields,
|
||||||
dvlm_pi,
|
dvlm_pi,
|
||||||
dvlm_flume.rt_shm,
|
dvlm_flume.rt_shm,
|
||||||
dvlm_flume,
|
dvlm_flume,
|
||||||
|
@ -930,12 +888,6 @@ async def open_vlm_displays(
|
||||||
},
|
},
|
||||||
|
|
||||||
)
|
)
|
||||||
# add custom auto range handler
|
|
||||||
tr_pi.vb.maxmin = partial(
|
|
||||||
multi_maxmin,
|
|
||||||
# keep both regular and dark vlm in view
|
|
||||||
names=trade_rate_fields,
|
|
||||||
)
|
|
||||||
tr_pi.hideAxis('bottom')
|
tr_pi.hideAxis('bottom')
|
||||||
|
|
||||||
chart_curves(
|
chart_curves(
|
||||||
|
|
|
@ -20,8 +20,13 @@ Chart view box primitives
|
||||||
"""
|
"""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
from contextlib import asynccontextmanager
|
from contextlib import asynccontextmanager
|
||||||
|
from functools import partial
|
||||||
import time
|
import time
|
||||||
from typing import Optional, Callable
|
from typing import (
|
||||||
|
Optional,
|
||||||
|
Callable,
|
||||||
|
TYPE_CHECKING,
|
||||||
|
)
|
||||||
|
|
||||||
import pyqtgraph as pg
|
import pyqtgraph as pg
|
||||||
# from pyqtgraph.GraphicsScene import mouseEvents
|
# from pyqtgraph.GraphicsScene import mouseEvents
|
||||||
|
@ -39,6 +44,10 @@ from .._profile import pg_profile_enabled, ms_slower_then
|
||||||
from ._editors import SelectRect
|
from ._editors import SelectRect
|
||||||
from . import _event
|
from . import _event
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from ._chart import ChartPlotWidget
|
||||||
|
from ._dataviz import Viz
|
||||||
|
|
||||||
|
|
||||||
log = get_logger(__name__)
|
log = get_logger(__name__)
|
||||||
|
|
||||||
|
@ -365,7 +374,6 @@ class ChartView(ViewBox):
|
||||||
)
|
)
|
||||||
# for "known y-range style"
|
# for "known y-range style"
|
||||||
self._static_yrange = static_yrange
|
self._static_yrange = static_yrange
|
||||||
self._maxmin = None
|
|
||||||
|
|
||||||
# disable vertical scrolling
|
# disable vertical scrolling
|
||||||
self.setMouseEnabled(
|
self.setMouseEnabled(
|
||||||
|
@ -374,7 +382,7 @@ class ChartView(ViewBox):
|
||||||
)
|
)
|
||||||
|
|
||||||
self.linked = None
|
self.linked = None
|
||||||
self._chart: 'ChartPlotWidget' = None # noqa
|
self._chart: ChartPlotWidget | None = None # noqa
|
||||||
|
|
||||||
# add our selection box annotator
|
# add our selection box annotator
|
||||||
self.select_box = SelectRect(self)
|
self.select_box = SelectRect(self)
|
||||||
|
@ -385,6 +393,7 @@ class ChartView(ViewBox):
|
||||||
|
|
||||||
self.setFocusPolicy(QtCore.Qt.StrongFocus)
|
self.setFocusPolicy(QtCore.Qt.StrongFocus)
|
||||||
self._ic = None
|
self._ic = None
|
||||||
|
self._yranger: Callable | None = None
|
||||||
|
|
||||||
def start_ic(
|
def start_ic(
|
||||||
self,
|
self,
|
||||||
|
@ -445,29 +454,18 @@ class ChartView(ViewBox):
|
||||||
yield self
|
yield self
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def chart(self) -> 'ChartPlotWidget': # type: ignore # noqa
|
def chart(self) -> ChartPlotWidget: # type: ignore # noqa
|
||||||
return self._chart
|
return self._chart
|
||||||
|
|
||||||
@chart.setter
|
@chart.setter
|
||||||
def chart(self, chart: 'ChartPlotWidget') -> None: # type: ignore # noqa
|
def chart(self, chart: ChartPlotWidget) -> None: # type: ignore # noqa
|
||||||
self._chart = chart
|
self._chart = chart
|
||||||
self.select_box.chart = chart
|
self.select_box.chart = chart
|
||||||
if self._maxmin is None:
|
|
||||||
self._maxmin = chart.maxmin
|
|
||||||
|
|
||||||
@property
|
|
||||||
def maxmin(self) -> Callable:
|
|
||||||
return self._maxmin
|
|
||||||
|
|
||||||
@maxmin.setter
|
|
||||||
def maxmin(self, callback: Callable) -> None:
|
|
||||||
self._maxmin = callback
|
|
||||||
|
|
||||||
def wheelEvent(
|
def wheelEvent(
|
||||||
self,
|
self,
|
||||||
ev,
|
ev,
|
||||||
axis=None,
|
axis=None,
|
||||||
# relayed_from: ChartView = None,
|
|
||||||
):
|
):
|
||||||
'''
|
'''
|
||||||
Override "center-point" location for scrolling.
|
Override "center-point" location for scrolling.
|
||||||
|
@ -482,7 +480,6 @@ class ChartView(ViewBox):
|
||||||
if (
|
if (
|
||||||
not linked
|
not linked
|
||||||
):
|
):
|
||||||
# print(f'{self.name} not linked but relay from {relayed_from.name}')
|
|
||||||
return
|
return
|
||||||
|
|
||||||
if axis in (0, 1):
|
if axis in (0, 1):
|
||||||
|
@ -494,18 +491,19 @@ class ChartView(ViewBox):
|
||||||
chart = self.linked.chart
|
chart = self.linked.chart
|
||||||
|
|
||||||
# don't zoom more then the min points setting
|
# don't zoom more then the min points setting
|
||||||
out = l, lbar, rbar, r = chart.get_viz(chart.name).bars_range()
|
viz = chart.get_viz(chart.name)
|
||||||
# vl = r - l
|
vl, lbar, rbar, vr = viz.bars_range()
|
||||||
|
|
||||||
# if ev.delta() > 0 and vl <= _min_points_to_show:
|
# TODO: max/min zoom limits incorporating time step size.
|
||||||
# log.debug("Max zoom bruh...")
|
# rl = vr - vl
|
||||||
|
# if ev.delta() > 0 and rl <= _min_points_to_show:
|
||||||
|
# log.warning("Max zoom bruh...")
|
||||||
# return
|
# return
|
||||||
|
|
||||||
# if (
|
# if (
|
||||||
# ev.delta() < 0
|
# ev.delta() < 0
|
||||||
# and vl >= len(chart._vizs[chart.name].shm.array) + 666
|
# and rl >= len(chart._vizs[chart.name].shm.array) + 666
|
||||||
# ):
|
# ):
|
||||||
# log.debug("Min zoom bruh...")
|
# log.warning("Min zoom bruh...")
|
||||||
# return
|
# return
|
||||||
|
|
||||||
# actual scaling factor
|
# actual scaling factor
|
||||||
|
@ -536,49 +534,17 @@ class ChartView(ViewBox):
|
||||||
self.scaleBy(s, center)
|
self.scaleBy(s, center)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
|
# use right-most point of current curve graphic
|
||||||
# center = pg.Point(
|
xl = viz.graphics.x_last()
|
||||||
# fn.invertQTransform(self.childGroup.transform()).map(ev.pos())
|
|
||||||
# )
|
|
||||||
|
|
||||||
# XXX: scroll "around" the right most element in the view
|
|
||||||
# which stays "pinned" in place.
|
|
||||||
|
|
||||||
# furthest_right_coord = self.boundingRect().topRight()
|
|
||||||
|
|
||||||
# yaxis = pg.Point(
|
|
||||||
# fn.invertQTransform(
|
|
||||||
# self.childGroup.transform()
|
|
||||||
# ).map(furthest_right_coord)
|
|
||||||
# )
|
|
||||||
|
|
||||||
# This seems like the most "intuitive option, a hybrid of
|
|
||||||
# tws and tv styles
|
|
||||||
last_bar = pg.Point(int(rbar)) + 1
|
|
||||||
|
|
||||||
ryaxis = chart.getAxis('right')
|
|
||||||
r_axis_x = ryaxis.pos().x()
|
|
||||||
|
|
||||||
end_of_l1 = pg.Point(
|
|
||||||
round(
|
|
||||||
chart.cv.mapToView(
|
|
||||||
pg.Point(r_axis_x - chart._max_l1_line_len)
|
|
||||||
# QPointF(chart._max_l1_line_len, 0)
|
|
||||||
).x()
|
|
||||||
)
|
|
||||||
) # .x()
|
|
||||||
|
|
||||||
# self.state['viewRange'][0][1] = end_of_l1
|
|
||||||
# focal = pg.Point((last_bar.x() + end_of_l1)/2)
|
|
||||||
|
|
||||||
focal = min(
|
focal = min(
|
||||||
last_bar,
|
xl,
|
||||||
end_of_l1,
|
vr,
|
||||||
key=lambda p: p.x()
|
|
||||||
)
|
)
|
||||||
# focal = pg.Point(last_bar.x() + end_of_l1)
|
|
||||||
|
|
||||||
self._resetTarget()
|
self._resetTarget()
|
||||||
|
|
||||||
|
# NOTE: scroll "around" the right most datum-element in view
|
||||||
|
# gives the feeling of staying "pinned" in place.
|
||||||
self.scaleBy(s, focal)
|
self.scaleBy(s, focal)
|
||||||
|
|
||||||
# XXX: the order of the next 2 lines i'm pretty sure
|
# XXX: the order of the next 2 lines i'm pretty sure
|
||||||
|
@ -604,21 +570,8 @@ class ChartView(ViewBox):
|
||||||
self,
|
self,
|
||||||
ev,
|
ev,
|
||||||
axis: Optional[int] = None,
|
axis: Optional[int] = None,
|
||||||
# relayed_from: ChartView = None,
|
|
||||||
|
|
||||||
) -> None:
|
) -> None:
|
||||||
# if relayed_from:
|
|
||||||
# print(f'PAN: {self.name} -> RELAYED FROM: {relayed_from.name}')
|
|
||||||
|
|
||||||
# NOTE since in the overlay case axes are already
|
|
||||||
# "linked" any x-range change will already be mirrored
|
|
||||||
# in all overlaid ``PlotItems``, so we need to simply
|
|
||||||
# ignore the signal here since otherwise we get N-calls
|
|
||||||
# from N-overlays resulting in an "accelerated" feeling
|
|
||||||
# panning motion instead of the expect linear shift.
|
|
||||||
# if relayed_from:
|
|
||||||
# return
|
|
||||||
|
|
||||||
pos = ev.pos()
|
pos = ev.pos()
|
||||||
lastPos = ev.lastPos()
|
lastPos = ev.lastPos()
|
||||||
dif = pos - lastPos
|
dif = pos - lastPos
|
||||||
|
@ -688,9 +641,6 @@ class ChartView(ViewBox):
|
||||||
|
|
||||||
# PANNING MODE
|
# PANNING MODE
|
||||||
else:
|
else:
|
||||||
# XXX: WHY
|
|
||||||
ev.accept()
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self.start_ic()
|
self.start_ic()
|
||||||
except RuntimeError:
|
except RuntimeError:
|
||||||
|
@ -722,6 +672,9 @@ class ChartView(ViewBox):
|
||||||
# self._ic = None
|
# self._ic = None
|
||||||
# self.chart.resume_all_feeds()
|
# self.chart.resume_all_feeds()
|
||||||
|
|
||||||
|
# XXX: WHY
|
||||||
|
ev.accept()
|
||||||
|
|
||||||
# WEIRD "RIGHT-CLICK CENTER ZOOM" MODE
|
# WEIRD "RIGHT-CLICK CENTER ZOOM" MODE
|
||||||
elif button & QtCore.Qt.RightButton:
|
elif button & QtCore.Qt.RightButton:
|
||||||
|
|
||||||
|
@ -767,7 +720,12 @@ class ChartView(ViewBox):
|
||||||
*,
|
*,
|
||||||
|
|
||||||
yrange: Optional[tuple[float, float]] = None,
|
yrange: Optional[tuple[float, float]] = None,
|
||||||
range_margin: float = 0.06,
|
viz: Viz | None = None,
|
||||||
|
|
||||||
|
# NOTE: this value pairs (more or less) with L1 label text
|
||||||
|
# height offset from from the bid/ask lines.
|
||||||
|
range_margin: float = 0.09,
|
||||||
|
|
||||||
bars_range: Optional[tuple[int, int, int, int]] = None,
|
bars_range: Optional[tuple[int, int, int, int]] = None,
|
||||||
|
|
||||||
# flag to prevent triggering sibling charts from the same linked
|
# flag to prevent triggering sibling charts from the same linked
|
||||||
|
@ -820,18 +778,28 @@ class ChartView(ViewBox):
|
||||||
# XXX: only compute the mxmn range
|
# XXX: only compute the mxmn range
|
||||||
# if none is provided as input!
|
# if none is provided as input!
|
||||||
if not yrange:
|
if not yrange:
|
||||||
# flow = chart._vizs[name]
|
|
||||||
yrange = self._maxmin()
|
if not viz:
|
||||||
|
breakpoint()
|
||||||
|
|
||||||
|
out = viz.maxmin()
|
||||||
|
if out is None:
|
||||||
|
log.warning(f'No yrange provided for {name}!?')
|
||||||
|
return
|
||||||
|
(
|
||||||
|
ixrng,
|
||||||
|
_,
|
||||||
|
yrange
|
||||||
|
) = out
|
||||||
|
|
||||||
|
profiler(f'`{self.name}:Viz.maxmin()` -> {ixrng}=>{yrange}')
|
||||||
|
|
||||||
if yrange is None:
|
if yrange is None:
|
||||||
log.warning(f'No yrange provided for {name}!?')
|
log.warning(f'No yrange provided for {name}!?')
|
||||||
print(f"WTF NO YRANGE {name}")
|
|
||||||
return
|
return
|
||||||
|
|
||||||
ylow, yhigh = yrange
|
ylow, yhigh = yrange
|
||||||
|
|
||||||
profiler(f'callback ._maxmin(): {yrange}')
|
|
||||||
|
|
||||||
# view margins: stay within a % of the "true range"
|
# view margins: stay within a % of the "true range"
|
||||||
diff = yhigh - ylow
|
diff = yhigh - ylow
|
||||||
ylow = ylow - (diff * range_margin)
|
ylow = ylow - (diff * range_margin)
|
||||||
|
@ -851,6 +819,7 @@ class ChartView(ViewBox):
|
||||||
|
|
||||||
def enable_auto_yrange(
|
def enable_auto_yrange(
|
||||||
self,
|
self,
|
||||||
|
viz: Viz,
|
||||||
src_vb: Optional[ChartView] = None,
|
src_vb: Optional[ChartView] = None,
|
||||||
|
|
||||||
) -> None:
|
) -> None:
|
||||||
|
@ -862,8 +831,17 @@ class ChartView(ViewBox):
|
||||||
if src_vb is None:
|
if src_vb is None:
|
||||||
src_vb = self
|
src_vb = self
|
||||||
|
|
||||||
|
if self._yranger is None:
|
||||||
|
self._yranger = partial(
|
||||||
|
self._set_yrange,
|
||||||
|
viz=viz,
|
||||||
|
)
|
||||||
|
|
||||||
# widget-UIs/splitter(s) resizing
|
# widget-UIs/splitter(s) resizing
|
||||||
src_vb.sigResized.connect(self._set_yrange)
|
src_vb.sigResized.connect(self._yranger)
|
||||||
|
|
||||||
|
# mouse wheel doesn't emit XRangeChanged
|
||||||
|
src_vb.sigRangeChangedManually.connect(self._yranger)
|
||||||
|
|
||||||
# re-sampling trigger:
|
# re-sampling trigger:
|
||||||
# TODO: a smarter way to avoid calling this needlessly?
|
# TODO: a smarter way to avoid calling this needlessly?
|
||||||
|
@ -875,34 +853,21 @@ class ChartView(ViewBox):
|
||||||
src_vb.sigRangeChangedManually.connect(
|
src_vb.sigRangeChangedManually.connect(
|
||||||
self.maybe_downsample_graphics
|
self.maybe_downsample_graphics
|
||||||
)
|
)
|
||||||
# mouse wheel doesn't emit XRangeChanged
|
|
||||||
src_vb.sigRangeChangedManually.connect(self._set_yrange)
|
|
||||||
|
|
||||||
# XXX: enabling these will cause "jittery"-ness
|
|
||||||
# on zoom where sharp diffs in the y-range will
|
|
||||||
# not re-size right away until a new sample update?
|
|
||||||
# if src_vb is not self:
|
|
||||||
# src_vb.sigXRangeChanged.connect(self._set_yrange)
|
|
||||||
# src_vb.sigXRangeChanged.connect(
|
|
||||||
# self.maybe_downsample_graphics
|
|
||||||
# )
|
|
||||||
|
|
||||||
def disable_auto_yrange(self) -> None:
|
def disable_auto_yrange(self) -> None:
|
||||||
|
|
||||||
|
# XXX: not entirely sure why we can't de-reg this..
|
||||||
self.sigResized.disconnect(
|
self.sigResized.disconnect(
|
||||||
self._set_yrange,
|
self._yranger,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
self.sigRangeChangedManually.disconnect(
|
||||||
|
self._yranger,
|
||||||
|
)
|
||||||
|
|
||||||
self.sigRangeChangedManually.disconnect(
|
self.sigRangeChangedManually.disconnect(
|
||||||
self.maybe_downsample_graphics
|
self.maybe_downsample_graphics
|
||||||
)
|
)
|
||||||
self.sigRangeChangedManually.disconnect(
|
|
||||||
self._set_yrange,
|
|
||||||
)
|
|
||||||
|
|
||||||
# self.sigXRangeChanged.disconnect(self._set_yrange)
|
|
||||||
# self.sigXRangeChanged.disconnect(
|
|
||||||
# self.maybe_downsample_graphics
|
|
||||||
# )
|
|
||||||
|
|
||||||
def x_uppx(self) -> float:
|
def x_uppx(self) -> float:
|
||||||
'''
|
'''
|
||||||
|
@ -924,7 +889,7 @@ class ChartView(ViewBox):
|
||||||
|
|
||||||
def maybe_downsample_graphics(
|
def maybe_downsample_graphics(
|
||||||
self,
|
self,
|
||||||
autoscale_overlays: bool = True,
|
autoscale_overlays: bool = False,
|
||||||
):
|
):
|
||||||
profiler = Profiler(
|
profiler = Profiler(
|
||||||
msg=f'ChartView.maybe_downsample_graphics() for {self.name}',
|
msg=f'ChartView.maybe_downsample_graphics() for {self.name}',
|
||||||
|
@ -960,21 +925,19 @@ class ChartView(ViewBox):
|
||||||
|
|
||||||
# pass in no array which will read and render from the last
|
# pass in no array which will read and render from the last
|
||||||
# passed array (normally provided by the display loop.)
|
# passed array (normally provided by the display loop.)
|
||||||
chart.update_graphics_from_flow(
|
chart.update_graphics_from_flow(name)
|
||||||
name,
|
|
||||||
use_vr=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
# for each overlay on this chart auto-scale the
|
# for each overlay on this chart auto-scale the
|
||||||
# y-range to max-min values.
|
# y-range to max-min values.
|
||||||
if autoscale_overlays:
|
# if autoscale_overlays:
|
||||||
overlay = chart.pi_overlay
|
# overlay = chart.pi_overlay
|
||||||
if overlay:
|
# if overlay:
|
||||||
for pi in overlay.overlays:
|
# for pi in overlay.overlays:
|
||||||
pi.vb._set_yrange(
|
# pi.vb._set_yrange(
|
||||||
# TODO: get the range once up front...
|
# # TODO: get the range once up front...
|
||||||
# bars_range=br,
|
# # bars_range=br,
|
||||||
)
|
# viz=pi.viz,
|
||||||
profiler('autoscaled linked plots')
|
# )
|
||||||
|
# profiler('autoscaled linked plots')
|
||||||
|
|
||||||
profiler(f'<{chart_name}>.update_graphics_from_flow({name})')
|
profiler(f'<{chart_name}>.update_graphics_from_flow({name})')
|
||||||
|
|
|
@ -18,13 +18,8 @@ Super fast OHLC sampling graphics types.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
from typing import (
|
|
||||||
Optional,
|
|
||||||
TYPE_CHECKING,
|
|
||||||
)
|
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import pyqtgraph as pg
|
|
||||||
from PyQt5 import (
|
from PyQt5 import (
|
||||||
QtGui,
|
QtGui,
|
||||||
QtWidgets,
|
QtWidgets,
|
||||||
|
@ -33,18 +28,14 @@ from PyQt5.QtCore import (
|
||||||
QLineF,
|
QLineF,
|
||||||
QRectF,
|
QRectF,
|
||||||
)
|
)
|
||||||
|
from PyQt5.QtWidgets import QGraphicsItem
|
||||||
from PyQt5.QtGui import QPainterPath
|
from PyQt5.QtGui import QPainterPath
|
||||||
|
|
||||||
from ._curve import FlowGraphic
|
from ._curve import FlowGraphic
|
||||||
from .._profile import pg_profile_enabled, ms_slower_then
|
from .._profile import pg_profile_enabled, ms_slower_then
|
||||||
from ._style import hcolor
|
|
||||||
from ..log import get_logger
|
from ..log import get_logger
|
||||||
from .._profile import Profiler
|
from .._profile import Profiler
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
|
||||||
from ._chart import LinkedSplits
|
|
||||||
|
|
||||||
|
|
||||||
log = get_logger(__name__)
|
log = get_logger(__name__)
|
||||||
|
|
||||||
|
@ -100,30 +91,18 @@ class BarItems(FlowGraphic):
|
||||||
"Price range" bars graphics rendered from a OHLC sampled sequence.
|
"Price range" bars graphics rendered from a OHLC sampled sequence.
|
||||||
|
|
||||||
'''
|
'''
|
||||||
|
# XXX: causes this weird jitter bug when click-drag panning
|
||||||
|
# where the path curve will awkwardly flicker back and forth?
|
||||||
|
cache_mode: int = QGraphicsItem.NoCache
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
linked: LinkedSplits,
|
*args,
|
||||||
plotitem: 'pg.PlotItem', # noqa
|
**kwargs,
|
||||||
color: str = 'bracket',
|
|
||||||
last_bar_color: str = 'original',
|
|
||||||
|
|
||||||
name: Optional[str] = None,
|
|
||||||
|
|
||||||
) -> None:
|
) -> None:
|
||||||
super().__init__()
|
|
||||||
self.linked = linked
|
|
||||||
# XXX: for the mega-lulz increasing width here increases draw
|
|
||||||
# latency... so probably don't do it until we figure that out.
|
|
||||||
self._color = color
|
|
||||||
self.bars_pen = pg.mkPen(hcolor(color), width=1)
|
|
||||||
self.last_bar_pen = pg.mkPen(hcolor(last_bar_color), width=2)
|
|
||||||
self._name = name
|
|
||||||
|
|
||||||
# XXX: causes this weird jitter bug when click-drag panning
|
super().__init__(*args, **kwargs)
|
||||||
# where the path curve will awkwardly flicker back and forth?
|
|
||||||
# self.setCacheMode(QtWidgets.QGraphicsItem.DeviceCoordinateCache)
|
|
||||||
|
|
||||||
self.path = QPainterPath()
|
|
||||||
self._last_bar_lines: tuple[QLineF, ...] | None = None
|
self._last_bar_lines: tuple[QLineF, ...] | None = None
|
||||||
|
|
||||||
def x_last(self) -> None | float:
|
def x_last(self) -> None | float:
|
||||||
|
@ -218,12 +197,12 @@ class BarItems(FlowGraphic):
|
||||||
# as is necesarry for what's in "view". Not sure if this will
|
# as is necesarry for what's in "view". Not sure if this will
|
||||||
# lead to any perf gains other then when zoomed in to less bars
|
# lead to any perf gains other then when zoomed in to less bars
|
||||||
# in view.
|
# in view.
|
||||||
p.setPen(self.last_bar_pen)
|
p.setPen(self.last_step_pen)
|
||||||
if self._last_bar_lines:
|
if self._last_bar_lines:
|
||||||
p.drawLines(*tuple(filter(bool, self._last_bar_lines)))
|
p.drawLines(*tuple(filter(bool, self._last_bar_lines)))
|
||||||
profiler('draw last bar')
|
profiler('draw last bar')
|
||||||
|
|
||||||
p.setPen(self.bars_pen)
|
p.setPen(self._pen)
|
||||||
p.drawPath(self.path)
|
p.drawPath(self.path)
|
||||||
profiler(f'draw history path: {self.path.capacity()}')
|
profiler(f'draw history path: {self.path.capacity()}')
|
||||||
|
|
||||||
|
@ -299,5 +278,4 @@ class BarItems(FlowGraphic):
|
||||||
# date / from some previous sample. It's weird though
|
# date / from some previous sample. It's weird though
|
||||||
# because i've seen it do this to bars i - 3 back?
|
# because i've seen it do this to bars i - 3 back?
|
||||||
|
|
||||||
# return ohlc['time'], ohlc['close']
|
|
||||||
return ohlc[index_field], ohlc['close']
|
return ohlc[index_field], ohlc['close']
|
||||||
|
|
|
@ -92,11 +92,11 @@ class ComposedGridLayout:
|
||||||
'''
|
'''
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
item: PlotItem,
|
pi: PlotItem,
|
||||||
|
|
||||||
) -> None:
|
) -> None:
|
||||||
|
|
||||||
self.items: list[PlotItem] = []
|
self.pitems: list[PlotItem] = []
|
||||||
self._pi2axes: dict[ # TODO: use a ``bidict`` here?
|
self._pi2axes: dict[ # TODO: use a ``bidict`` here?
|
||||||
int,
|
int,
|
||||||
dict[str, AxisItem],
|
dict[str, AxisItem],
|
||||||
|
@ -125,7 +125,7 @@ class ComposedGridLayout:
|
||||||
|
|
||||||
layout.setOrientation(orient)
|
layout.setOrientation(orient)
|
||||||
|
|
||||||
self.insert_plotitem(0, item)
|
self.insert_plotitem(0, pi)
|
||||||
|
|
||||||
# insert surrounding linear layouts into the parent pi's layout
|
# insert surrounding linear layouts into the parent pi's layout
|
||||||
# such that additional axes can be appended arbitrarily without
|
# such that additional axes can be appended arbitrarily without
|
||||||
|
@ -135,13 +135,14 @@ class ComposedGridLayout:
|
||||||
# TODO: do we need this?
|
# TODO: do we need this?
|
||||||
# axis should have been removed during insert above
|
# axis should have been removed during insert above
|
||||||
index = _axes_layout_indices[name]
|
index = _axes_layout_indices[name]
|
||||||
axis = item.layout.itemAt(*index)
|
axis = pi.layout.itemAt(*index)
|
||||||
if axis and axis.isVisible():
|
if axis and axis.isVisible():
|
||||||
assert linlayout.itemAt(0) is axis
|
assert linlayout.itemAt(0) is axis
|
||||||
|
|
||||||
# item.layout.removeItem(axis)
|
# XXX: see comment in ``.insert_plotitem()``...
|
||||||
item.layout.addItem(linlayout, *index)
|
# pi.layout.removeItem(axis)
|
||||||
layout = item.layout.itemAt(*index)
|
pi.layout.addItem(linlayout, *index)
|
||||||
|
layout = pi.layout.itemAt(*index)
|
||||||
assert layout is linlayout
|
assert layout is linlayout
|
||||||
|
|
||||||
def _register_item(
|
def _register_item(
|
||||||
|
@ -157,14 +158,14 @@ class ComposedGridLayout:
|
||||||
self._pi2axes.setdefault(name, {})[index] = axis
|
self._pi2axes.setdefault(name, {})[index] = axis
|
||||||
|
|
||||||
# enter plot into list for index tracking
|
# enter plot into list for index tracking
|
||||||
self.items.insert(index, plotitem)
|
self.pitems.insert(index, plotitem)
|
||||||
|
|
||||||
def insert_plotitem(
|
def insert_plotitem(
|
||||||
self,
|
self,
|
||||||
index: int,
|
index: int,
|
||||||
plotitem: PlotItem,
|
plotitem: PlotItem,
|
||||||
|
|
||||||
) -> (int, int):
|
) -> tuple[int, list[AxisItem]]:
|
||||||
'''
|
'''
|
||||||
Place item at index by inserting all axes into the grid
|
Place item at index by inserting all axes into the grid
|
||||||
at list-order appropriate position.
|
at list-order appropriate position.
|
||||||
|
@ -175,11 +176,14 @@ class ComposedGridLayout:
|
||||||
'`.insert_plotitem()` only supports an index >= 0'
|
'`.insert_plotitem()` only supports an index >= 0'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
inserted_axes: list[AxisItem] = []
|
||||||
|
|
||||||
# add plot's axes in sequence to the embedded linear layouts
|
# add plot's axes in sequence to the embedded linear layouts
|
||||||
# for each "side" thus avoiding graphics collisions.
|
# for each "side" thus avoiding graphics collisions.
|
||||||
for name, axis_info in plotitem.axes.copy().items():
|
for name, axis_info in plotitem.axes.copy().items():
|
||||||
linlayout, axes = self.sides[name]
|
linlayout, axes = self.sides[name]
|
||||||
axis = axis_info['item']
|
axis = axis_info['item']
|
||||||
|
inserted_axes.append(axis)
|
||||||
|
|
||||||
if axis in axes:
|
if axis in axes:
|
||||||
# TODO: re-order using ``.pop()`` ?
|
# TODO: re-order using ``.pop()`` ?
|
||||||
|
@ -192,19 +196,20 @@ class ComposedGridLayout:
|
||||||
if (
|
if (
|
||||||
not axis.isVisible()
|
not axis.isVisible()
|
||||||
|
|
||||||
# XXX: we never skip moving the axes for the *first*
|
# XXX: we never skip moving the axes for the *root*
|
||||||
# plotitem inserted (even if not shown) since we need to
|
# plotitem inserted (even if not shown) since we need to
|
||||||
# move all the hidden axes into linear sub-layouts for
|
# move all the hidden axes into linear sub-layouts for
|
||||||
# that "central" plot in the overlay. Also if we don't
|
# that "central" plot in the overlay. Also if we don't
|
||||||
# do it there's weird geomoetry calc offsets that make
|
# do it there's weird geomoetry calc offsets that make
|
||||||
# view coords slightly off somehow .. smh
|
# view coords slightly off somehow .. smh
|
||||||
and not len(self.items) == 0
|
and not len(self.pitems) == 0
|
||||||
):
|
):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# XXX: Remove old axis? No, turns out we don't need this?
|
# XXX: Remove old axis?
|
||||||
# DON'T unlink it since we the original ``ViewBox``
|
# No, turns out we don't need this?
|
||||||
# to still drive it B)
|
# DON'T UNLINK IT since we need the original ``ViewBox`` to
|
||||||
|
# still drive it with events/handlers B)
|
||||||
# popped = plotitem.removeAxis(name, unlink=False)
|
# popped = plotitem.removeAxis(name, unlink=False)
|
||||||
# assert axis is popped
|
# assert axis is popped
|
||||||
|
|
||||||
|
@ -220,7 +225,7 @@ class ComposedGridLayout:
|
||||||
|
|
||||||
self._register_item(index, plotitem)
|
self._register_item(index, plotitem)
|
||||||
|
|
||||||
return index
|
return (index, inserted_axes)
|
||||||
|
|
||||||
def append_plotitem(
|
def append_plotitem(
|
||||||
self,
|
self,
|
||||||
|
@ -234,7 +239,7 @@ class ComposedGridLayout:
|
||||||
'''
|
'''
|
||||||
# for left and bottom axes we have to first remove
|
# for left and bottom axes we have to first remove
|
||||||
# items and re-insert to maintain a list-order.
|
# items and re-insert to maintain a list-order.
|
||||||
return self.insert_plotitem(len(self.items), item)
|
return self.insert_plotitem(len(self.pitems), item)
|
||||||
|
|
||||||
def get_axis(
|
def get_axis(
|
||||||
self,
|
self,
|
||||||
|
@ -247,7 +252,7 @@ class ComposedGridLayout:
|
||||||
if axis for that name is not shown.
|
if axis for that name is not shown.
|
||||||
|
|
||||||
'''
|
'''
|
||||||
index = self.items.index(plot)
|
index = self.pitems.index(plot)
|
||||||
named = self._pi2axes[name]
|
named = self._pi2axes[name]
|
||||||
return named.get(index)
|
return named.get(index)
|
||||||
|
|
||||||
|
@ -306,10 +311,13 @@ class PlotItemOverlay:
|
||||||
# events/signals.
|
# events/signals.
|
||||||
root_plotitem.vb.setZValue(10)
|
root_plotitem.vb.setZValue(10)
|
||||||
|
|
||||||
self.overlays: list[PlotItem] = []
|
|
||||||
self.layout = ComposedGridLayout(root_plotitem)
|
self.layout = ComposedGridLayout(root_plotitem)
|
||||||
self._relays: dict[str, Signal] = {}
|
self._relays: dict[str, Signal] = {}
|
||||||
|
|
||||||
|
@property
|
||||||
|
def overlays(self) -> list[PlotItem]:
|
||||||
|
return self.layout.pitems
|
||||||
|
|
||||||
def add_plotitem(
|
def add_plotitem(
|
||||||
self,
|
self,
|
||||||
plotitem: PlotItem,
|
plotitem: PlotItem,
|
||||||
|
@ -324,11 +332,9 @@ class PlotItemOverlay:
|
||||||
# (0, 1), # link both
|
# (0, 1), # link both
|
||||||
link_axes: tuple[int] = (),
|
link_axes: tuple[int] = (),
|
||||||
|
|
||||||
) -> None:
|
) -> tuple[int, list[AxisItem]]:
|
||||||
|
|
||||||
index = index or len(self.overlays)
|
|
||||||
root = self.root_plotitem
|
root = self.root_plotitem
|
||||||
self.overlays.insert(index, plotitem)
|
|
||||||
vb: ViewBox = plotitem.vb
|
vb: ViewBox = plotitem.vb
|
||||||
|
|
||||||
# TODO: some sane way to allow menu event broadcast XD
|
# TODO: some sane way to allow menu event broadcast XD
|
||||||
|
@ -476,7 +482,10 @@ class PlotItemOverlay:
|
||||||
# ``PlotItem`` dynamically.
|
# ``PlotItem`` dynamically.
|
||||||
|
|
||||||
# append-compose into the layout all axes from this plot
|
# append-compose into the layout all axes from this plot
|
||||||
self.layout.insert_plotitem(index, plotitem)
|
if index is None:
|
||||||
|
insert_index, axes = self.layout.append_plotitem(plotitem)
|
||||||
|
else:
|
||||||
|
insert_index, axes = self.layout.insert_plotitem(index, plotitem)
|
||||||
|
|
||||||
plotitem.setGeometry(root.vb.sceneBoundingRect())
|
plotitem.setGeometry(root.vb.sceneBoundingRect())
|
||||||
|
|
||||||
|
@ -496,6 +505,11 @@ class PlotItemOverlay:
|
||||||
|
|
||||||
vb.setZValue(100)
|
vb.setZValue(100)
|
||||||
|
|
||||||
|
return (
|
||||||
|
index,
|
||||||
|
axes,
|
||||||
|
)
|
||||||
|
|
||||||
def get_axis(
|
def get_axis(
|
||||||
self,
|
self,
|
||||||
plot: PlotItem,
|
plot: PlotItem,
|
||||||
|
|
|
@ -24,7 +24,6 @@ for fast incremental update.
|
||||||
'''
|
'''
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
from typing import (
|
from typing import (
|
||||||
Optional,
|
|
||||||
TYPE_CHECKING,
|
TYPE_CHECKING,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -58,19 +57,8 @@ class Renderer(msgspec.Struct):
|
||||||
|
|
||||||
# output graphics rendering, the main object
|
# output graphics rendering, the main object
|
||||||
# processed in ``QGraphicsObject.paint()``
|
# processed in ``QGraphicsObject.paint()``
|
||||||
path: Optional[QPainterPath] = None
|
path: QPainterPath | None = None
|
||||||
fast_path: Optional[QPainterPath] = None
|
fast_path: QPainterPath | None = None
|
||||||
|
|
||||||
# XXX: just ideas..
|
|
||||||
# called on the final data (transform) output to convert
|
|
||||||
# to "graphical data form" a format that can be passed to
|
|
||||||
# the ``.draw()`` implementation.
|
|
||||||
# graphics_t: Optional[Callable[ShmArray, np.ndarray]] = None
|
|
||||||
# graphics_t_shm: Optional[ShmArray] = None
|
|
||||||
|
|
||||||
# path graphics update implementation methods
|
|
||||||
# prepend_fn: Optional[Callable[QPainterPath, QPainterPath]] = None
|
|
||||||
# append_fn: Optional[Callable[QPainterPath, QPainterPath]] = None
|
|
||||||
|
|
||||||
# downsampling state
|
# downsampling state
|
||||||
_last_uppx: float = 0
|
_last_uppx: float = 0
|
||||||
|
@ -81,7 +69,7 @@ class Renderer(msgspec.Struct):
|
||||||
x: np.ndarray,
|
x: np.ndarray,
|
||||||
y: np.ndarray,
|
y: np.ndarray,
|
||||||
connect: str | np.ndarray = 'all',
|
connect: str | np.ndarray = 'all',
|
||||||
path: Optional[QPainterPath] = None,
|
path: QPainterPath | None = None,
|
||||||
redraw: bool = False,
|
redraw: bool = False,
|
||||||
|
|
||||||
) -> QPainterPath:
|
) -> QPainterPath:
|
||||||
|
@ -105,7 +93,7 @@ class Renderer(msgspec.Struct):
|
||||||
# - https://doc.qt.io/qt-5/qpainterpath.html#reserve
|
# - https://doc.qt.io/qt-5/qpainterpath.html#reserve
|
||||||
# - https://doc.qt.io/qt-5/qpainterpath.html#capacity
|
# - https://doc.qt.io/qt-5/qpainterpath.html#capacity
|
||||||
# - https://doc.qt.io/qt-5/qpainterpath.html#clear
|
# - https://doc.qt.io/qt-5/qpainterpath.html#clear
|
||||||
# XXX: right now this is based on had hoc checks on a
|
# XXX: right now this is based on ad-hoc checks on a
|
||||||
# hidpi 3840x2160 4k monitor but we should optimize for
|
# hidpi 3840x2160 4k monitor but we should optimize for
|
||||||
# the target display(s) on the sys.
|
# the target display(s) on the sys.
|
||||||
# if no_path_yet:
|
# if no_path_yet:
|
||||||
|
@ -218,22 +206,24 @@ class Renderer(msgspec.Struct):
|
||||||
):
|
):
|
||||||
# print(f"{self.viz.name} -> REDRAWING BRUH")
|
# print(f"{self.viz.name} -> REDRAWING BRUH")
|
||||||
if new_sample_rate and showing_src_data:
|
if new_sample_rate and showing_src_data:
|
||||||
log.info(f'DEDOWN -> {array_key}')
|
log.info(f'DE-downsampling -> {array_key}')
|
||||||
self._in_ds = False
|
self._in_ds = False
|
||||||
|
|
||||||
elif should_ds and uppx > 1:
|
elif should_ds and uppx > 1:
|
||||||
|
|
||||||
x_1d, y_1d, ymn, ymx = xy_downsample(
|
ds_out = xy_downsample(
|
||||||
x_1d,
|
x_1d,
|
||||||
y_1d,
|
y_1d,
|
||||||
uppx,
|
uppx,
|
||||||
)
|
)
|
||||||
self.viz.yrange = ymn, ymx
|
if ds_out is not None:
|
||||||
# print(f'{self.viz.name} post ds: ymn, ymx: {ymn},{ymx}')
|
x_1d, y_1d, ymn, ymx = ds_out
|
||||||
|
self.viz.yrange = ymn, ymx
|
||||||
|
# print(f'{self.viz.name} post ds: ymn, ymx: {ymn},{ymx}')
|
||||||
|
|
||||||
reset = True
|
reset = True
|
||||||
profiler(f'FULL PATH downsample redraw={should_ds}')
|
profiler(f'FULL PATH downsample redraw={should_ds}')
|
||||||
self._in_ds = True
|
self._in_ds = True
|
||||||
|
|
||||||
path = self.draw_path(
|
path = self.draw_path(
|
||||||
x=x_1d,
|
x=x_1d,
|
||||||
|
@ -269,10 +259,7 @@ class Renderer(msgspec.Struct):
|
||||||
append_length > 0
|
append_length > 0
|
||||||
and do_append
|
and do_append
|
||||||
):
|
):
|
||||||
print(f'{array_key} append len: {append_length}')
|
profiler(f'sliced append path {append_length}')
|
||||||
# new_x = x_1d[-append_length - 2:] # slice_to_head]
|
|
||||||
# new_y = y_1d[-append_length - 2:] # slice_to_head]
|
|
||||||
profiler('sliced append path')
|
|
||||||
# (
|
# (
|
||||||
# x_1d,
|
# x_1d,
|
||||||
# y_1d,
|
# y_1d,
|
||||||
|
@ -300,22 +287,23 @@ class Renderer(msgspec.Struct):
|
||||||
profiler('generated append qpath')
|
profiler('generated append qpath')
|
||||||
|
|
||||||
if use_fpath:
|
if use_fpath:
|
||||||
# print(f'{self.viz.name}: FAST PATH')
|
|
||||||
# an attempt at trying to make append-updates faster..
|
# an attempt at trying to make append-updates faster..
|
||||||
if fast_path is None:
|
if fast_path is None:
|
||||||
fast_path = append_path
|
fast_path = append_path
|
||||||
# fast_path.reserve(int(6e3))
|
# fast_path.reserve(int(6e3))
|
||||||
else:
|
else:
|
||||||
|
# print(
|
||||||
|
# f'{self.viz.name}: FAST PATH\n'
|
||||||
|
# f"append_path br: {append_path.boundingRect()}\n"
|
||||||
|
# f"path size: {size}\n"
|
||||||
|
# f"append_path len: {append_path.length()}\n"
|
||||||
|
# f"fast_path len: {fast_path.length()}\n"
|
||||||
|
# )
|
||||||
|
|
||||||
fast_path.connectPath(append_path)
|
fast_path.connectPath(append_path)
|
||||||
size = fast_path.capacity()
|
size = fast_path.capacity()
|
||||||
profiler(f'connected fast path w size: {size}')
|
profiler(f'connected fast path w size: {size}')
|
||||||
|
|
||||||
print(
|
|
||||||
f"append_path br: {append_path.boundingRect()}\n"
|
|
||||||
f"path size: {size}\n"
|
|
||||||
f"append_path len: {append_path.length()}\n"
|
|
||||||
f"fast_path len: {fast_path.length()}\n"
|
|
||||||
)
|
|
||||||
# graphics.path.moveTo(new_x[0], new_y[0])
|
# graphics.path.moveTo(new_x[0], new_y[0])
|
||||||
# path.connectPath(append_path)
|
# path.connectPath(append_path)
|
||||||
|
|
||||||
|
|
|
@ -144,15 +144,29 @@ class CompleterView(QTreeView):
|
||||||
self._font_size: int = 0 # pixels
|
self._font_size: int = 0 # pixels
|
||||||
self._init: bool = False
|
self._init: bool = False
|
||||||
|
|
||||||
async def on_pressed(self, idx: QModelIndex) -> None:
|
async def on_pressed(
|
||||||
|
self,
|
||||||
|
idx: QModelIndex,
|
||||||
|
) -> None:
|
||||||
'''
|
'''
|
||||||
Mouse pressed on view handler.
|
Mouse pressed on view handler.
|
||||||
|
|
||||||
'''
|
'''
|
||||||
search = self.parent()
|
search = self.parent()
|
||||||
await search.chart_current_item()
|
|
||||||
|
await search.chart_current_item(
|
||||||
|
clear_to_cache=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
# XXX: this causes Qt to hang and segfault..lovely
|
||||||
|
# self.show_cache_entries(
|
||||||
|
# only=True,
|
||||||
|
# keep_current_item_selected=True,
|
||||||
|
# )
|
||||||
|
|
||||||
search.focus()
|
search.focus()
|
||||||
|
|
||||||
|
|
||||||
def set_font_size(self, size: int = 18):
|
def set_font_size(self, size: int = 18):
|
||||||
# print(size)
|
# print(size)
|
||||||
if size < 0:
|
if size < 0:
|
||||||
|
@ -288,7 +302,7 @@ class CompleterView(QTreeView):
|
||||||
def select_first(self) -> QStandardItem:
|
def select_first(self) -> QStandardItem:
|
||||||
'''
|
'''
|
||||||
Select the first depth >= 2 entry from the completer tree and
|
Select the first depth >= 2 entry from the completer tree and
|
||||||
return it's item.
|
return its item.
|
||||||
|
|
||||||
'''
|
'''
|
||||||
# ensure we're **not** selecting the first level parent node and
|
# ensure we're **not** selecting the first level parent node and
|
||||||
|
@ -615,6 +629,8 @@ class SearchWidget(QtWidgets.QWidget):
|
||||||
def show_cache_entries(
|
def show_cache_entries(
|
||||||
self,
|
self,
|
||||||
only: bool = False,
|
only: bool = False,
|
||||||
|
keep_current_item_selected: bool = False,
|
||||||
|
|
||||||
) -> None:
|
) -> None:
|
||||||
'''
|
'''
|
||||||
Clear the search results view and show only cached (aka recently
|
Clear the search results view and show only cached (aka recently
|
||||||
|
@ -624,10 +640,14 @@ class SearchWidget(QtWidgets.QWidget):
|
||||||
godw = self.godwidget
|
godw = self.godwidget
|
||||||
|
|
||||||
# first entry in the cache is the current symbol(s)
|
# first entry in the cache is the current symbol(s)
|
||||||
fqsns = []
|
fqsns = set()
|
||||||
|
|
||||||
for multi_fqsns in list(godw._chart_cache):
|
for multi_fqsns in list(godw._chart_cache):
|
||||||
fqsns.extend(list(multi_fqsns))
|
for fqsn in set(multi_fqsns):
|
||||||
|
fqsns.add(fqsn)
|
||||||
|
|
||||||
|
if keep_current_item_selected:
|
||||||
|
sel = self.view.selectionModel()
|
||||||
|
cidx = sel.currentIndex()
|
||||||
|
|
||||||
self.view.set_section_entries(
|
self.view.set_section_entries(
|
||||||
'cache',
|
'cache',
|
||||||
|
@ -637,7 +657,17 @@ class SearchWidget(QtWidgets.QWidget):
|
||||||
reverse=True,
|
reverse=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
def get_current_item(self) -> Optional[tuple[str, str]]:
|
if (
|
||||||
|
keep_current_item_selected
|
||||||
|
and cidx.isValid()
|
||||||
|
):
|
||||||
|
# set current selection back to what it was before filling out
|
||||||
|
# the view results.
|
||||||
|
self.view.select_from_idx(cidx)
|
||||||
|
else:
|
||||||
|
self.view.select_first()
|
||||||
|
|
||||||
|
def get_current_item(self) -> tuple[QModelIndex, str, str] | None:
|
||||||
'''
|
'''
|
||||||
Return the current completer tree selection as
|
Return the current completer tree selection as
|
||||||
a tuple ``(parent: str, child: str)`` if valid, else ``None``.
|
a tuple ``(parent: str, child: str)`` if valid, else ``None``.
|
||||||
|
@ -665,7 +695,11 @@ class SearchWidget(QtWidgets.QWidget):
|
||||||
if provider == 'cache':
|
if provider == 'cache':
|
||||||
symbol, _, provider = symbol.rpartition('.')
|
symbol, _, provider = symbol.rpartition('.')
|
||||||
|
|
||||||
return provider, symbol
|
return (
|
||||||
|
cidx,
|
||||||
|
provider,
|
||||||
|
symbol,
|
||||||
|
)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
return None
|
return None
|
||||||
|
@ -686,7 +720,7 @@ class SearchWidget(QtWidgets.QWidget):
|
||||||
if value is None:
|
if value is None:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
provider, symbol = value
|
cidx, provider, symbol = value
|
||||||
godw = self.godwidget
|
godw = self.godwidget
|
||||||
|
|
||||||
fqsn = f'{symbol}.{provider}'
|
fqsn = f'{symbol}.{provider}'
|
||||||
|
@ -715,7 +749,9 @@ class SearchWidget(QtWidgets.QWidget):
|
||||||
godw.rt_linked,
|
godw.rt_linked,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
self.show_cache_entries(only=True)
|
self.show_cache_entries(
|
||||||
|
only=True,
|
||||||
|
)
|
||||||
|
|
||||||
self.bar.focus()
|
self.bar.focus()
|
||||||
return fqsn
|
return fqsn
|
||||||
|
@ -956,11 +992,10 @@ async def handle_keyboard_input(
|
||||||
global _search_active, _search_enabled
|
global _search_active, _search_enabled
|
||||||
|
|
||||||
# startup
|
# startup
|
||||||
bar = searchbar
|
searchw = searchbar.parent()
|
||||||
search = searchbar.parent()
|
godwidget = searchw.godwidget
|
||||||
godwidget = search.godwidget
|
view = searchbar.view
|
||||||
view = bar.view
|
view.set_font_size(searchbar.dpi_font.px_size)
|
||||||
view.set_font_size(bar.dpi_font.px_size)
|
|
||||||
send, recv = trio.open_memory_channel(616)
|
send, recv = trio.open_memory_channel(616)
|
||||||
|
|
||||||
async with trio.open_nursery() as n:
|
async with trio.open_nursery() as n:
|
||||||
|
@ -971,13 +1006,13 @@ async def handle_keyboard_input(
|
||||||
n.start_soon(
|
n.start_soon(
|
||||||
partial(
|
partial(
|
||||||
fill_results,
|
fill_results,
|
||||||
search,
|
searchw,
|
||||||
recv,
|
recv,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
bar.focus()
|
searchbar.focus()
|
||||||
search.show_cache_entries()
|
searchw.show_cache_entries()
|
||||||
await trio.sleep(0)
|
await trio.sleep(0)
|
||||||
|
|
||||||
async for kbmsg in recv_chan:
|
async for kbmsg in recv_chan:
|
||||||
|
@ -994,16 +1029,24 @@ async def handle_keyboard_input(
|
||||||
Qt.Key_Return
|
Qt.Key_Return
|
||||||
):
|
):
|
||||||
_search_enabled = False
|
_search_enabled = False
|
||||||
await search.chart_current_item(clear_to_cache=True)
|
await searchw.chart_current_item(clear_to_cache=True)
|
||||||
search.show_cache_entries(only=True)
|
|
||||||
|
# XXX: causes hang and segfault..
|
||||||
|
# searchw.show_cache_entries(
|
||||||
|
# only=True,
|
||||||
|
# keep_current_item_selected=True,
|
||||||
|
# )
|
||||||
|
|
||||||
view.show_matches()
|
view.show_matches()
|
||||||
search.focus()
|
searchw.focus()
|
||||||
|
|
||||||
elif not ctl and not bar.text():
|
|
||||||
|
|
||||||
|
elif (
|
||||||
|
not ctl
|
||||||
|
and not searchbar.text()
|
||||||
|
):
|
||||||
# TODO: really should factor this somewhere..bc
|
# TODO: really should factor this somewhere..bc
|
||||||
# we're doin it in another spot as well..
|
# we're doin it in another spot as well..
|
||||||
search.show_cache_entries(only=True)
|
searchw.show_cache_entries(only=True)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# cancel and close
|
# cancel and close
|
||||||
|
@ -1012,7 +1055,7 @@ async def handle_keyboard_input(
|
||||||
Qt.Key_Space, # i feel like this is the "native" one
|
Qt.Key_Space, # i feel like this is the "native" one
|
||||||
Qt.Key_Alt,
|
Qt.Key_Alt,
|
||||||
}:
|
}:
|
||||||
bar.unfocus()
|
searchbar.unfocus()
|
||||||
|
|
||||||
# kill the search and focus back on main chart
|
# kill the search and focus back on main chart
|
||||||
if godwidget:
|
if godwidget:
|
||||||
|
@ -1020,41 +1063,54 @@ async def handle_keyboard_input(
|
||||||
|
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if ctl and key in {
|
if (
|
||||||
Qt.Key_L,
|
ctl
|
||||||
}:
|
and key in {Qt.Key_L}
|
||||||
|
):
|
||||||
# like url (link) highlight in a web browser
|
# like url (link) highlight in a web browser
|
||||||
bar.focus()
|
searchbar.focus()
|
||||||
|
|
||||||
# selection navigation controls
|
# selection navigation controls
|
||||||
elif ctl and key in {
|
elif (
|
||||||
Qt.Key_D,
|
ctl
|
||||||
}:
|
and key in {Qt.Key_D}
|
||||||
|
):
|
||||||
view.next_section(direction='down')
|
view.next_section(direction='down')
|
||||||
_search_enabled = False
|
_search_enabled = False
|
||||||
|
|
||||||
elif ctl and key in {
|
elif (
|
||||||
Qt.Key_U,
|
ctl
|
||||||
}:
|
and key in {Qt.Key_U}
|
||||||
|
):
|
||||||
view.next_section(direction='up')
|
view.next_section(direction='up')
|
||||||
_search_enabled = False
|
_search_enabled = False
|
||||||
|
|
||||||
# selection navigation controls
|
# selection navigation controls
|
||||||
elif (ctl and key in {
|
elif (
|
||||||
|
ctl and (
|
||||||
|
key in {
|
||||||
|
Qt.Key_K,
|
||||||
|
Qt.Key_J,
|
||||||
|
}
|
||||||
|
|
||||||
Qt.Key_K,
|
or key in {
|
||||||
Qt.Key_J,
|
Qt.Key_Up,
|
||||||
|
Qt.Key_Down,
|
||||||
}) or key in {
|
}
|
||||||
|
)
|
||||||
Qt.Key_Up,
|
):
|
||||||
Qt.Key_Down,
|
|
||||||
}:
|
|
||||||
_search_enabled = False
|
_search_enabled = False
|
||||||
if key in {Qt.Key_K, Qt.Key_Up}:
|
|
||||||
|
if key in {
|
||||||
|
Qt.Key_K,
|
||||||
|
Qt.Key_Up
|
||||||
|
}:
|
||||||
item = view.select_previous()
|
item = view.select_previous()
|
||||||
|
|
||||||
elif key in {Qt.Key_J, Qt.Key_Down}:
|
elif key in {
|
||||||
|
Qt.Key_J,
|
||||||
|
Qt.Key_Down,
|
||||||
|
}:
|
||||||
item = view.select_next()
|
item = view.select_next()
|
||||||
|
|
||||||
if item:
|
if item:
|
||||||
|
@ -1063,15 +1119,18 @@ async def handle_keyboard_input(
|
||||||
# if we're in the cache section and thus the next
|
# if we're in the cache section and thus the next
|
||||||
# selection is a cache item, switch and show it
|
# selection is a cache item, switch and show it
|
||||||
# immediately since it should be very fast.
|
# immediately since it should be very fast.
|
||||||
if parent_item and parent_item.text() == 'cache':
|
if (
|
||||||
await search.chart_current_item(clear_to_cache=False)
|
parent_item
|
||||||
|
and parent_item.text() == 'cache'
|
||||||
|
):
|
||||||
|
await searchw.chart_current_item(clear_to_cache=False)
|
||||||
|
|
||||||
# ACTUAL SEARCH BLOCK #
|
# ACTUAL SEARCH BLOCK #
|
||||||
# where we fuzzy complete and fill out sections.
|
# where we fuzzy complete and fill out sections.
|
||||||
elif not ctl:
|
elif not ctl:
|
||||||
# relay to completer task
|
# relay to completer task
|
||||||
_search_enabled = True
|
_search_enabled = True
|
||||||
send.send_nowait(search.bar.text())
|
send.send_nowait(searchw.bar.text())
|
||||||
_search_active.set()
|
_search_active.set()
|
||||||
|
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue