Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Cyclic graph draw #356

Open
wants to merge 29 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
dfcd618
moving clearing stripe
Reznic Apr 17, 2020
68db24b
first poc
Reznic Apr 17, 2020
fd1dafe
merged with master (auto-scale)
Reznic Apr 19, 2020
a93e015
improved poc.
Reznic Apr 19, 2020
905d788
works decently. graph width calculated. samples num can change. begin…
Reznic Apr 20, 2020
c2e5f77
testing with scatter
Reznic Apr 20, 2020
248c998
removed scatter
Reznic Apr 20, 2020
e093fff
trying tight layout to solve large margins
Reznic Apr 20, 2020
e7d7369
minor adjustments
Reznic Apr 20, 2020
f096226
changed graph values queue to dequeue
Reznic Apr 20, 2020
a534bbc
fixed double printing bug
Reznic Apr 20, 2020
0ac4c6f
shit works but with voodoo. fuck my life
Reznic Apr 20, 2020
0fb504b
solved async sample gui bug
Reznic Apr 20, 2020
8dbc271
merge with previous work
Reznic Apr 20, 2020
b2dc0fa
almost finished
Reznic Apr 21, 2020
518a243
fixed thresholds update on load
Reznic Apr 21, 2020
f5c827b
fixed missed erasing bug, by drawing 5 samples after erase index. rem…
Reznic Apr 21, 2020
f16498e
now blitting only 2 columns - drawn and erased. should improve perfor…
Reznic Apr 21, 2020
458ee67
fixed graph begin offset
Reznic Apr 21, 2020
f656be6
added documentation lines
Reznic Apr 26, 2020
220d800
merge with master
Reznic Apr 26, 2020
82f539f
removed unnecessary method
Reznic Apr 26, 2020
fcfc068
need to fix ut
Reznic Apr 26, 2020
1c835ec
UT passes!
Reznic Apr 26, 2020
df515d4
fixed another UT
Reznic Apr 26, 2020
377e9f2
fixed another shit in UT
Reznic Apr 26, 2020
63a1f34
hopefully all ut will pass now Amengit add -u!
Reznic Apr 26, 2020
9154db2
decreased graph begin offset
Reznic Apr 26, 2020
8a5f2f8
Merge branch 'master' into cyclic_graph_draw
Reznic Apr 26, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 2 additions & 4 deletions application.py
Original file line number Diff line number Diff line change
Expand Up @@ -103,12 +103,10 @@ def run(self):
try:
time_now = time.time()
if (time_now - self.last_gui_update_ts) >= self.frame_interval:
self.gui_update()
self.last_gui_update_ts = time_now

if (time_now - self.last_sample_update_ts) >= self.sample_interval:
self.sample()
self.last_sample_update_ts = time_now
self.gui_update()
self.last_gui_update_ts = time_now

self.arm_wd_event.set()
except KeyboardInterrupt:
Expand Down
35 changes: 18 additions & 17 deletions data/measurements.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from queue import Queue
from collections import deque
from threading import Lock

from data.configurations import Configurations
Expand All @@ -10,10 +10,9 @@ def __init__(self, sample_rate=22):
self.expiration_volume = 0
self.avg_insp_volume = 0
self.avg_exp_volume = 0
self.flow_measurements = Queue(maxsize=40) # TODO: Rename?
self.pressure_measurements = Queue(maxsize=40) # TODO: Rename?
self.sample_interval = 1 / sample_rate
self.x_axis = range(0, self._amount_of_samples_in_graph)
self.init_samples_queues(0)
self.x_axis = range(0, self.samples_in_graph)
self.intake_peak_flow = 0
self.intake_peak_pressure = 0
self.peep_min_pressure = 0
Expand All @@ -22,6 +21,14 @@ def __init__(self, sample_rate=22):
self.battery_percentage = 0
self.lock = Lock()

def init_samples_queues(self, init_value, size=None):
if size is None:
size = self.samples_in_graph
self.flow_measurements = deque([init_value] * size,
maxlen=self.samples_in_graph)
self.pressure_measurements = deque([init_value] * size,
maxlen=self.samples_in_graph)

