@@ -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,7 @@ def __init__(
89
99
# updated if autorange
90
100
self ._x = x
91
101
self ._y = y
102
+ self ._redraw = True # _redraw: redraw primitives
92
103
93
104
super ().__init__ (x = x , y = y ) # self is a group of lines
94
105
@@ -98,6 +109,7 @@ def clear_values(self) -> None:
98
109
for _ in range (len (self )): # remove all items from the current group
99
110
self .pop ()
100
111
self ._spark_list = [] # empty the list
112
+ self ._redraw = True
101
113
102
114
def add_value (self , value : float , update : bool = True ) -> None :
103
115
"""Add a value to the sparkline.
@@ -114,7 +126,22 @@ def add_value(self, value: float, update: bool = True) -> None:
114
126
len (self ._spark_list ) >= self ._max_items
115
127
): # if list is full, remove the first item
116
128
self ._spark_list .pop (0 )
129
+ self ._redraw = True
117
130
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
+
118
145
if update :
119
146
self .update ()
120
147
@@ -146,107 +173,109 @@ def _plotline(
146
173
last_value : float ,
147
174
x_2 : int ,
148
175
value : float ,
149
- y_bottom : int ,
150
- y_top : int ,
151
176
) -> None :
152
177
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
+ )
155
182
self .append (Line (x_1 , y_1 , x_2 , y_2 , self .color )) # plot the line
183
+ self ._last = [x_2 , value ]
156
184
157
185
# pylint: disable= too-many-branches, too-many-nested-blocks
158
186
159
187
def update (self ) -> None :
160
188
"""Update the drawing of the sparkline."""
161
189
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
167
195
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
170
200
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
205
246
pass
206
247
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
250
279
251
280
def values (self ) -> List [float ]:
252
281
"""Returns the values displayed on the sparkline."""
0 commit comments