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