def reset(self):
self.inspiration_volume = 0
self.expiration_volume = 0
Expand All @@ -34,25 +41,19 @@ def reset(self):

def set_flow_value(self, new_value):
with self.lock:
# pop last item if queue is full
if self.flow_measurements.full():
self.flow_measurements.get()
self.flow_measurements.put(new_value)
self.flow_measurements.append(new_value)

def set_pressure_value(self, new_value):
with self.lock:
# pop last item if queue is full
if self.pressure_measurements.full():
self.pressure_measurements.get()
self.pressure_measurements.put(new_value)
self.pressure_measurements.append(new_value)

def get_flow_value(self, new_value):
def get_flow_value(self):
with self.lock:
self.flow_measurements.get(new_value)
return (self.flow_measurements[-2], self.flow_measurements[-1])

def get_pressure_value(self, new_value):
def get_pressure_value(self):
with self.lock:
self.pressure_measurements.get(new_value)
return (self.pressure_measurements[-2], self.pressure_measurements[-1])

def set_intake_peaks(self, flow, pressure, volume):
self.intake_peak_flow = flow
Expand All @@ -66,6 +67,6 @@ def set_battery_percentage(self, percentage):
self.battery_percentage = percentage

@property
def _amount_of_samples_in_graph(self):
def samples_in_graph(self):
config = Configurations.instance()
return int(config.graph_seconds / self.sample_interval)
164 changes: 137 additions & 27 deletions graphics/graphs.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
from matplotlib.figure import Figure
from matplotlib import rcParams
from matplotlib.transforms import Bbox
from matplotlib import ticker
from math import ceil

from data.configurations import Configurations
from graphics.themes import Theme
Expand All @@ -14,6 +16,8 @@ class Graph(object):
YLABEL = NotImplemented
COLOR = NotImplemented
DPI = 100 # pixels per inch
ERASE_GAP = 10 # samples to be cleaned from tail, ahead of new sample print
GRAPH_BEGIN_OFFSET = 70 # pixel offset from canvas edge, to begin of graph

def __init__(self, parent, measurements, width, height):
rcParams.update({'figure.autolayout': True})
Expand All @@ -23,9 +27,19 @@ def __init__(self, parent, measurements, width, height):
self.config = Configurations.instance()
self.height = height
self.width = width

# pixel width of graph draw area, without axes
self.graph_width = None
self.graph_height = None
self.graph_bbox = None
self.graph_bg = None
self.current_min_y, self.current_max_y = self.configured_scale
# snapshot of graph frame, in clean state
self.graph_clean_bg = None
# snapshot of 1 sample-width column, from clean graph
self.eraser_bg = None
self.pixels_per_sample = None
# index of last updated sample
self.print_index = -1
self.erase_index = None

