Merge pull request #2136 from caternuson/clue_plotter
Update CLUE Plotter
This commit is contained in:
commit
c1d58372cc
2 changed files with 243 additions and 192 deletions
|
|
@ -43,44 +43,56 @@ import gc
|
|||
import board
|
||||
|
||||
from plotter import Plotter
|
||||
from plot_source import PlotSource, TemperaturePlotSource, PressurePlotSource, \
|
||||
HumidityPlotSource, ColorPlotSource, ProximityPlotSource, \
|
||||
IlluminatedColorPlotSource, VolumePlotSource, \
|
||||
AccelerometerPlotSource, GyroPlotSource, \
|
||||
MagnetometerPlotSource, PinPlotSource
|
||||
from plot_source import (
|
||||
TemperaturePlotSource,
|
||||
PressurePlotSource,
|
||||
HumidityPlotSource,
|
||||
ColorPlotSource,
|
||||
ProximityPlotSource,
|
||||
IlluminatedColorPlotSource,
|
||||
VolumePlotSource,
|
||||
AccelerometerPlotSource,
|
||||
GyroPlotSource,
|
||||
MagnetometerPlotSource,
|
||||
PinPlotSource,
|
||||
)
|
||||
from adafruit_clue import clue
|
||||
|
||||
|
||||
debug = 1
|
||||
|
||||
|
||||
# A list of all the data sources for plotting
|
||||
sources = [TemperaturePlotSource(clue, mode="Celsius"),
|
||||
TemperaturePlotSource(clue, mode="Fahrenheit"),
|
||||
PressurePlotSource(clue, mode="Metric"),
|
||||
PressurePlotSource(clue, mode="Imperial"),
|
||||
HumidityPlotSource(clue),
|
||||
ColorPlotSource(clue),
|
||||
ProximityPlotSource(clue),
|
||||
IlluminatedColorPlotSource(clue, mode="Red"),
|
||||
IlluminatedColorPlotSource(clue, mode="Green"),
|
||||
IlluminatedColorPlotSource(clue, mode="Blue"),
|
||||
IlluminatedColorPlotSource(clue, mode="Clear"),
|
||||
VolumePlotSource(clue),
|
||||
AccelerometerPlotSource(clue),
|
||||
GyroPlotSource(clue),
|
||||
MagnetometerPlotSource(clue),
|
||||
PinPlotSource([board.P0, board.P1, board.P2])
|
||||
]
|
||||
# NOTE: Due to memory contraints, the total number of data sources
|
||||
# is limited. Can try adding more until a memory limit is hit. At that
|
||||
# point, decide what to keep and what to toss. Can comment/uncomment lines
|
||||
# below as desired.
|
||||
sources = [
|
||||
TemperaturePlotSource(clue, mode="Celsius"),
|
||||
# TemperaturePlotSource(clue, mode="Fahrenheit"),
|
||||
PressurePlotSource(clue, mode="Metric"),
|
||||
# PressurePlotSource(clue, mode="Imperial"),
|
||||
HumidityPlotSource(clue),
|
||||
ColorPlotSource(clue),
|
||||
ProximityPlotSource(clue),
|
||||
# IlluminatedColorPlotSource(clue, mode="Red"),
|
||||
# IlluminatedColorPlotSource(clue, mode="Green"),
|
||||
# IlluminatedColorPlotSource(clue, mode="Blue"),
|
||||
# IlluminatedColorPlotSource(clue, mode="Clear"),
|
||||
# VolumePlotSource(clue),
|
||||
AccelerometerPlotSource(clue),
|
||||
# GyroPlotSource(clue),
|
||||
# MagnetometerPlotSource(clue),
|
||||
# PinPlotSource([board.P0, board.P1, board.P2])
|
||||
]
|
||||
# The first source to select when plotting starts
|
||||
current_source_idx = 0
|
||||
|
||||
# The various plotting styles - scroll is currently a jump scroll
|
||||
stylemodes = (("lines", "scroll"), # draws lines between points
|
||||
("lines", "wrap"),
|
||||
("dots", "scroll"), # just points - slightly quicker
|
||||
("dots", "wrap")
|
||||
)
|
||||
stylemodes = (
|
||||
("lines", "scroll"), # draws lines between points
|
||||
("lines", "wrap"),
|
||||
("dots", "scroll"), # just points - slightly quicker
|
||||
("dots", "wrap"),
|
||||
)
|
||||
current_sm_idx = 0
|
||||
|
||||
|
||||
|
|
@ -94,7 +106,7 @@ def d_print(level, *args, **kwargs):
|
|||
|
||||
def select_colors(plttr, src, def_palette):
|
||||
"""Choose the colours based on the particular PlotSource
|
||||
or forcing use of default palette."""
|
||||
or forcing use of default palette."""
|
||||
# otherwise use defaults
|
||||
channel_colidx = []
|
||||
palette = plttr.get_colors()
|
||||
|
|
@ -109,7 +121,7 @@ def select_colors(plttr, src, def_palette):
|
|||
|
||||
def ready_plot_source(plttr, srcs, def_palette, index=0):
|
||||
"""Select the plot source by index from srcs list and then setup the
|
||||
plot parameters by retrieving meta-data from the PlotSource object."""
|
||||
plot parameters by retrieving meta-data from the PlotSource object."""
|
||||
src = srcs[index]
|
||||
# Put the description of the source on screen at the top
|
||||
source_name = str(src)
|
||||
|
|
@ -132,12 +144,12 @@ def ready_plot_source(plttr, srcs, def_palette, index=0):
|
|||
|
||||
def wait_release(func, menu):
|
||||
"""Calls func repeatedly waiting for it to return a false value
|
||||
and goes through menu list as time passes.
|
||||
and goes through menu list as time passes.
|
||||
|
||||
The menu is a list of menu entries where each entry is a
|
||||
two element list of time passed in seconds and text to display
|
||||
for that period.
|
||||
The entries must be in ascending time order."""
|
||||
The menu is a list of menu entries where each entry is a
|
||||
two element list of time passed in seconds and text to display
|
||||
for that period.
|
||||
The entries must be in ascending time order."""
|
||||
|
||||
start_t_ns = time.monotonic_ns()
|
||||
menu_option = None
|
||||
|
|
@ -162,7 +174,7 @@ def wait_release(func, menu):
|
|||
|
||||
def popup_text(plttr, text, duration=1.0):
|
||||
"""Place some text on the screen using info property of Plotter object
|
||||
for duration seconds."""
|
||||
for duration seconds."""
|
||||
plttr.info = text
|
||||
time.sleep(duration)
|
||||
plttr.info = None
|
||||
|
|
@ -175,13 +187,15 @@ initial_title = "CLUE Plotter"
|
|||
# displayio has some static limits on text - pre-calculate the maximum
|
||||
# length of all of the different PlotSource objects
|
||||
max_title_len = max(len(initial_title), max([len(str(so)) for so in sources]))
|
||||
plotter = Plotter(board.DISPLAY,
|
||||
style=stylemodes[current_sm_idx][0],
|
||||
mode=stylemodes[current_sm_idx][1],
|
||||
title=initial_title,
|
||||
max_title_len=max_title_len,
|
||||
mu_output=mu_plotter_output,
|
||||
debug=debug)
|
||||
plotter = Plotter(
|
||||
board.DISPLAY,
|
||||
style=stylemodes[current_sm_idx][0],
|
||||
mode=stylemodes[current_sm_idx][1],
|
||||
title=initial_title,
|
||||
max_title_len=max_title_len,
|
||||
mu_output=mu_plotter_output,
|
||||
debug=debug,
|
||||
)
|
||||
|
||||
# If set to true this forces use of colour blindness friendly colours
|
||||
use_def_pal = False
|
||||
|
|
@ -190,21 +204,28 @@ clue.pixel[0] = clue.BLACK # turn off the NeoPixel on the back of CLUE board
|
|||
|
||||
plotter.display_on()
|
||||
# Using left and right here in case the CLUE is cased hiding A/B labels
|
||||
popup_text(plotter,
|
||||
"\n".join(["Button Guide",
|
||||
"Left: next source",
|
||||
" 2secs: palette",
|
||||
" 4s: Mu plot",
|
||||
" 6s: range lock",
|
||||
"Right: style change"]), duration=10)
|
||||
popup_text(
|
||||
plotter,
|
||||
"\n".join(
|
||||
[
|
||||
"Button Guide",
|
||||
"Left: next source",
|
||||
" 2secs: palette",
|
||||
" 4s: Mu plot",
|
||||
" 6s: range lock",
|
||||
"Right: style change",
|
||||
]
|
||||
),
|
||||
duration=10,
|
||||
)
|
||||
|
||||
count = 0
|
||||
|
||||
while True:
|
||||
# Set the source and start items
|
||||
(source, channels) = ready_plot_source(plotter, sources,
|
||||
use_def_pal,
|
||||
current_source_idx)
|
||||
(source, channels) = ready_plot_source(
|
||||
plotter, sources, use_def_pal, current_source_idx
|
||||
)
|
||||
|
||||
while True:
|
||||
# Read data from sensor or voltage from pad
|
||||
|
|
@ -213,25 +234,22 @@ while True:
|
|||
# Check for left (A) and right (B) buttons
|
||||
if clue.button_a:
|
||||
# Wait for button release with time-based menu
|
||||
opt, _ = wait_release(lambda: clue.button_a,
|
||||
[(2, "Next\nsource"),
|
||||
(4,
|
||||
("Source" if use_def_pal else "Default")
|
||||
+ "\npalette"),
|
||||
(6,
|
||||
"Mu output "
|
||||
+ ("off" if mu_plotter_output else "on")),
|
||||
(8,
|
||||
"Range lock\n" + ("off" if range_lock else "on"))
|
||||
])
|
||||
opt, _ = wait_release(
|
||||
lambda: clue.button_a,
|
||||
[
|
||||
(2, "Next\nsource"),
|
||||
(4, ("Source" if use_def_pal else "Default") + "\npalette"),
|
||||
(6, "Mu output " + ("off" if mu_plotter_output else "on")),
|
||||
(8, "Range lock\n" + ("off" if range_lock else "on")),
|
||||
],
|
||||
)
|
||||
if opt == 0: # change plot source
|
||||
current_source_idx = (current_source_idx + 1) % len(sources)
|
||||
break # to leave inner while and select the new source
|
||||
|
||||
elif opt == 1: # toggle palette
|
||||
use_def_pal = not use_def_pal
|
||||
plotter.channel_colidx = select_colors(plotter, source,
|
||||
use_def_pal)
|
||||
plotter.channel_colidx = select_colors(plotter, source, use_def_pal)
|
||||
|
||||
elif opt == 2: # toggle Mu output
|
||||
mu_plotter_output = not mu_plotter_output
|
||||
|
|
@ -244,8 +262,7 @@ while True:
|
|||
if clue.button_b: # change plot style and mode
|
||||
current_sm_idx = (current_sm_idx + 1) % len(stylemodes)
|
||||
(new_style, new_mode) = stylemodes[current_sm_idx]
|
||||
wait_release(lambda: clue.button_b,
|
||||
[(2, new_style + "\n" + new_mode)])
|
||||
wait_release(lambda: clue.button_b, [(2, new_style + "\n" + new_mode)])
|
||||
d_print(1, "Graph change", new_style, new_mode)
|
||||
plotter.change_stylemode(new_style, new_mode)
|
||||
|
||||
|
|
@ -256,7 +273,7 @@ while True:
|
|||
plotter.data_add(all_data)
|
||||
|
||||
# An occasional print of free heap
|
||||
if debug >=3 and count % 15 == 0:
|
||||
if debug >= 3 and count % 15 == 0:
|
||||
gc.collect() # must collect() first to measure free memory
|
||||
print("Free memory:", gc.mem_free())
|
||||
|
||||
|
|
|
|||
|
|
@ -49,7 +49,7 @@ import array
|
|||
import displayio
|
||||
import terminalio
|
||||
|
||||
from adafruit_display_text.label import Label
|
||||
from adafruit_display_text.bitmap_label import Label
|
||||
|
||||
|
||||
def mapf(value, in_min, in_max, out_min, out_max):
|
||||
|
|
@ -59,10 +59,11 @@ def mapf(value, in_min, in_max, out_min, out_max):
|
|||
# This creates ('{:.0f}', '{:.1f}', '{:.2f}', etc
|
||||
_FMT_DEC_PLACES = tuple("{:." + str(x) + "f}" for x in range(10))
|
||||
|
||||
|
||||
def format_width(nchars, value):
|
||||
"""Simple attempt to generate a value within nchars characters.
|
||||
Return value can be too long, e.g. for nchars=5, bad things happen
|
||||
with values > 99999 or < -9999 or < -99.9."""
|
||||
Return value can be too long, e.g. for nchars=5, bad things happen
|
||||
with values > 99999 or < -9999 or < -99.9."""
|
||||
neg_format = _FMT_DEC_PLACES[nchars - 3]
|
||||
pos_format = _FMT_DEC_PLACES[nchars - 2]
|
||||
if value <= -10.0:
|
||||
|
|
@ -76,23 +77,24 @@ def format_width(nchars, value):
|
|||
return text_value
|
||||
|
||||
|
||||
class Plotter():
|
||||
_DEFAULT_SCALE_MODE = {"lines": "onscroll",
|
||||
"dots": "screen"}
|
||||
class Plotter:
|
||||
_DEFAULT_SCALE_MODE = {"lines": "onscroll", "dots": "screen"}
|
||||
|
||||
# Palette for plotting, first one is set transparent
|
||||
TRANSPARENT_IDX = 0
|
||||
# Removed one colour to get number down to 8 for more efficient
|
||||
# bit-packing in displayio's Bitmap
|
||||
_PLOT_COLORS = (0x000000,
|
||||
0x0000ff,
|
||||
0x00ff00,
|
||||
0x00ffff,
|
||||
0xff0000,
|
||||
# 0xff00ff,
|
||||
0xffff00,
|
||||
0xffffff,
|
||||
0xff0080)
|
||||
_PLOT_COLORS = (
|
||||
0x000000,
|
||||
0x0000FF,
|
||||
0x00FF00,
|
||||
0x00FFFF,
|
||||
0xFF0000,
|
||||
# 0xff00ff,
|
||||
0xFFFF00,
|
||||
0xFFFFFF,
|
||||
0xFF0080,
|
||||
)
|
||||
|
||||
POS_INF = float("inf")
|
||||
NEG_INF = float("-inf")
|
||||
|
|
@ -110,12 +112,12 @@ class Plotter():
|
|||
_GRAPH_TOP = 30 # y position for the graph placement
|
||||
|
||||
INFO_FG_COLOR = 0x000080
|
||||
INFO_BG_COLOR = 0xc0c000
|
||||
LABEL_COLOR = 0xc0c0c0
|
||||
INFO_BG_COLOR = 0xC0C000
|
||||
LABEL_COLOR = 0xC0C0C0
|
||||
|
||||
def _display_manual(self):
|
||||
"""Intention was to disable auto_refresh here but this needs a
|
||||
simple displayio refresh to work well."""
|
||||
simple displayio refresh to work well."""
|
||||
self._output.auto_refresh = True
|
||||
|
||||
def _display_auto(self):
|
||||
|
|
@ -123,24 +125,32 @@ class Plotter():
|
|||
|
||||
def _display_refresh(self):
|
||||
"""Intention was to call self._output.refresh() but this does not work well
|
||||
as current implementation is designed with a fixed frame rate in mind."""
|
||||
as current implementation is designed with a fixed frame rate in mind."""
|
||||
if self._output.auto_refresh:
|
||||
return True
|
||||
else:
|
||||
return True
|
||||
|
||||
def __init__(self, output,
|
||||
style="lines", mode="scroll", scale_mode=None,
|
||||
screen_width=240, screen_height=240,
|
||||
plot_width=192, plot_height=201,
|
||||
x_divs=4, y_divs=4,
|
||||
scroll_px=50,
|
||||
max_channels=3,
|
||||
est_rate=50,
|
||||
title="",
|
||||
max_title_len=20,
|
||||
mu_output=False,
|
||||
debug=0):
|
||||
def __init__(
|
||||
self,
|
||||
output,
|
||||
style="lines",
|
||||
mode="scroll",
|
||||
scale_mode=None,
|
||||
screen_width=240,
|
||||
screen_height=240,
|
||||
plot_width=192,
|
||||
plot_height=201,
|
||||
x_divs=4,
|
||||
y_divs=4,
|
||||
scroll_px=50,
|
||||
max_channels=3,
|
||||
est_rate=50,
|
||||
title="",
|
||||
max_title_len=20,
|
||||
mu_output=False,
|
||||
debug=0,
|
||||
):
|
||||
"""scroll_px of greater than 1 gives a jump scroll."""
|
||||
# pylint: disable=too-many-locals,too-many-statements
|
||||
self._output = output
|
||||
|
|
@ -167,8 +177,8 @@ class Plotter():
|
|||
self._data_value = []
|
||||
for _ in range(self._max_channels):
|
||||
# 'i' is 32 bit signed integer
|
||||
self._data_y_pos.append(array.array('i', [0] * self._data_size))
|
||||
self._data_value.append(array.array('f', [0.0] * self._data_size))
|
||||
self._data_y_pos.append(array.array("i", [0] * self._data_size))
|
||||
self._data_value.append(array.array("f", [0.0] * self._data_size))
|
||||
|
||||
# begin-keep-pylint-happy
|
||||
self._data_mins = None
|
||||
|
|
@ -243,14 +253,18 @@ class Plotter():
|
|||
|
||||
for ch_idx in range(self._channels):
|
||||
# intentional use of negative array indexing
|
||||
for data_idx in range(self._data_idx - 1,
|
||||
self._data_idx - 1 - self._data_values,
|
||||
-1):
|
||||
self._data_y_pos[ch_idx][data_idx] = round(mapf(self._data_value[ch_idx][data_idx],
|
||||
self._plot_min,
|
||||
self._plot_max,
|
||||
self._plot_height_m1,
|
||||
0))
|
||||
for data_idx in range(
|
||||
self._data_idx - 1, self._data_idx - 1 - self._data_values, -1
|
||||
):
|
||||
self._data_y_pos[ch_idx][data_idx] = round(
|
||||
mapf(
|
||||
self._data_value[ch_idx][data_idx],
|
||||
self._plot_min,
|
||||
self._plot_max,
|
||||
self._plot_height_m1,
|
||||
0,
|
||||
)
|
||||
)
|
||||
|
||||
def get_colors(self):
|
||||
return self._PLOT_COLORS
|
||||
|
|
@ -288,23 +302,23 @@ class Plotter():
|
|||
self._display_manual()
|
||||
|
||||
def _make_empty_tg_plot_bitmap(self):
|
||||
plot_bitmap = displayio.Bitmap(self._plot_width, self._plot_height,
|
||||
len(self._PLOT_COLORS))
|
||||
plot_bitmap = displayio.Bitmap(
|
||||
self._plot_width, self._plot_height, len(self._PLOT_COLORS)
|
||||
)
|
||||
# Create a colour palette for plot dots/lines
|
||||
plot_palette = displayio.Palette(len(self._PLOT_COLORS))
|
||||
|
||||
for idx in range(len(self._PLOT_COLORS)):
|
||||
plot_palette[idx] = self._PLOT_COLORS[idx]
|
||||
plot_palette.make_transparent(0)
|
||||
tg_plot_data = displayio.TileGrid(plot_bitmap,
|
||||
pixel_shader=plot_palette)
|
||||
tg_plot_data = displayio.TileGrid(plot_bitmap, pixel_shader=plot_palette)
|
||||
tg_plot_data.x = self._screen_width - self._plot_width - 1
|
||||
tg_plot_data.y = self._GRAPH_TOP
|
||||
return (tg_plot_data, plot_bitmap)
|
||||
|
||||
def _make_tg_grid(self):
|
||||
# pylint: disable=too-many-locals
|
||||
grid_width = self._plot_width
|
||||
grid_width = self._plot_width
|
||||
grid_height = self._plot_height_m1
|
||||
div_width = self._plot_width // self._x_divs
|
||||
div_height = self._plot_height // self._y_divs
|
||||
|
|
@ -325,24 +339,24 @@ class Plotter():
|
|||
a_plot_grid[0, y] = 1
|
||||
|
||||
right_line = displayio.Bitmap(1, grid_height, 2)
|
||||
tg_right_line = displayio.TileGrid(right_line,
|
||||
pixel_shader=grid_palette)
|
||||
tg_right_line = displayio.TileGrid(right_line, pixel_shader=grid_palette)
|
||||
for y in range(0, grid_height, self.GRID_DOT_SPACING):
|
||||
right_line[0, y] = 1
|
||||
|
||||
bottom_line = displayio.Bitmap(grid_width + 1, 1, 2)
|
||||
tg_bottom_line = displayio.TileGrid(bottom_line,
|
||||
pixel_shader=grid_palette)
|
||||
tg_bottom_line = displayio.TileGrid(bottom_line, pixel_shader=grid_palette)
|
||||
for x in range(0, grid_width + 1, self.GRID_DOT_SPACING):
|
||||
bottom_line[x, 0] = 1
|
||||
|
||||
# Create a TileGrid using the Bitmap and Palette
|
||||
# and tiling it based on number of divisions required
|
||||
tg_plot_grid = displayio.TileGrid(a_plot_grid,
|
||||
pixel_shader=grid_palette,
|
||||
width=self._x_divs,
|
||||
height=self._y_divs,
|
||||
default_tile = 0)
|
||||
tg_plot_grid = displayio.TileGrid(
|
||||
a_plot_grid,
|
||||
pixel_shader=grid_palette,
|
||||
width=self._x_divs,
|
||||
height=self._y_divs,
|
||||
default_tile=0,
|
||||
)
|
||||
tg_plot_grid.x = self._screen_width - self._plot_width - 1
|
||||
tg_plot_grid.y = self._GRAPH_TOP
|
||||
tg_right_line.x = tg_plot_grid.x + grid_width
|
||||
|
|
@ -360,32 +374,39 @@ class Plotter():
|
|||
def _make_empty_graph(self, tg_and_plot=None):
|
||||
font_w, font_h = self._font.get_bounding_box()
|
||||
|
||||
self._displayio_title = Label(self._font,
|
||||
text=self._title,
|
||||
scale=2,
|
||||
line_spacing=1,
|
||||
color=self._y_lab_color)
|
||||
self._displayio_title = Label(
|
||||
self._font,
|
||||
text=self._title,
|
||||
scale=2,
|
||||
line_spacing=1,
|
||||
color=self._y_lab_color,
|
||||
)
|
||||
self._displayio_title.x = self._screen_width - self._plot_width
|
||||
self._displayio_title.y = font_h // 2
|
||||
|
||||
self._displayio_y_axis_lab = Label(self._font,
|
||||
text=self._y_axis_lab,
|
||||
line_spacing=1,
|
||||
color=self._y_lab_color)
|
||||
self._displayio_y_axis_lab = Label(
|
||||
self._font, text=self._y_axis_lab, line_spacing=1, color=self._y_lab_color
|
||||
)
|
||||
self._displayio_y_axis_lab.x = 0 # 0 works here because text is ""
|
||||
self._displayio_y_axis_lab.y = font_h // 2
|
||||
|
||||
plot_y_labels = []
|
||||
# y increases top to bottom of screen
|
||||
for y_div in range(self._y_divs + 1):
|
||||
plot_y_labels.append(Label(self._font,
|
||||
text=" " * self._y_lab_width,
|
||||
line_spacing=1,
|
||||
color=self._y_lab_color))
|
||||
plot_y_labels[-1].x = (self._screen_width - self._plot_width
|
||||
- self._y_lab_width * font_w - 5)
|
||||
plot_y_labels[-1].y = (round(y_div * self._plot_height / self._y_divs)
|
||||
+ self._GRAPH_TOP - 1)
|
||||
plot_y_labels.append(
|
||||
Label(
|
||||
self._font,
|
||||
text=" " * self._y_lab_width,
|
||||
line_spacing=1,
|
||||
color=self._y_lab_color,
|
||||
)
|
||||
)
|
||||
plot_y_labels[-1].x = (
|
||||
self._screen_width - self._plot_width - self._y_lab_width * font_w - 5
|
||||
)
|
||||
plot_y_labels[-1].y = (
|
||||
round(y_div * self._plot_height / self._y_divs) + self._GRAPH_TOP - 1
|
||||
)
|
||||
self._displayio_y_labs = plot_y_labels
|
||||
|
||||
# Three items (grid, axis label, title) plus the y tick labels
|
||||
|
|
@ -417,7 +438,7 @@ class Plotter():
|
|||
for idx, tick_label in enumerate(self._displayio_y_labs):
|
||||
value = y_max - idx * px_per_div
|
||||
text_value = format_width(self._y_lab_width, value)
|
||||
tick_label.text = text_value[:self._y_lab_width]
|
||||
tick_label.text = text_value[: self._y_lab_width]
|
||||
|
||||
def display_on(self, tg_and_plot=None):
|
||||
if self._displayio_graph is None:
|
||||
|
|
@ -429,8 +450,7 @@ class Plotter():
|
|||
pass
|
||||
|
||||
def _draw_vline(self, x1, y1, y2, colidx):
|
||||
"""Draw a clipped vertical line at x1 from pixel one along from y1 to y2.
|
||||
"""
|
||||
"""Draw a clipped vertical line at x1 from pixel one along from y1 to y2."""
|
||||
if y2 == y1:
|
||||
if 0 <= y2 <= self._plot_height_m1:
|
||||
self._displayio_plot[x1, y2] = colidx
|
||||
|
|
@ -439,9 +459,11 @@ class Plotter():
|
|||
# For y2 above y1, on screen this translates to being below
|
||||
step = 1 if y2 > y1 else -1
|
||||
|
||||
for line_y_pos in range(max(0, min(y1 + step, self._plot_height_m1)),
|
||||
max(0, min(y2, self._plot_height_m1)) + step,
|
||||
step):
|
||||
for line_y_pos in range(
|
||||
max(0, min(y1 + step, self._plot_height_m1)),
|
||||
max(0, min(y2, self._plot_height_m1)) + step,
|
||||
step,
|
||||
):
|
||||
self._displayio_plot[x1, line_y_pos] = colidx
|
||||
|
||||
# def _clear_plot_bitmap(self): ### woz here
|
||||
|
|
@ -460,7 +482,9 @@ class Plotter():
|
|||
for x_pos in range(x_cols):
|
||||
# "jump" the gap in the circular buffer for wrap mode
|
||||
if wrapMode and x_pos == self._x_pos:
|
||||
data_idx = (data_idx + self._data_size - self._plot_width) % self._data_size
|
||||
data_idx = (
|
||||
data_idx + self._data_size - self._plot_width
|
||||
) % self._data_size
|
||||
# ideally this should inhibit lines between wrapped data
|
||||
|
||||
y_pos = self._data_y_pos[ch_idx][data_idx]
|
||||
|
|
@ -484,12 +508,10 @@ class Plotter():
|
|||
self._redraw_all_col_idx([self.TRANSPARENT_IDX] * self._channels)
|
||||
self._plot_dirty = False
|
||||
|
||||
|
||||
def _redraw_all(self):
|
||||
self._redraw_all_col_idx(self._channel_colidx)
|
||||
self._plot_dirty = True
|
||||
|
||||
|
||||
def _undraw_column(self, x_pos, data_idx):
|
||||
"""Undraw a single column at x_pos based on data from data_idx."""
|
||||
colidx = self.TRANSPARENT_IDX
|
||||
|
|
@ -529,9 +551,9 @@ class Plotter():
|
|||
"""Update the statistics for minimum and maximum."""
|
||||
for idx, value in enumerate(values):
|
||||
# Occasionally check if we need to add a new bucket to stats
|
||||
if idx == 0 and self._values & 0xf == 0:
|
||||
if idx == 0 and self._values & 0xF == 0:
|
||||
now_ns = time.monotonic_ns()
|
||||
if now_ns - self._data_start_ns[-1] > 1e9:
|
||||
if now_ns - self._data_start_ns[-1] > 1e9:
|
||||
self._data_start_ns.append(now_ns)
|
||||
self._data_mins.append(value)
|
||||
self._data_maxs.append(value)
|
||||
|
|
@ -563,9 +585,9 @@ class Plotter():
|
|||
for ch_idx, value in enumerate(values):
|
||||
# Last two parameters appear "swapped" - this deals with the
|
||||
# displayio screen y coordinate increasing downwards
|
||||
y_pos = round(mapf(value,
|
||||
self._plot_min, self._plot_max,
|
||||
self._plot_height_m1, 0))
|
||||
y_pos = round(
|
||||
mapf(value, self._plot_min, self._plot_max, self._plot_height_m1, 0)
|
||||
)
|
||||
|
||||
if y_pos < 0 or y_pos >= self._plot_height:
|
||||
offscale = True
|
||||
|
|
@ -575,8 +597,7 @@ class Plotter():
|
|||
if self._style == "lines" and self._x_pos != 0:
|
||||
# Python supports negative array index
|
||||
prev_y_pos = self._data_y_pos[ch_idx][data_idx - 1]
|
||||
self._draw_vline(x_pos, prev_y_pos, y_pos,
|
||||
self._channel_colidx[ch_idx])
|
||||
self._draw_vline(x_pos, prev_y_pos, y_pos, self._channel_colidx[ch_idx])
|
||||
self._plot_dirty = True # bit wrong if whole line is off screen
|
||||
else:
|
||||
if not offscale:
|
||||
|
|
@ -585,9 +606,9 @@ class Plotter():
|
|||
|
||||
def _check_zoom_in(self):
|
||||
"""Check if recent data warrants zooming in on y axis scale based on checking
|
||||
minimum and maximum times which are recorded in approximate 1 second buckets.
|
||||
Returns two element tuple with (min, max) or empty tuple for no zoom required.
|
||||
Caution is required with min == max."""
|
||||
minimum and maximum times which are recorded in approximate 1 second buckets.
|
||||
Returns two element tuple with (min, max) or empty tuple for no zoom required.
|
||||
Caution is required with min == max."""
|
||||
start_idx = len(self._data_start_ns) - self.ZOOM_IN_TIME
|
||||
if start_idx < 0:
|
||||
return ()
|
||||
|
|
@ -602,8 +623,10 @@ class Plotter():
|
|||
headroom = recent_range * self.ZOOM_HEADROOM
|
||||
|
||||
# No zoom if the range of data is near the plot range
|
||||
if (self._plot_min > recent_min - headroom
|
||||
and self._plot_max < recent_max + headroom):
|
||||
if (
|
||||
self._plot_min > recent_min - headroom
|
||||
and self._plot_max < recent_max + headroom
|
||||
):
|
||||
return ()
|
||||
|
||||
new_plot_min = max(recent_min - headroom, self._abs_min)
|
||||
|
|
@ -612,8 +635,8 @@ class Plotter():
|
|||
|
||||
def _auto_plot_range(self, redraw_plot=True):
|
||||
"""Check if we need to zoom out or in based on checking historical
|
||||
data values unless y_range_lock has been set.
|
||||
"""
|
||||
data values unless y_range_lock has been set.
|
||||
"""
|
||||
if self._plot_range_lock:
|
||||
return False
|
||||
zoom_in = False
|
||||
|
|
@ -630,11 +653,10 @@ class Plotter():
|
|||
|
||||
# set new range if the data does not fit on the screen
|
||||
# this will also redo y tick labels if necessary
|
||||
if (new_plot_min < self._plot_min or new_plot_max > self._plot_max):
|
||||
if new_plot_min < self._plot_min or new_plot_max > self._plot_max:
|
||||
if self._debug >= 2:
|
||||
print("Zoom out")
|
||||
self._change_y_range(new_plot_min, new_plot_max,
|
||||
redraw_plot=redraw_plot)
|
||||
self._change_y_range(new_plot_min, new_plot_max, redraw_plot=redraw_plot)
|
||||
zoom_out = True
|
||||
|
||||
else: # otherwise check if zoom in is warranted
|
||||
|
|
@ -642,8 +664,11 @@ class Plotter():
|
|||
if rescale_zoom_range:
|
||||
if self._debug >= 2:
|
||||
print("Zoom in")
|
||||
self._change_y_range(rescale_zoom_range[0], rescale_zoom_range[1],
|
||||
redraw_plot=redraw_plot)
|
||||
self._change_y_range(
|
||||
rescale_zoom_range[0],
|
||||
rescale_zoom_range[1],
|
||||
redraw_plot=redraw_plot,
|
||||
)
|
||||
zoom_in = True
|
||||
|
||||
if zoom_in or zoom_out:
|
||||
|
|
@ -664,8 +689,11 @@ class Plotter():
|
|||
changed = self._auto_plot_range(redraw_plot=False)
|
||||
|
||||
# Undraw any previous data at current x position
|
||||
if (not changed and self._data_values >= self._plot_width
|
||||
and self._values >= self._plot_width):
|
||||
if (
|
||||
not changed
|
||||
and self._data_values >= self._plot_width
|
||||
and self._values >= self._plot_width
|
||||
):
|
||||
self._undraw_column(self._x_pos, data_idx - self._plot_width)
|
||||
|
||||
elif self._mode == "scroll":
|
||||
|
|
@ -674,12 +702,13 @@ class Plotter():
|
|||
if not changed:
|
||||
self._undraw_bitmap() # Need to cls for the scroll
|
||||
|
||||
sc_data_idx = ((data_idx + self._scroll_px - self._plot_width)
|
||||
% self._data_size)
|
||||
sc_data_idx = (
|
||||
data_idx + self._scroll_px - self._plot_width
|
||||
) % self._data_size
|
||||
self._data_values -= self._scroll_px
|
||||
self._redraw_for_scroll(0,
|
||||
self._plot_width - 1 - self._scroll_px,
|
||||
sc_data_idx)
|
||||
self._redraw_for_scroll(
|
||||
0, self._plot_width - 1 - self._scroll_px, sc_data_idx
|
||||
)
|
||||
x_pos = self._plot_width - self._scroll_px
|
||||
|
||||
elif self._scale_mode == "pixel":
|
||||
|
|
@ -751,7 +780,7 @@ class Plotter():
|
|||
|
||||
@title.setter
|
||||
def title(self, value):
|
||||
self._title = value[:self._max_title_len] # does not show truncation
|
||||
self._title = value[: self._max_title_len] # does not show truncation
|
||||
self._displayio_title.text = self._title
|
||||
|
||||
@property
|
||||
|
|
@ -763,8 +792,8 @@ class Plotter():
|
|||
@info.setter
|
||||
def info(self, value):
|
||||
"""Place some text on the screen.
|
||||
Multiple lines are supported with newline character.
|
||||
Font will be 3x standard terminalio font or 2x if that does not fit."""
|
||||
Multiple lines are supported with newline character.
|
||||
Font will be 3x standard terminalio font or 2x if that does not fit."""
|
||||
if self._displayio_info is not None:
|
||||
self._displayio_graph.pop()
|
||||
|
||||
|
|
@ -776,20 +805,25 @@ class Plotter():
|
|||
text_lines = value.split("\n")
|
||||
max_word_chars = max([len(word) for word in text_lines])
|
||||
# If too large reduce the scale
|
||||
if (max_word_chars * font_scale * font_w > self._screen_width
|
||||
or len(text_lines) * font_scale * font_h * line_spacing > self._screen_height):
|
||||
if (
|
||||
max_word_chars * font_scale * font_w > self._screen_width
|
||||
or len(text_lines) * font_scale * font_h * line_spacing
|
||||
> self._screen_height
|
||||
):
|
||||
font_scale -= 1
|
||||
|
||||
self._displayio_info = Label(self._font, text=value,
|
||||
line_spacing=line_spacing,
|
||||
scale=font_scale,
|
||||
background_color=None,
|
||||
color=self.INFO_BG_COLOR)
|
||||
self._displayio_info.palette[0] = self.INFO_FG_COLOR
|
||||
self._displayio_info.palette.make_opaque(0)
|
||||
self._displayio_info = Label(
|
||||
self._font,
|
||||
text=value,
|
||||
line_spacing=line_spacing,
|
||||
scale=font_scale,
|
||||
background_color=self.INFO_FG_COLOR,
|
||||
color=self.INFO_BG_COLOR,
|
||||
)
|
||||
# centre the (left justified) text
|
||||
self._displayio_info.x = (self._screen_width
|
||||
- font_scale * font_w * max_word_chars) // 2
|
||||
self._displayio_info.x = (
|
||||
self._screen_width - font_scale * font_w * max_word_chars
|
||||
) // 2
|
||||
self._displayio_info.y = self._screen_height // 2
|
||||
self._displayio_graph.append(self._displayio_info)
|
||||
|
||||
|
|
@ -841,7 +875,7 @@ class Plotter():
|
|||
|
||||
@y_axis_lab.setter
|
||||
def y_axis_lab(self, text):
|
||||
self._y_axis_lab = text[:self._y_lab_width]
|
||||
self._y_axis_lab = text[: self._y_lab_width]
|
||||
font_w, _ = self._font.get_bounding_box()
|
||||
x_pos = (40 - font_w * len(self._y_axis_lab)) // 2
|
||||
# max() used to prevent negative (off-screen) values
|
||||
|
|
|
|||
Loading…
Reference in a new issue