Add native QCustomPlot renderer and shared plot adapters
This commit is contained in:
80
python/set_devices/plot_decimation.py
Normal file
80
python/set_devices/plot_decimation.py
Normal file
@@ -0,0 +1,80 @@
|
||||
"""Pixel-column envelope for drawing; measurement samples are never changed."""
|
||||
import math
|
||||
from bisect import bisect_left, bisect_right
|
||||
|
||||
|
||||
class IndexedTimes(list):
|
||||
"""Random-access timestamps, with ordering validated once when loaded."""
|
||||
def __init__(self, points):
|
||||
super().__init__(points)
|
||||
self.ordered = all(math.isfinite(x) and (i == 0 or self[i-1] <= x)
|
||||
for i, x in enumerate(self))
|
||||
|
||||
|
||||
def viewport_samples(timestamps, values, left, right, neighbours=False, budget=None):
|
||||
"""Select only the viewport in logarithmic time for indexed records."""
|
||||
if isinstance(timestamps, (list, tuple, range)) and getattr(timestamps, "ordered", True):
|
||||
start = bisect_left(timestamps, left)
|
||||
stop = bisect_right(timestamps, right)
|
||||
if neighbours:
|
||||
start = max(0, start - 1)
|
||||
stop = min(len(timestamps), stop + 1)
|
||||
stride = max(1, math.ceil((stop-start) / budget)) if budget else 1
|
||||
for i in range(start, min(stop, len(values)), stride):
|
||||
yield timestamps[i], values[i]
|
||||
if budget and stop > start and (stop - 1 - start) % stride:
|
||||
yield timestamps[stop-1], values[stop-1]
|
||||
else:
|
||||
# Streaming or unordered data keeps its original gap/order semantics.
|
||||
yield from zip(timestamps, values)
|
||||
|
||||
|
||||
|
||||
def pixel_envelope(timestamps, values, left, right, width):
|
||||
"""Yield first/min/max/last samples in time order for each pixel column.
|
||||
|
||||
NaNs separate paths. Keep one neighbouring sample on either side so clipping
|
||||
and zooming retain lines crossing the viewport boundaries.
|
||||
"""
|
||||
width = max(1, int(width))
|
||||
scale = width / (right - left) if right > left else 1.
|
||||
bucket = None
|
||||
first = low = high = last = None
|
||||
|
||||
def emit():
|
||||
if first is None:
|
||||
return ()
|
||||
selected = {point[0]: point for point in (first, low, high, last)}
|
||||
return tuple((selected[index][1], selected[index][2]) for index in sorted(selected))
|
||||
|
||||
for index, (stamp, value) in enumerate(viewport_samples(timestamps, values, left, right, neighbours=True)):
|
||||
if not math.isfinite(stamp) or not math.isfinite(value):
|
||||
yield from emit()
|
||||
yield None
|
||||
bucket = None
|
||||
first = low = high = last = None
|
||||
continue
|
||||
column = -1 if stamp < left else width if stamp > right else min(width - 1, int((stamp - left) * scale))
|
||||
point = (index, stamp, value)
|
||||
if column != bucket:
|
||||
yield from emit()
|
||||
bucket = column
|
||||
first = low = high = last = point
|
||||
elif column == -1:
|
||||
first = low = high = last = point
|
||||
elif column == width:
|
||||
continue
|
||||
else:
|
||||
last = point
|
||||
if value < low[2]:
|
||||
low = point
|
||||
if value > high[2]:
|
||||
high = point
|
||||
yield from emit()
|
||||
|
||||
|
||||
def preview_window_count(requested, preview_count, original_count):
|
||||
"""Translate a raw-sample window to a display-envelope window."""
|
||||
if not original_count or original_count <= preview_count:
|
||||
return requested
|
||||
return max(1, min(preview_count, math.ceil(requested * preview_count / original_count)))
|
||||
Reference in New Issue
Block a user