Files
templates/python/set_devices/plot_decimation.py

81 lines
3.2 KiB
Python

"""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)))