Skip to content

Commit 63f9d2f

Browse files
authored
Change sparkline to use cyclic buffer
Since we specify the maximum ammount of data points, we can use cyclic buffer underneath, therefore avoiding memory fragmentation. This should also help for problems decribed in #25
1 parent de9791d commit 63f9d2f

1 file changed

Lines changed: 64 additions & 14 deletions

File tree

adafruit_display_shapes/sparkline.py

Lines changed: 64 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
Various common shapes for use with displayio - Sparkline!
2626
2727
28-
* Author(s): Kevin Matocha
28+
* Author(s): Kevin Matocha, Maciej Sokołowski
2929
3030
Implementation Notes
3131
--------------------
@@ -47,6 +47,56 @@
4747
from adafruit_display_shapes.line import Line
4848

4949

50+
class _CyclicBuffer():
51+
def __init__(self, size: int) -> None:
52+
self._buffer = [None] * size
53+
self._start = 0 # between 0 and size-1
54+
self._end = 0 # between 0 and 2*size-1
55+
56+
def push(self, value: float) -> None:
57+
if self.len() == len(self._buffer):
58+
raise RuntimeError("Trying to push to full buffer")
59+
self._buffer[self._end % len(self._buffer)] = value
60+
self._end += 1
61+
62+
def pop(self) -> float:
63+
if self.len() == 0:
64+
raise RuntimeError("Trying to pop from empty buffer")
65+
result = self.first()
66+
self._start += 1
67+
if self._start == len(self._buffer):
68+
self._start -= len(self._buffer)
69+
self._end -= len(self._buffer)
70+
return result
71+
72+
def first(self) -> float:
73+
if self.len() == 0:
74+
return None
75+
return self._buffer[self._start]
76+
77+
def last(self) -> float:
78+
if self.len() == 0:
79+
return None
80+
return self._buffer[(self._end - 1) % len(self._buffer)]
81+
82+
def len(self) -> int:
83+
return self._end - self._start
84+
85+
def clear(self) -> None:
86+
self._start = 0
87+
self._end = 0
88+
89+
def values(self) -> List[float]:
90+
if self.len() == 0:
91+
return []
92+
start = self._start
93+
end = self._end % len(self._buffer)
94+
if start < end:
95+
return self._buffer[start:end]
96+
else:
97+
return self._buffer[start:] + self._buffer[:end]
98+
99+
50100
class Sparkline(displayio.Group):
51101
# pylint: disable=too-many-arguments
52102
"""A sparkline graph.
@@ -85,7 +135,7 @@ def __init__(
85135
self.height = height # in pixels
86136
self.color = color #
87137
self._max_items = max_items # maximum number of items in the list
88-
self._spark_list = [] # list containing the values
138+
self._buffer = _CyclicBuffer(self._max_items)
89139
self.dyn_xpitch = dyn_xpitch
90140
if not dyn_xpitch:
91141
self._xpitch = (width - 1) / (self._max_items - 1)
@@ -103,11 +153,11 @@ def __init__(
103153
super().__init__(x=x, y=y) # self is a group of lines
104154

105155
def clear_values(self) -> None:
106-
"""Removes all values from the _spark_list list and removes all lines in the group"""
156+
"""Clears _buffer and removes all lines in the group"""
107157

108158
for _ in range(len(self)): # remove all items from the current group
109159
self.pop()
110-
self._spark_list = [] # empty the list
160+
self._buffer.clear()
111161
self._redraw = True
112162

113163
def add_value(self, value: float, update: bool = True) -> None:
@@ -123,16 +173,16 @@ def add_value(self, value: float, update: bool = True) -> None:
123173

124174
if value is not None:
125175
if (
126-
len(self._spark_list) >= self._max_items
176+
self._buffer.len() >= self._max_items
127177
): # if list is full, remove the first item
128-
first = self._spark_list.pop(0)
178+
first = self._buffer.pop()
129179
# check if boundaries have to be updated
130180
if self.y_min is None and first == self.y_bottom:
131-
self.y_bottom = min(self._spark_list)
181+
self.y_bottom = min(self._buffer.values())
132182
if self.y_max is None and first == self.y_top:
133-
self.y_top = max(self._spark_list)
183+
self.y_top = max(self._buffer.values())
134184
self._redraw = True
135-
self._spark_list.append(value)
185+
self._buffer.push(value)
136186

137187
if self.y_min is None:
138188
self._redraw = self._redraw or value < self.y_bottom
@@ -194,9 +244,9 @@ def update(self) -> None:
194244
"""Update the drawing of the sparkline."""
195245

196246
# bail out early if we only have a single point
197-
n_points = len(self._spark_list)
247+
n_points = self._buffer.len()
198248
if n_points < 2:
199-
self._last = [0, self._spark_list[0]]
249+
self._last = [0, self._buffer.first()]
200250
return
201251

202252
if self.dyn_xpitch:
@@ -213,15 +263,15 @@ def update(self) -> None:
213263
y_m1 = self._last[1]
214264
# end of new line (new point, read as "x(0)")
215265
x_0 = int(x_m1 + xpitch)
216-
y_0 = self._spark_list[-1]
266+
y_0 = self._buffer.last()
217267
self._plotline(x_m1, y_m1, x_0, y_0)
218268
return
219269

220270
self._redraw = False # reset, since we now redraw everything
221271
for _ in range(len(self)): # remove all items from the current group
222272
self.pop()
223273

224-
for count, value in enumerate(self._spark_list):
274+
for count, value in enumerate(self._buffer.values()):
225275
if count == 0:
226276
pass # don't draw anything for a first point
227277
else:
@@ -284,4 +334,4 @@ def update(self) -> None:
284334
def values(self) -> List[float]:
285335
"""Returns the values displayed on the sparkline."""
286336

287-
return self._spark_list
337+
return self._buffer.values()

0 commit comments

Comments
 (0)