Skip to content

Commit b99718b

Browse files
authored
Change Sparkline to use polygon
Previous implementation used multiple Line elements, causing unnecessary memory overhead
1 parent c701027 commit b99718b

1 file changed

Lines changed: 34 additions & 76 deletions

File tree

adafruit_display_shapes/sparkline.py

Lines changed: 34 additions & 76 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, Maciej Sokołowski
28+
* Author(s): Kevin Matocha
2929
3030
Implementation Notes
3131
--------------------
@@ -44,7 +44,7 @@
4444
except ImportError:
4545
pass
4646
import displayio
47-
from adafruit_display_shapes.line import Line
47+
from adafruit_display_shapes.polygon import Polygon
4848

4949

5050
class _CyclicBuffer():
@@ -62,23 +62,13 @@ def push(self, value: float) -> None:
6262
def pop(self) -> float:
6363
if self.len() == 0:
6464
raise RuntimeError("Trying to pop from empty buffer")
65-
result = self.first()
65+
result = self._buffer[self._start]
6666
self._start += 1
6767
if self._start == len(self._buffer):
6868
self._start -= len(self._buffer)
6969
self._end -= len(self._buffer)
7070
return result
7171

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-
8272
def len(self) -> int:
8373
return self._end - self._start
8474

@@ -147,18 +137,17 @@ def __init__(
147137
self.y_top = y_max
148138
# y_top: The actual minimum value of the vertical scale, will be
149139
# updated if autorange
150-
self._redraw = True # _redraw: redraw primitives
151-
self._last = [] # _last: last point of sparkline
140+
self._points = [] # _points: all points of sparkline
152141

153-
super().__init__(x=x, y=y) # self is a group of lines
142+
super().__init__(x=x, y=y) # self is a group of single Polygon
143+
# (TODO: it has one element, maybe group is no longer needed?)
154144

155145
def clear_values(self) -> None:
156146
"""Clears _buffer and removes all lines in the group"""
157147

158148
for _ in range(len(self)): # remove all items from the current group
159149
self.pop()
160150
self._buffer.clear()
161-
self._redraw = True
162151

163152
def add_value(self, value: float, update: bool = True) -> None:
164153
"""Add a value to the sparkline.
@@ -181,16 +170,13 @@ def add_value(self, value: float, update: bool = True) -> None:
181170
self.y_bottom = min(self._buffer.values())
182171
if self.y_max is None and first == self.y_top:
183172
self.y_top = max(self._buffer.values())
184-
self._redraw = True
185173
self._buffer.push(value)
186174

187175
if self.y_min is None:
188-
self._redraw = self._redraw or value < self.y_bottom
189176
self.y_bottom = (
190177
value if not self.y_bottom else min(value, self.y_bottom)
191178
)
192179
if self.y_max is None:
193-
self._redraw = self._redraw or value > self.y_top
194180
self.y_top = value if not self.y_top else max(value, self.y_top)
195181

196182
if update:
@@ -218,25 +204,23 @@ def _xintercept(
218204
) / slope # calculate the x-intercept at position y=horizontalY
219205
return int(xint)
220206

221-
def _plotline(
207+
def _add_point(
222208
self,
223-
x_1: int,
224-
last_value: float,
225-
x_2: int,
209+
x: int,
226210
value: float,
227211
) -> None:
228212

229213
# Guard for y_top and y_bottom being the same
230214
if self.y_top == self.y_bottom:
231-
y_2 = int(0.5 * self.height)
232-
y_1 = int(0.5 * self.height)
215+
y = int(0.5 * self.height)
233216
else:
234-
y_2 = int(self.height * (self.y_top - value) / (self.y_top - self.y_bottom))
235-
y_1 = int(
236-
self.height * (self.y_top - last_value) / (self.y_top - self.y_bottom)
237-
)
238-
self.append(Line(x_1, y_1, x_2, y_2, self.color)) # plot the line
239-
self._last = [x_2, value]
217+
y = int(self.height * (self.y_top - value) / (self.y_top - self.y_bottom))
218+
self._points.append((x, y))
219+
220+
def _draw(self) -> None:
221+
while(len(self)):
222+
self.pop()
223+
self.append(Polygon(self._points, outline=self.color, close=False)) # plot the polyline
240224

241225
# pylint: disable= too-many-branches, too-many-nested-blocks, too-many-locals, too-many-statements
242226

@@ -246,42 +230,27 @@ def update(self) -> None:
246230
# bail out early if we only have a single point
247231
n_points = self._buffer.len()
248232
if n_points < 2:
249-
self._last = [0, self._buffer.first()]
250233
return
251234

252235
if self.dyn_xpitch:
253236
# this is a float, only make int when plotting the line
254237
xpitch = (self.width - 1) / (n_points - 1)
255-
self._redraw = True
256238
else:
257239
xpitch = self._xpitch
258240

259-
# only add new segment if redrawing is not necessary
260-
if not self._redraw:
261-
# end of last line (last point, read as "x(-1)")
262-
x_m1 = self._last[0]
263-
y_m1 = self._last[1]
264-
# end of new line (new point, read as "x(0)")
265-
x_0 = int(x_m1 + xpitch)
266-
y_0 = self._buffer.last()
267-
self._plotline(x_m1, y_m1, x_0, y_0)
268-
return
269-
270-
self._redraw = False # reset, since we now redraw everything
271-
for _ in range(len(self)): # remove all items from the current group
272-
self.pop()
241+
self._points = [] # remove all points
273242

274243
for count, value in enumerate(self._buffer.values()):
275244
if count == 0:
276-
pass # don't draw anything for a first point
245+
self._add_point(0, value)
277246
else:
278-
x_2 = int(xpitch * count)
279-
x_1 = int(xpitch * (count - 1))
247+
x = int(xpitch * count)
248+
last_x = int(xpitch * (count - 1))
280249

281250
if (self.y_bottom <= last_value <= self.y_top) and (
282251
self.y_bottom <= value <= self.y_top
283252
): # both points are in range, plot the line
284-
self._plotline(x_1, last_value, x_2, value)
253+
self._add_point(x, value)
285254

286255
else: # at least one point is out of range, clip one or both ends the line
287256
if ((last_value > self.y_top) and (value > self.y_top)) or (
@@ -291,45 +260,34 @@ def update(self) -> None:
291260
pass
292261
else:
293262
xint_bottom = self._xintercept(
294-
x_1, last_value, x_2, value, self.y_bottom
263+
last_x, last_value, x, value, self.y_bottom
295264
) # get possible new x intercept points
296265
xint_top = self._xintercept(
297-
x_1, last_value, x_2, value, self.y_top
266+
last_x, last_value, x, value, self.y_top
298267
) # on the top and bottom of range
299268
if (xint_bottom is None) or (
300269
xint_top is None
301270
): # out of range doublecheck
302271
pass
303272
else:
304273
# Initialize the adjusted values as the baseline
305-
adj_x_1 = x_1
306-
adj_last_value = last_value
307-
adj_x_2 = x_2
274+
adj_x = x
308275
adj_value = value
309276

310277
if value > last_value: # slope is positive
311-
if xint_bottom >= x_1: # bottom is clipped
312-
adj_x_1 = xint_bottom
313-
adj_last_value = self.y_bottom # y_1
314-
if xint_top <= x_2: # top is clipped
315-
adj_x_2 = xint_top
316-
adj_value = self.y_top # y_2
278+
if xint_top <= x: # top is clipped
279+
adj_x = xint_top
280+
adj_value = self.y_top # y
317281
else: # slope is negative
318-
if xint_top >= x_1: # top is clipped
319-
adj_x_1 = xint_top
320-
adj_last_value = self.y_top # y_1
321-
if xint_bottom <= x_2: # bottom is clipped
322-
adj_x_2 = xint_bottom
323-
adj_value = self.y_bottom # y_2
324-
325-
self._plotline(
326-
adj_x_1,
327-
adj_last_value,
328-
adj_x_2,
329-
adj_value,
330-
)
282+
if xint_bottom <= x: # bottom is clipped
283+
adj_x = xint_bottom
284+
adj_value = self.y_bottom # y
285+
286+
self._add_point(adj_x, adj_value)
331287

332288
last_value = value # store value for the next iteration
289+
290+
self._draw()
333291

334292
def values(self) -> List[float]:
335293
"""Returns the values displayed on the sparkline."""

0 commit comments

Comments
 (0)