Make `FastAppendCurve` optionally view range aware
As with the `BarItems` graphics, this makes it possible to pass in a "in view" range of array data that can be *only* rendered improving performance for large(r) data sets. All the other normal behaviour is kept (i.e a persistent, (pre/ap)pendable path can still be maintained) if a ``view_range`` is not provided. Further updates, - drop the `.should_ds_or_redraw()` and `.maybe_downsample()` predicates instead moving all that logic inside `.update_from_array()`. - disable the "cache flipping", which doesn't seem to be needed to avoid artifacts any more? - handle all redraw/dowsampling logic in `.update_from_array()`. - even more profiling. - drop path `.reserve()` stuff until we better figure out how it's supposed to work.big_data_lines
							parent
							
								
									fdd5aa33d2
								
							
						
					
					
						commit
						b6f852e0ad
					
				|  | @ -214,6 +214,9 @@ class FastAppendCurve(pg.GraphicsObject): | |||
|         vr = self.viewRect() | ||||
|         l, r = int(vr.left()), int(vr.right()) | ||||
| 
 | ||||
|         if not self._xrange: | ||||
|             return 0 | ||||
| 
 | ||||
|         start, stop = self._xrange | ||||
|         lbar = max(l, start) | ||||
|         rbar = min(r, stop) | ||||
|  | @ -222,45 +225,6 @@ class FastAppendCurve(pg.GraphicsObject): | |||
|             QLineF(lbar, 0, rbar, 0) | ||||
|         ).length() | ||||
| 
 | ||||
|     def should_ds_or_redraw( | ||||
|         self, | ||||
| 
 | ||||
|     ) -> tuple[bool, bool]: | ||||
| 
 | ||||
|         uppx = self.x_uppx() | ||||
|         px_width = self.px_width() | ||||
|         # uppx_diff = abs(uppx - self._last_uppx) | ||||
|         uppx_diff = (uppx - self._last_uppx) | ||||
|         self._last_uppx = uppx | ||||
| 
 | ||||
|         should_redraw: bool = False | ||||
|         should_ds: bool = False | ||||
| 
 | ||||
|         # print(uppx_diff) | ||||
| 
 | ||||
|         if ( | ||||
|             uppx <= 8 | ||||
|         ): | ||||
|             # trigger redraw or original non-downsampled data | ||||
|             if self._in_ds: | ||||
|                 print('REVERTING BACK TO SRC DATA') | ||||
|                 # clear downsampled curve(s) and expect | ||||
|                 # refresh of path segments. | ||||
|                 should_redraw = True | ||||
| 
 | ||||
|         elif ( | ||||
|             uppx_diff >= 4 | ||||
|             or uppx_diff <= -2 | ||||
|             or self._step_mode and abs(uppx_diff) >= 1 | ||||
|         ): | ||||
|             log.info( | ||||
|                 f'{self._name} downsampler change: {self._last_uppx} -> {uppx}' | ||||
|             ) | ||||
|             should_ds = {'px_width': px_width, 'uppx': uppx} | ||||
|             should_redraw = True | ||||
| 
 | ||||
|         return should_ds, should_redraw | ||||
| 
 | ||||