self.figure = Figure(figsize=(self.width/self.DPI,
self.height/self.DPI),
Expand Down Expand Up @@ -55,7 +69,6 @@ def __init__(self, parent, measurements, width, height):
self.axis.axhline(y=0, color='white', lw=1)

# Configure graph
self.display_values = [0] * self.measurements._amount_of_samples_in_graph
self.graph, = self.axis.plot(
self.measurements.x_axis,
self.display_values,
Expand All @@ -66,9 +79,30 @@ def __init__(self, parent, measurements, width, height):
self.canvas = FigureCanvasTkAgg(self.figure, master=self.root)

# Scaling
self.current_min_y, self.current_max_y = self.configured_scale
self.graph.axes.set_ylim(*self.configured_scale)
self.figure.tight_layout()

def get_graph_width(self):
"""Return the pixel width of the graph axis."""
boundaries = self.axis.get_position() * \
self.axis.get_figure().get_size_inches() * self.DPI
width, height = boundaries[1] - boundaries[0]
return ceil(float(width))

def save_eraser_bg(self):
"""Capture background for eraser of cyclic graph."""
# graph boundary points
x1, y1, x2, y2 = self.graph_clean_bg.get_extents()

# Capture column of pixels, from middle of the clean background,
# This column will be pasted cyclically on the graph, to clean it.
capture_offset = x1 + (self.graph_width / 2)
eraser_width = ceil(self.pixels_per_sample)
self.graph_height = y2 - y1
bbox = (capture_offset, y1, capture_offset + eraser_width, y2)
self.eraser_bg = self.canvas.copy_from_bbox(bbox)

@staticmethod
@ticker.FuncFormatter
def tick_aligned_format(x, pos):
Expand All @@ -81,24 +115,82 @@ def tick_aligned_format(x, pos):

return label

def save_bg(self):
"""Capture the current drawing of graph, and render it as background."""
self.graph_bg = self.canvas.copy_from_bbox(self.graph_bbox)

def render(self):
self._redraw_frame()

def _redraw_frame(self):
"""Called only once - to render the graph"""
self.canvas.draw()
self.canvas.get_tk_widget().place(relx=self.RELX, rely=self.RELY,
height=self.height,
width=self.width)
self.graph_bbox = self.canvas.figure.bbox
self.save_bg()
self.graph_clean_bg = self.canvas.copy_from_bbox(self.graph_bbox)
self.graph_width = self.get_graph_width()
self.pixels_per_sample = \
float(self.graph_width) / self.measurements.samples_in_graph
self.save_eraser_bg()

def redraw_graph(self):
"""Redraw entire graph. Called when graph properties changed."""
print_index = self.print_index + 2

# Clone and reorder samples list, to match the draw order
recovery_y_values = self.display_values.copy()
recovery_y_values.rotate(print_index)
recovery_y_values = list(recovery_y_values)

# Ranges of graph parts to redraw
draw_ranges = []

if self.print_index < self.erase_index:
# Normal case: during draw cycle
# |~~~ ~~~|
draw_ranges = [(0, print_index),
(self.erase_index+5, self.measurements.samples_in_graph)]

elif self.print_index > self.erase_index:
# Draw cycle at the right edge. started erasing the left edge
# | ~~~~ |
draw_ranges = [(self.erase_index+5, print_index)]

# Draw all graph parts
for start_index, end_index in draw_ranges:
self.graph.set_ydata(recovery_y_values[start_index:end_index])
self.graph.set_xdata(self.measurements.x_axis[start_index:end_index])
self.axis.draw_artist(self.graph)

self.figure.canvas.blit(self.graph_bbox)

def update(self):
# Restore the saved background, and redraw the graph
self.figure.canvas.restore_region(self.graph_bg)
self.graph.set_ydata(self.display_values)
# drawn sample index advances cyclically
self.print_index += 1
self.print_index %= self.measurements.samples_in_graph

# Calculate pixel offsets to erase and print at
self.erase_index = (self.print_index + self.ERASE_GAP) % \
self.measurements.samples_in_graph
erase_offset = int(self.erase_index * self.pixels_per_sample) \
% self.graph_width + self.GRAPH_BEGIN_OFFSET
print_offset = int(self.print_index * self.pixels_per_sample) \
% self.graph_width + self.GRAPH_BEGIN_OFFSET

# Paste eraser column background, to erase tail sample
self.figure.canvas.restore_region(self.eraser_bg,
xy=(erase_offset, 0))

# Draw line between 2 most recent samples
self.graph.set_ydata([self.display_values[-2], self.display_values[-1]])
self.graph.set_xdata([self.print_index, self.print_index + 1])
self.axis.draw_artist(self.graph)
self.figure.canvas.blit(self.graph_bbox)

# Update screen only in printed column and erased column areas
self.figure.canvas.blit(Bbox.from_bounds(x0=print_offset, y0=0,
width=self.pixels_per_sample,
height=self.graph_height))
self.figure.canvas.blit(Bbox.from_bounds(x0=erase_offset, y0=0,
width=self.pixels_per_sample,
height=self.graph_height))
self.figure.canvas.flush_events()

@property
Expand All @@ -109,6 +201,10 @@ def element(self):
def configured_scale(self):
raise NotImplementedError()

@property
def display_values(self):
raise NotImplementedError()


class FlowGraph(Graph):
RELX = 0
Expand All @@ -132,14 +228,22 @@ def __init__(self, *args, **kwargs):
def configured_scale(self):
return self.config.flow_y_scale

@property
def display_values(self):
return self.measurements.flow_measurements

def autoscale(self):
"""Symmetrically rescale the Y-axis."""
self.current_iteration += 1
self.current_iteration %= max(self.ZOOM_IN_FREQUENCY,
self.ZOOM_OUT_FREQUENCY)

new_min_y = min(self.display_values)
new_max_y = max(self.display_values)
if len(self.display_values):
new_min_y = min(self.display_values)
new_max_y = max(self.display_values)
else:
new_min_y = 0
new_max_y = 0

# Once every <self.ZOOM_IN_FREQUENCY> calls we want to try and
# zoom back-in
Expand All @@ -151,7 +255,8 @@ def autoscale(self):

self.current_min_y, self.current_max_y = original_min, original_max
self.graph.axes.set_ylim(self.current_min_y, self.current_max_y)
self.render()
self._redraw_frame()
self.redraw_graph()
return

if self.current_iteration % self.ZOOM_OUT_FREQUENCY != 0:
Expand All @@ -175,14 +280,14 @@ def autoscale(self):
self.graph.axes.set_ylim(self.current_min_y - self.GRAPH_MARGINS,
self.current_max_y + self.GRAPH_MARGINS)

self.render()
self._redraw_frame()
self.redraw_graph()

def update(self):
super().update()
if self.config.autoscale:
self.autoscale()

super().update()


class AirPressureGraph(Graph):
RELX = 0
Expand All @@ -195,12 +300,11 @@ def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.min_threshold = None
self.max_threshold = None
self.config.pressure_range.observer.subscribe(self, self.update_thresholds)
self.update_thresholds((self.config.pressure_range.min,
self.config.pressure_range.max))
self.config.pressure_range.observer.subscribe(self,
self.update_thresholds)

def update_thresholds(self, range):
min_value, max_value = range
def update_thresholds(self, pressure_range, redraw_graph=True):
min_value, max_value = pressure_range

if self.min_threshold:
self.min_threshold.remove()
Expand All @@ -210,13 +314,19 @@ def update_thresholds(self, range):
self.min_threshold = self.axis.axhline(y=min_value, color='red', lw=1)
self.max_threshold = self.axis.axhline(y=max_value, color='red', lw=1)

self.canvas.draw()
self.save_bg()
self._redraw_frame()
if redraw_graph:
self.redraw_graph()

def render(self):
self.update_thresholds((self.config.pressure_range.min,
self.config.pressure_range.max),
redraw_graph=False)

@property
def configured_scale(self):
return self.config.pressure_y_scale

@property
def element(self):
return self.canvas
def display_values(self):
return self.measurements.pressure_measurements
15 changes: 0 additions & 15 deletions graphics/panes.py
Original file line number Diff line number Diff line change
Expand Up @@ -119,14 +119,6 @@ def __init__(self, parent, measurements):
self.pressure_graph = AirPressureGraph(self, self.measurements,
self.width, self.height/2)

def pop_queue_to_list(self, q, lst):
# pops all queue values into list, returns if items appended to queue
had_values = not q.empty()
while not q.empty():
lst.pop(0)
lst.append(q.get())
return had_values

@property
def element(self):
return self.frame
Expand All @@ -143,13 +135,6 @@ def render(self):


def update(self):
# Get measurments from peripherals

self.pop_queue_to_list(self.measurements.pressure_measurements,
self.pressure_graph.display_values)
self.pop_queue_to_list(self.measurements.flow_measurements,
self.flow_graph.display_values)

for graph in self.graphs:
graph.update()

Expand Down
Loading