@@ -54,18 +54,25 @@ class Sparkline(displayio.Group):
54
54
:param width: Width of the sparkline graph in pixels
55
55
:param height: Height of the sparkline graph in pixels
56
56
:param max_items: Maximum number of values housed in the sparkline
57
+ :param dyn_xpitch: dynamically change xpitch (True)
57
58
:param y_min: Lower range for the y-axis. Set to None for autorange.
58
59
:param y_max: Upper range for the y-axis. Set to None for autorange.
59
60
:param x: X-position on the screen, in pixels
60
61
:param y: Y-position on the screen, in pixels
61
62
: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.
62
68
"""
63
69
64
70
def __init__ (
65
71
self ,
66
72
width : int ,
67
73
height : int ,
68
74
max_items : int ,
75
+ dyn_xpitch : Optional [bool ] = True , # True = dynamic pitch size
69
76
y_min : Optional [int ] = None , # None = autoscaling
70
77
y_max : Optional [int ] = None , # None = autoscaling
71
78
x : int = 0 ,
@@ -79,6 +86,9 @@ def __init__(
79
86
self .color = color #
80
87
self ._max_items = max_items # maximum number of items in the list
81
88
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 )
82
92
self .y_min = y_min # minimum of y-axis (None: autoscale)
83
93
self .y_max = y_max # maximum of y-axis (None: autoscale)
84
94
self .y_bottom = y_min
@@ -89,6 +99,8 @@ def __init__(
89
99
# updated if autorange
90
100
self ._x = x
91
101
self ._y = y
102
+ self ._redraw = True # _redraw: redraw primitives
103
+ self ._last = [] # _last: last point of sparkline
92
104
93
105
super ().__init__ (x = x , y = y ) # self is a group of lines
94
106
@@ -98,6 +110,7 @@ def clear_values(self) -> None:
98
110
for _ in range (len (self )): # remove all items from the current group
99
111
self .pop ()
100
112
self ._spark_list = [] # empty the list
113
+ self ._redraw = True
101
114
102
115
def add_value (self , value : float , update : bool = True ) -> None :
103
116
"""Add a value to the sparkline.
@@ -115,7 +128,22 @@ def add_value(self, value: float, update: bool = True) -> None:
115
128
len (self ._spark_list ) >= self ._max_items
116
129
): # if list is full, remove the first item
117
130
self ._spark_list .pop (0 )
131
+ self ._redraw = True
118
132
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
+
119
147
if update :
120
148
self .update ()
121
149
@@ -147,107 +175,107 @@ def _plotline(
147
175
last_value : float ,
148
176
x_2 : int ,
149
177
value : float ,
150
- y_bottom : int ,
151
- y_top : int ,
152
178
) -> None :
153
179
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
+ )
156
184
self .append (Line (x_1 , y_1 , x_2 , y_2 , self .color )) # plot the line
185
+ self ._last = [x_2 , value ]
157
186
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
159
188
160
189
def update (self ) -> None :
161
190
"""Update the drawing of the sparkline."""
162
191
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
168
197
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
171
202
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
206
248
pass
207
249
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
251
279
252
280
def values (self ) -> List [float ]:
253
281
"""Returns the values displayed on the sparkline."""
0 commit comments