Skip to content

Commit bcd9e5c

Browse files
authored
Merge pull request #53 from bablokb/sparkline_opt2
performance: implement growing sparkline
2 parents 7c23dbe + 25a1bc7 commit bcd9e5c

File tree

1 file changed

+117
-89
lines changed

1 file changed

+117
-89
lines changed

adafruit_display_shapes/sparkline.py

+117-89
Original file line numberDiff line numberDiff line change
@@ -54,18 +54,25 @@ class Sparkline(displayio.Group):
5454
:param width: Width of the sparkline graph in pixels
5555
:param height: Height of the sparkline graph in pixels
5656
:param max_items: Maximum number of values housed in the sparkline
57+
:param dyn_xpitch: dynamically change xpitch (True)
5758
:param y_min: Lower range for the y-axis. Set to None for autorange.
5859
:param y_max: Upper range for the y-axis. Set to None for autorange.
5960
:param x: X-position on the screen, in pixels
6061
:param y: Y-position on the screen, in pixels
6162
:param color: Line color, the default value is 0xFFFFFF (WHITE)
63+
64+
Note: If dyn_xpitch is True (default), the sparkline will allways span
65+
the complete width. Otherwise, the sparkline will grow when you
66+
add values. Once the line has reached the full width, the sparkline
67+
will scroll to the left.
6268
"""
6369

6470
def __init__(
6571
self,
6672
width: int,
6773
height: int,
6874
max_items: int,
75+
dyn_xpitch: Optional[bool] = True, # True = dynamic pitch size
6976
y_min: Optional[int] = None, # None = autoscaling
7077
y_max: Optional[int] = None, # None = autoscaling
7178
x: int = 0,
@@ -79,6 +86,9 @@ def __init__(
7986
self.color = color #
8087
self._max_items = max_items # maximum number of items in the list
8188
self._spark_list = [] # list containing the values
89+
self.dyn_xpitch = dyn_xpitch
90+
if not dyn_xpitch:
91+
self._xpitch = (width - 1) / (self._max_items - 1)
8292
self.y_min = y_min # minimum of y-axis (None: autoscale)
8393
self.y_max = y_max # maximum of y-axis (None: autoscale)
8494
self.y_bottom = y_min
@@ -89,6 +99,8 @@ def __init__(
8999
# updated if autorange
90100
self._x = x
91101
self._y = y
102+
self._redraw = True # _redraw: redraw primitives
103+
self._last = [] # _last: last point of sparkline
92104

93105
super().__init__(x=x, y=y) # self is a group of lines
94106

@@ -98,6 +110,7 @@ def clear_values(self) -> None:
98110
for _ in range(len(self)): # remove all items from the current group
99111
self.pop()
100112
self._spark_list = [] # empty the list
113+
self._redraw = True
101114

102115
def add_value(self, value: float, update: bool = True) -> None:
103116
"""Add a value to the sparkline.
@@ -115,7 +128,22 @@ def add_value(self, value: float, update: bool = True) -> None:
115128
len(self._spark_list) >= self._max_items
116129
): # if list is full, remove the first item
117130
self._spark_list.pop(0)
131+
self._redraw = True
118132
self._spark_list.append(value)
133+
134+
if self.y_min is None:
135+
self._redraw = self._redraw or value < self.y_bottom
136+
self.y_bottom = (
137+
value if not self.y_bottom else min(value, self.y_bottom)
138+
)
139+
if self.y_max is None:
140+
self._redraw = self._redraw or value > self.y_top
141+
self.y_top = value if not self.y_top else max(value, self.y_top)
142+
143+
# Guard for y_top and y_bottom being the same
144+
if self.y_top == self.y_bottom:
145+
self.y_bottom *= 0.99
146+
119147
if update:
120148
self.update()
121149

@@ -147,107 +175,107 @@ def _plotline(
147175
last_value: float,
148176
x_2: int,
149177
value: float,
150-
y_bottom: int,
151-
y_top: int,
152178
) -> None:
153179

154-
y_2 = int(self.height * (y_top - value) / (y_top - y_bottom))
155-
y_1 = int(self.height * (y_top - last_value) / (y_top - y_bottom))
180+
y_2 = int(self.height * (self.y_top - value) / (self.y_top - self.y_bottom))
181+
y_1 = int(
182+
self.height * (self.y_top - last_value) / (self.y_top - self.y_bottom)
183+
)
156184
self.append(Line(x_1, y_1, x_2, y_2, self.color)) # plot the line
185+
self._last = [x_2, value]
157186

158-
# pylint: disable= too-many-branches, too-many-nested-blocks
187+
# pylint: disable= too-many-branches, too-many-nested-blocks, too-many-locals, too-many-statements
159188

160189
def update(self) -> None:
161190
"""Update the drawing of the sparkline."""
162191

163-
# get the y range
164-
if self.y_min is None:
165-
self.y_bottom = min(self._spark_list)
166-
else:
167-
self.y_bottom = self.y_min
192+
# bail out early if we only have a single point
193+
n_points = len(self._spark_list)
194+
if n_points < 2:
195+
self._last = [0, self._spark_list[0]]
196+
return
168197

