@@ -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,7 @@ def __init__(
8999 # updated if autorange
90100 self ._x = x
91101 self ._y = y
102+ self ._redraw = True # _redraw: redraw primitives
92103
93104 super ().__init__ (x = x , y = y ) # self is a group of lines
94105
@@ -98,6 +109,7 @@ def clear_values(self) -> None:
98109 for _ in range (len (self )): # remove all items from the current group
99110 self .pop ()
100111 self ._spark_list = [] # empty the list
112+ self ._redraw = True
101113
102114 def add_value (self , value : float , update : bool = True ) -> None :
103115 """Add a value to the sparkline.
@@ -114,7 +126,22 @@ def add_value(self, value: float, update: bool = True) -> None:
114126 len (self ._spark_list ) >= self ._max_items
115127 ): # if list is full, remove the first item
116128 self ._spark_list .pop (0 )
129+ self ._redraw = True
117130 self ._spark_list .append (value )
131+
132+ if self .y_min is None :
133+ self ._redraw = self ._redraw or value < self .y_bottom
134+ self .y_bottom = (
135+ value if not self .y_bottom else min (value , self .y_bottom )
136+ )
137+ if self .y_max is None :
138+ self ._redraw = self ._redraw or value > self .y_top
139+ self .y_top = value if not self .y_top else max (value , self .y_top )
140+
141+ # Guard for y_top and y_bottom being the same
142+ if self .y_top == self .y_bottom :
143+ self .y_bottom *= 0.99
144+
118145 if update :
119146 self .update ()
120147
@@ -146,107 +173,109 @@ def _plotline(
146173 last_value : float ,
147174 x_2 : int ,
148175 value : float ,
149- y_bottom : int ,
150- y_top : int ,
151176 ) -> None :
152177
153- y_2 = int (self .height * (y_top - value ) / (y_top - y_bottom ))
154- y_1 = int (self .height * (y_top - last_value ) / (y_top - y_bottom ))
178+ y_2 = int (self .height * (self .y_top - value ) / (self .y_top - self .y_bottom ))
179+ y_1 = int (
180+ self .height * (self .y_top - last_value ) / (self .y_top - self .y_bottom )
181+ )
155182 self .append (Line (x_1 , y_1 , x_2 , y_2 , self .color )) # plot the line
183+ self ._last = [x_2 , value ]
156184
157185 # pylint: disable= too-many-branches, too-many-nested-blocks
158186
159187 def update (self ) -> None :
160188 """Update the drawing of the sparkline."""
161189
162- # get the y range
163- if self .y_min is None :
164- self . y_bottom = min ( self . _spark_list )
165- else :
166- self . y_bottom = self . y_min
190+ # bail out early if we only have a single point
191+ n_points = len ( self ._spark_list )
192+ if n_points < 2 :
193+ self . _last = [ 0 , self . _spark_list [ 0 ]]
194+ return
167195
168- if self .y_max is None :
169- self .y_top = max (self ._spark_list )
196+ if self .dyn_xpitch :
197+ # this is a float, only make int when plotting the line
198+ xpitch = (self .width - 1 ) / (n_points - 1 )
199+ self ._redraw = True
170200 else :
171- self .y_top = self .y_max
172-
173- # Guard for y_top and y_bottom being the same
174- if self .y_top == self .y_bottom :
175- self .y_bottom -= 10
176- self .y_top += 10
177-
178- if len (self ._spark_list ) > 2 :
179- xpitch = (self .width - 1 ) / (
180- len (self ._spark_list ) - 1
181- ) # this is a float, only make int when plotting the line
182-
183- for _ in range (len (self )): # remove all items from the current group
184- self .pop ()
185-
186- for count , value in enumerate (self ._spark_list ):
187- if count == 0 :
188- pass # don't draw anything for a first point
189- else :
190- x_2 = int (xpitch * count )
191- x_1 = int (xpitch * (count - 1 ))
192-
193- if (self .y_bottom <= last_value <= self .y_top ) and (
194- self .y_bottom <= value <= self .y_top
195- ): # both points are in range, plot the line
196- self ._plotline (
197- x_1 , last_value , x_2 , value , self .y_bottom , self .y_top
198- )
199-
200- else : # at least one point is out of range, clip one or both ends the line
201- if ((last_value > self .y_top ) and (value > self .y_top )) or (
202- (last_value < self .y_bottom ) and (value < self .y_bottom )
203- ):
204- # both points are on the same side out of range: don't draw anything
201+ xpitch = self ._xpitch
202+
203+ # only add new segment if redrawing is not necessary
204+ if not self ._redraw :
205+ # end of last line (last point, read as "x(-1)")
206+ x_m1 = self ._last [0 ]
207+ y_m1 = self ._last [1 ]
208+ # end of new line (new point, read as "x(0)")
209+ x_0 = int (x_m1 + xpitch )
210+ y_0 = self ._spark_list [- 1 ]
211+ self ._plotline (x_m1 , y_m1 , x_0 , y_0 )
212+ return
213+
214+ self ._redraw = False # reset, since we now redraw everything
215+ for _ in range (len (self )): # remove all items from the current group
216+ self .pop ()
217+
218+ for count , value in enumerate (self ._spark_list ):
219+ if count == 0 :
220+ pass # don't draw anything for a first point
221+ else :
222+ x_2 = int (xpitch * count )
223+ x_1 = int (xpitch * (count - 1 ))
224+
225+ if (self .y_bottom <= last_value <= self .y_top ) and (
226+ self .y_bottom <= value <= self .y_top
227+ ): # both points are in range, plot the line
228+ self ._plotline (x_1 , last_value , x_2 , value )
229+
230+ else : # at least one point is out of range, clip one or both ends the line
231+ if ((last_value > self .y_top ) and (value > self .y_top )) or (
232+ (last_value < self .y_bottom ) and (value < self .y_bottom )
233+ ):
234+ # both points are on the same side out of range: don't draw anything
235+ pass
236+ else :
237+ xint_bottom = self ._xintercept (
238+ x_1 , last_value , x_2 , value , self .y_bottom
239+ ) # get possible new x intercept points
240+ xint_top = self ._xintercept (
241+ x_1 , last_value , x_2 , value , self .y_top
242+ ) # on the top and bottom of range
243+ if (xint_bottom is None ) or (
244+ xint_top is None
245+ ): # out of range doublecheck
205246 pass
206247 else :
207- xint_bottom = self ._xintercept (
208- x_1 , last_value , x_2 , value , self .y_bottom
209- ) # get possible new x intercept points
210- xint_top = self ._xintercept (
211- x_1 , last_value , x_2 , value , self .y_top
212- ) # on the top and bottom of range
213-
214- if (xint_bottom is None ) or (
215- xint_top is None
216- ): # out of range doublecheck
217- pass
218- else :
219- # Initialize the adjusted values as the baseline
220- adj_x_1 = x_1
221- adj_last_value = last_value
222- adj_x_2 = x_2
223- adj_value = value
224-
225- if value > last_value : # slope is positive
226- if xint_bottom >= x_1 : # bottom is clipped
227- adj_x_1 = xint_bottom
228- adj_last_value = self .y_bottom # y_1
229- if xint_top <= x_2 : # top is clipped
230- adj_x_2 = xint_top
231- adj_value = self .y_top # y_2
232- else : # slope is negative
233- if xint_top >= x_1 : # top is clipped
234- adj_x_1 = xint_top
235- adj_last_value = self .y_top # y_1
236- if xint_bottom <= x_2 : # bottom is clipped
237- adj_x_2 = xint_bottom
238- adj_value = self .y_bottom # y_2
239-
240- self ._plotline (
241- adj_x_1 ,
242- adj_last_value ,
243- adj_x_2 ,
244- adj_value ,
245- self .y_bottom ,
246- self .y_top ,
247- )
248-
249- last_value = value # store value for the next iteration
248+ # Initialize the adjusted values as the baseline
249+ adj_x_1 = x_1
250+ adj_last_value = last_value
251+ adj_x_2 = x_2
252+ adj_value = value
253+
254+ if value > last_value : # slope is positive
255+ if xint_bottom >= x_1 : # bottom is clipped
256+ adj_x_1 = xint_bottom
257+ adj_last_value = self .y_bottom # y_1
258+ if xint_top <= x_2 : # top is clipped
259+ adj_x_2 = xint_top
260+ adj_value = self .y_top # y_2
261+ else : # slope is negative
262+ if xint_top >= x_1 : # top is clipped
263+ adj_x_1 = xint_top
264+ adj_last_value = self .y_top # y_1
265+ if xint_bottom <= x_2 : # bottom is clipped
266+ adj_x_2 = xint_bottom
267+ adj_value = self .y_bottom # y_2
268+
269+ self ._plotline (
270+ adj_x_1 ,
271+ adj_last_value ,
272+ adj_x_2 ,
273+ adj_value ,
274+ self .y_bottom ,
275+ self .y_top ,
276+ )
277+
278+ last_value = value # store value for the next iteration
250279
251280 def values (self ) -> List [float ]:
252281 """Returns the values displayed on the sparkline."""
0 commit comments