Merge pull request #2136 from caternuson/clue_plotter

Update CLUE Plotter
This commit is contained in:
Anne Barela 2022-04-05 18:56:05 -04:00 committed by GitHub
commit c1d58372cc
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
2 changed files with 243 additions and 192 deletions

View file

@ -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())

View file

@ -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