169-
if self.y_max is None:
170-
self.y_top = max(self._spark_list)
198+
if self.dyn_xpitch:
199+
# this is a float, only make int when plotting the line
200+
xpitch = (self.width - 1) / (n_points - 1)
201+
self._redraw = True
171202
else:
172-
self.y_top = self.y_max
173-
174-
# Guard for y_top and y_bottom being the same
175-
if self.y_top == self.y_bottom:
176-
self.y_bottom -= 10
177-
self.y_top += 10
178-
179-
if len(self._spark_list) > 2:
180-
xpitch = (self.width - 1) / (
181-
len(self._spark_list) - 1
182-
) # this is a float, only make int when plotting the line
183-
184-
for _ in range(len(self)): # remove all items from the current group
185-
self.pop()
186-
187-
for count, value in enumerate(self._spark_list):
188-
if count == 0:
189-
pass # don't draw anything for a first point
190-
else:
191-
x_2 = int(xpitch * count)
192-
x_1 = int(xpitch * (count - 1))
193-
194-
if (self.y_bottom <= last_value <= self.y_top) and (
195-
self.y_bottom <= value <= self.y_top
196-
): # both points are in range, plot the line
197-
self._plotline(
198-
x_1, last_value, x_2, value, self.y_bottom, self.y_top
199-
)
200-
201-
else: # at least one point is out of range, clip one or both ends the line
202-
if ((last_value > self.y_top) and (value > self.y_top)) or (
203-
(last_value < self.y_bottom) and (value < self.y_bottom)
204-
):
205-
# both points are on the same side out of range: don't draw anything
203+
xpitch = self._xpitch
204+
205+
# only add new segment if redrawing is not necessary
206+
if not self._redraw:
207+
# end of last line (last point, read as "x(-1)")
208+
x_m1 = self._last[0]
209+
y_m1 = self._last[1]
210+
# end of new line (new point, read as "x(0)")
211+
x_0 = int(x_m1 + xpitch)
212+
y_0 = self._spark_list[-1]
213+
self._plotline(x_m1, y_m1, x_0, y_0)
214+
return
215+
216+
self._redraw = False # reset, since we now redraw everything
217+
for _ in range(len(self)): # remove all items from the current group
218+
self.pop()
219+
220+
for count, value in enumerate(self._spark_list):
221+
if count == 0:
222+
pass # don't draw anything for a first point
223+
else:
224+
x_2 = int(xpitch * count)
225+
x_1 = int(xpitch * (count - 1))
226+
227+
if (self.y_bottom <= last_value <= self.y_top) and (
228+
self.y_bottom <= value <= self.y_top
229+
): # both points are in range, plot the line
230+
self._plotline(x_1, last_value, x_2, value)
231+
232+
else: # at least one point is out of range, clip one or both ends the line
233+
if ((last_value > self.y_top) and (value > self.y_top)) or (
234+
(last_value < self.y_bottom) and (value < self.y_bottom)
235+
):
236+
# both points are on the same side out of range: don't draw anything
237+
pass
238+
else:
239+
xint_bottom = self._xintercept(
240+
x_1, last_value, x_2, value, self.y_bottom
241+
) # get possible new x intercept points
242+
xint_top = self._xintercept(
243+
x_1, last_value, x_2, value, self.y_top
244+
) # on the top and bottom of range
245+
if (xint_bottom is None) or (
246+
xint_top is None
247+
): # out of range doublecheck
206248
pass
207249
else:
208-
xint_bottom = self._xintercept(
209-
x_1, last_value, x_2, value, self.y_bottom
210-
) # get possible new x intercept points
211-
xint_top = self._xintercept(
212-
x_1, last_value, x_2, value, self.y_top
213-
) # on the top and bottom of range
214-
215-
if (xint_bottom is None) or (
216-
xint_top is None
217-
): # out of range doublecheck
218-
pass
219-
else:
220-
# Initialize the adjusted values as the baseline
221-
adj_x_1 = x_1
222-
adj_last_value = last_value
223-
adj_x_2 = x_2
224-
adj_value = value
225-
226-
if value > last_value: # slope is positive
227-
if xint_bottom >= x_1: # bottom is clipped
228-
adj_x_1 = xint_bottom
229-
adj_last_value = self.y_bottom # y_1
230-
if xint_top <= x_2: # top is clipped
231-
adj_x_2 = xint_top
232-
adj_value = self.y_top # y_2
233-
else: # slope is negative
234-
if xint_top >= x_1: # top is clipped
235-
adj_x_1 = xint_top
236-
adj_last_value = self.y_top # y_1
237-
if xint_bottom <= x_2: # bottom is clipped
238-
adj_x_2 = xint_bottom
239-
adj_value = self.y_bottom # y_2
240-
241-
self._plotline(
242-
adj_x_1,
243-
adj_last_value,
244-
adj_x_2,
245-
adj_value,
246-
self.y_bottom,
247-
self.y_top,
248-
)
249-
250-
last_value = value # store value for the next iteration
250+
# Initialize the adjusted values as the baseline
251+
adj_x_1 = x_1
252+
adj_last_value = last_value
253+
adj_x_2 = x_2
254+
adj_value = value
255+
256+
if value > last_value: # slope is positive
257+
if xint_bottom >= x_1: # bottom is clipped
258+
adj_x_1 = xint_bottom
259+
adj_last_value = self.y_bottom # y_1
260+
if xint_top <= x_2: # top is clipped
261+
adj_x_2 = xint_top
262+
adj_value = self.y_top # y_2
263+
else: # slope is negative
264+
if xint_top >= x_1: # top is clipped
265+
adj_x_1 = xint_top
266+
adj_last_value = self.y_top # y_1
267+
if xint_bottom <= x_2: # bottom is clipped
268+
adj_x_2 = xint_bottom
269+
adj_value = self.y_bottom # y_2
270+
271+
self._plotline(
272+
adj_x_1,
273+
adj_last_value,
274+
adj_x_2,
275+
adj_value,
276+
)
277+
278+
last_value = value # store value for the next iteration
251279

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

0 commit comments

Comments
 (0)