|     def downsample( | ||||
|         self, | ||||
|         x, | ||||
|  | @ -289,23 +253,19 @@ class FastAppendCurve(pg.GraphicsObject): | |||
|         self._in_ds = True | ||||
|         return x, y | ||||
| 
 | ||||
|     def maybe_downsample( | ||||
|         self, | ||||
|     ) -> None: | ||||
|         ''' | ||||
|         Simple update call but with previously cached arrays data. | ||||
| 
 | ||||
|         ''' | ||||
|         # print('DS CALLED FROM INTERACTION?') | ||||
|         # presume this is a so called "interaction update", see | ||||
|         # ``ChartView.maybe_downsample_graphics()``. | ||||
|         self.update_from_array(self._x, self._y) | ||||
| 
 | ||||
|     def update_from_array( | ||||
|         self, | ||||
| 
 | ||||
|         # full array input history | ||||
|         x: np.ndarray, | ||||
|         y: np.ndarray, | ||||
| 
 | ||||
|         # pre-sliced array data that's "in view" | ||||
|         x_iv: np.ndarray, | ||||
|         y_iv: np.ndarray, | ||||
| 
 | ||||
|         view_range: Optional[tuple[int, int]] = None, | ||||
| 
 | ||||
|     ) -> QtGui.QPainterPath: | ||||
|         ''' | ||||
|         Update curve from input 2-d data. | ||||
|  | @ -315,11 +275,11 @@ class FastAppendCurve(pg.GraphicsObject): | |||
| 
 | ||||
|         ''' | ||||
|         profiler = pg.debug.Profiler( | ||||
|             msg=f'{self._name}.update_from_array()', | ||||
|             msg=f'FastAppendCurve.update_from_array(): `{self._name}`', | ||||
|             disabled=not pg_profile_enabled(), | ||||
|             gt=ms_slower_then, | ||||
|         ) | ||||
|         flip_cache = False | ||||
|         # flip_cache = False | ||||
| 
 | ||||
|         if self._xrange: | ||||
|             istart, istop = self._xrange | ||||
|  | @ -327,40 +287,110 @@ class FastAppendCurve(pg.GraphicsObject): | |||
|             self._xrange = istart, istop = x[0], x[-1] | ||||
|         # print(f"xrange: {self._xrange}") | ||||
| 
 | ||||
|         should_ds, should_redraw = self.should_ds_or_redraw() | ||||
|         # XXX: lol brutal, the internals of `CurvePoint` (inherited by | ||||
|         # our `LineDot`) required ``.getData()`` to work.. | ||||
|         self.xData = x | ||||
|         self.yData = y | ||||
|         self._x, self._y = x, y | ||||
| 
 | ||||
|         if view_range: | ||||
|             profiler(f'view range slice {view_range}') | ||||
| 
 | ||||
|         # downsampling incremental state checking | ||||
|         uppx = self.x_uppx() | ||||
|         px_width = self.px_width() | ||||
|         uppx_diff = (uppx - self._last_uppx) | ||||
| 
 | ||||
|         should_ds = False | ||||
|         should_redraw = False | ||||
| 
 | ||||
|         # if a view range is passed, plan to draw the | ||||
|         # source ouput that's "in view" of the chart. | ||||
|         if view_range and not self._in_ds: | ||||
|             # print(f'{self._name} vr: {view_range}') | ||||
| 
 | ||||
|             # by default we only pull data up to the last (current) index | ||||
|             x_out, y_out = x_iv[:-1], y_iv[:-1] | ||||
| 
 | ||||
|             # step mode: draw flat top discrete "step" | ||||
|             # over the index space for each datum. | ||||
|             if self._step_mode: | ||||
|                 # TODO: numba this bish | ||||
|                 x_out, y_out = step_path_arrays_from_1d( | ||||
|                     x_out, | ||||
|                     y_out | ||||
|                 ) | ||||
|                 profiler('generated step arrays') | ||||
| 
 | ||||
|             should_redraw = True | ||||
|             profiler('sliced in-view array history') | ||||
| 
 | ||||
|             # x_last = x_iv[-1] | ||||
|             # y_last = y_iv[-1] | ||||
|             self._last_vr = view_range | ||||
| 
 | ||||
|             # self.disable_cache() | ||||
|             # flip_cache = True | ||||
| 
 | ||||
|         else: | ||||
|             self._xrange = x[0], x[-1] | ||||
| 
 | ||||
|         x_last = x[-1] | ||||
|         y_last = y[-1] | ||||
| 
 | ||||
|         # check for downsampling conditions | ||||
|         if ( | ||||
|             # std m4 downsample conditions | ||||
|             uppx_diff >= 2 | ||||
|             or uppx_diff <= -2 | ||||
|             or self._step_mode and abs(uppx_diff) >= 2 | ||||
| 
 | ||||
|         ): | ||||
|             log.info( | ||||
|                 f'{self._name} sampler change: {self._last_uppx} -> {uppx}' | ||||
|             ) | ||||
|             self._last_uppx = uppx | ||||
|             should_ds = {'px_width': px_width, 'uppx': uppx} | ||||
| 
 | ||||
|         elif ( | ||||
|             uppx <= 2 | ||||
|             and self._in_ds | ||||
|         ): | ||||
|             # we should de-downsample back to our original | ||||
|             # source data so we clear our path data in prep | ||||
|             # to generate a new one from original source data. | ||||
|             should_redraw = True | ||||
|             should_ds = False | ||||
| 
 | ||||
|         # compute the length diffs between the first/last index entry in | ||||
|         # the input data and the last indexes we have on record from the | ||||
|         # last time we updated the curve index. | ||||
|         prepend_length = int(istart - x[0]) | ||||
|         append_length = int(x[-1] - istop) | ||||
|         no_path_yet = self.path is None | ||||
| 
 | ||||
|         # no_path_yet = self.path is None | ||||
|         if ( | ||||
|             should_redraw or should_ds | ||||
|             or self.path is None | ||||
|             self.path is None | ||||
|             or should_redraw | ||||
|             or should_ds | ||||
|             or prepend_length > 0 | ||||
|         ): | ||||
|             # step mode: draw flat top discrete "step" | ||||
|             # over the index space for each datum. | ||||
|             if self._step_mode: | ||||
|                 x_out, y_out = step_path_arrays_from_1d( | ||||
|                     x[:-1], y[:-1] | ||||
|                 ) | ||||
|                 profiler('generated step arrays') | ||||
| 
 | ||||
|             else: | ||||
|             if ( | ||||
|                 not view_range | ||||
|                 or self._in_ds | ||||
|             ): | ||||
|                 # by default we only pull data up to the last (current) index | ||||
|                 x_out, y_out = x[:-1], y[:-1] | ||||
| 
 | ||||
|             if should_ds: | ||||
|                 x_out, y_out = self.downsample( | ||||
|                     x_out, | ||||
|                     y_out, | ||||
|                     **should_ds, | ||||
|                 ) | ||||
|                 profiler(f'path downsample redraw={should_ds}') | ||||
|                 self._in_ds = True | ||||
|                 # step mode: draw flat top discrete "step" | ||||
|                 # over the index space for each datum. | ||||
|                 if self._step_mode: | ||||
|                     x_out, y_out = step_path_arrays_from_1d( | ||||
|                         x_out, | ||||
|                         y_out, | ||||
|                     ) | ||||
|                     # TODO: numba this bish | ||||
|                     profiler('generated step arrays') | ||||
| 
 | ||||
|             if should_redraw: | ||||
|                 profiler('path reversion to non-ds') | ||||
|  | @ -371,10 +401,20 @@ class FastAppendCurve(pg.GraphicsObject): | |||
|                     self.fast_path.clear() | ||||
| 
 | ||||
|             if should_redraw and not should_ds: | ||||
|                 log.info(f'DEDOWN -> {self._name}') | ||||
|                 if self._in_ds: | ||||
|                     log.info(f'DEDOWN -> {self._name}') | ||||
| 
 | ||||
|                 self._in_ds = False | ||||
| 
 | ||||
|             # else: | ||||
|             elif should_ds: | ||||
|                 x_out, y_out = self.downsample( | ||||
|                     x_out, | ||||
|                     y_out, | ||||
|                     **should_ds, | ||||
|                 ) | ||||
|                 profiler(f'FULL PATH downsample redraw={should_ds}') | ||||
|                 self._in_ds = True | ||||
| 
 | ||||
|             self.path = pg.functions.arrayToQPath( | ||||
|                 x_out, | ||||
|                 y_out, | ||||
|  | @ -382,6 +422,9 @@ class FastAppendCurve(pg.GraphicsObject): | |||
|                 finiteCheck=False, | ||||
|                 path=self.path, | ||||
|             ) | ||||
|             profiler('generated fresh path') | ||||
|             # profiler(f'DRAW PATH IN VIEW -> {self._name}') | ||||
| 
 | ||||
|             # reserve mem allocs see: | ||||
|             # - https://doc.qt.io/qt-5/qpainterpath.html#reserve | ||||
|             # - https://doc.qt.io/qt-5/qpainterpath.html#capacity | ||||
|  | @ -389,17 +432,12 @@ class FastAppendCurve(pg.GraphicsObject): | |||
|             # XXX: right now this is based on had hoc checks on a | ||||
|             # hidpi 3840x2160 4k monitor but we should optimize for | ||||
|             # the target display(s) on the sys. | ||||
|             if no_path_yet: | ||||
|                 self.path.reserve(int(500e3)) | ||||
| 
 | ||||
|             profiler('generated fresh path') | ||||
| 
 | ||||
|             # if self._step_mode: | ||||
|             #     self.path.closeSubpath() | ||||
|             # if no_path_yet: | ||||
|             #     self.path.reserve(int(500e3)) | ||||
| 
 | ||||
|         # TODO: get this piecewise prepend working - right now it's | ||||
|         # giving heck on vwap... | ||||
|         # if prepend_length: | ||||
|         # elif prepend_length: | ||||
|         #     breakpoint() | ||||
| 
 | ||||
|         #     prepend_path = pg.functions.arrayToQPath( | ||||
|  | @ -416,11 +454,15 @@ class FastAppendCurve(pg.GraphicsObject): | |||
| 
 | ||||
|         elif ( | ||||
|             append_length > 0 | ||||
|             and not view_range | ||||
|         ): | ||||
|             new_x = x[-append_length - 2:-1] | ||||
|             new_y = y[-append_length - 2:-1] | ||||
| 
 | ||||
|             if self._step_mode: | ||||
|                 new_x, new_y = step_path_arrays_from_1d( | ||||
|                     x[-append_length - 2:-1], | ||||
|                     y[-append_length - 2:-1], | ||||
|                     new_x, | ||||
|                     new_y, | ||||
|                 ) | ||||
|                 # [1:] since we don't need the vertical line normally at | ||||
|                 # the beginning of the step curve taking the first (x, | ||||
|  | @ -429,12 +471,6 @@ class FastAppendCurve(pg.GraphicsObject): | |||
|                 new_x = new_x[1:] | ||||
|                 new_y = new_y[1:] | ||||
| 
 | ||||
|             else: | ||||
|                 # print(f"append_length: {append_length}") | ||||
|                 new_x = x[-append_length - 2:-1] | ||||
|                 new_y = y[-append_length - 2:-1] | ||||
|                 # print((new_x, new_y)) | ||||
| 
 | ||||
|             profiler('diffed append arrays') | ||||
| 
 | ||||
|             if should_ds: | ||||
|  | @ -490,21 +526,6 @@ class FastAppendCurve(pg.GraphicsObject): | |||
|             # self.disable_cache() | ||||
|             # flip_cache = True | ||||
| 
 | ||||
|         # XXX: do we need this any more? | ||||
|         # if ( | ||||
|         #     self._step_mode | ||||
|         # ): | ||||
|         #     self.disable_cache() | ||||
|         #     flip_cache = True | ||||
| 
 | ||||
|         # XXX: lol brutal, the internals of `CurvePoint` (inherited by | ||||
|         # our `LineDot`) required ``.getData()`` to work.. | ||||
|         self.xData = x | ||||
|         self.yData = y | ||||
| 
 | ||||
|         x0, x_last = self._xrange = x[0], x[-1] | ||||
|         y_last = y[-1] | ||||
| 
 | ||||
|         # draw the "current" step graphic segment so it lines up with | ||||
|         # the "middle" of the current (OHLC) sample. | ||||
|         if self._step_mode: | ||||
|  | @ -522,7 +543,6 @@ class FastAppendCurve(pg.GraphicsObject): | |||
|             #     f"last rect br: {self._last_step_rect}", | ||||
|             # ) | ||||
|         else: | ||||
|             # print((x[-1], y_last)) | ||||
|             self._last_line = QLineF( | ||||
|                 x[-2], y[-2], | ||||
|                 x[-1], y_last | ||||
|  | @ -536,11 +556,9 @@ class FastAppendCurve(pg.GraphicsObject): | |||
|         self.update() | ||||
|         profiler('.update()') | ||||
| 
 | ||||
|         if flip_cache: | ||||
|             # XXX: seems to be needed to avoid artifacts (see above). | ||||
|             self.setCacheMode(QGraphicsItem.DeviceCoordinateCache) | ||||
| 
 | ||||
|         self._x, self._y = x, y | ||||
|         # if flip_cache: | ||||
|         #     # XXX: seems to be needed to avoid artifacts (see above). | ||||
|         #     self.setCacheMode(QGraphicsItem.DeviceCoordinateCache) | ||||
| 
 | ||||
|     # XXX: lol brutal, the internals of `CurvePoint` (inherited by | ||||
|     # our `LineDot`) required ``.getData()`` to work.. | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue