Skip to content

Commit 6e181df

Browse files
author
Margaret Matocha
committed
Add example using line for a moving sparkline graph
1 parent 6b64c9f commit 6e181df

File tree

1 file changed

+338
-0
lines changed

1 file changed

+338
-0
lines changed

examples/display_shapes_sparkline.py

Lines changed: 338 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,338 @@
1+
# class of sparklines in CircuitPython
2+
# created by Kevin Matocha - Copyright 2020 (C)
3+
4+
# See the bottom for a code example using the `sparkline` Class.
5+
6+
# # File: display_shapes_sparkline.py
7+
# A sparkline is a scrolling line graph, where any values added to sparkline using `add_value` are plotted.
8+
#
9+
# The `sparkline` class creates an element suitable for adding to the display using `display.show(mySparkline)`
10+
# or adding to a `displayio.Group` to be displayed.
11+
#
12+
# When creating the sparkline, identify the number of `max_items` that will be included in the graph.
13+
# When additional elements are added to the sparkline and the number of items has exceeded max_items,
14+
# any excess values are removed from the left of the graph, and new values are added to the right.
15+
16+
import displayio
17+
18+
19+
class sparkline(displayio.Group):
20+
def __init__(
21+
self,
22+
width,
23+
height,
24+
max_items,
25+
yMin=None, # None = autoscaling
26+
yMax=None, # None = autoscaling
27+
line_color=0xFFFFFF, # default = WHITE
28+
x=0,
29+
y=0,
30+
):
31+
32+
# define class instance variables
33+
self.width = width # in pixels
34+
self.height = height # in pixels
35+
self.line_color = line_color #
36+
self._max_items = max_items # maximum number of items in the list
37+
self._spark_list = [] # list containing the values
38+
self.yMin = yMin # minimum of y-axis (None: autoscale)
39+
self.yMax = yMax # maximum of y-axis (None: autoscale)
40+
self._x = x
41+
self._y = y
42+
43+
super().__init__(
44+
max_size=self._max_items - 1, x=x, y=y
45+
) # self is a group of lines
46+
47+
def add_value(self, value):
48+
if value is not None:
49+
if (
50+
len(self._spark_list) >= self._max_items
51+
): # if list is full, remove the first item
52+
self._spark_list.pop(0)
53+
self._spark_list.append(value)
54+
# self.update()
55+
56+
@staticmethod
57+
def _xintercept(
58+
x1, y1, x2, y2, horizontalY
59+
): # finds intercept of the line and a horizontal line at horizontalY
60+
slope = (y2 - y1) / (x2 - x1)
61+
b = y1 - slope * x1
62+
63+
if slope == 0 and y1 != horizontalY: # does not intercept horizontalY
64+
return None
65+
else:
66+
xint = (
67+
horizontalY - b
68+
) / slope # calculate the x-intercept at position y=horizontalY
69+
return int(xint)
70+
71+
def _plotLine(self, x1, last_value, x2, value, yBottom, yTop):
72+
73+
from adafruit_display_shapes.line import Line
74+
75+
y2 = int(self.height * (yTop - value) / (yTop - yBottom))
76+
y1 = int(self.height * (yTop - last_value) / (yTop - yBottom))
77+
self.append(Line(x1, y1, x2, y2, self.line_color)) # plot the line
78+
79+
def update(self):
80+
# What to do if there is 0 or 1 element?
81+
82+
# get the y range
83+
if self.yMin == None:
84+
yBottom = min(self._spark_list)
85+
else:
86+
yBottom = self.yMin
87+
88+
if self.yMax == None:
89+
yTop = max(self._spark_list)
90+
else:
91+
yTop = self.yMax
92+
93+
if len(self._spark_list) > 2:
94+
xpitch = self.width / (
95+
len(self._spark_list) - 1
96+
) # this is a float, only make int when plotting the line
97+
98+
for i in range(len(self)): # remove all items from the current group
99+
self.pop()
100+
101+
for count, value in enumerate(self._spark_list):
102+
if count == 0:
103+
pass # don't draw anything for a first point
104+
else:
105+
x2 = int(xpitch * count)
106+
x1 = int(xpitch * (count - 1))
107+
108+
# print("x1: {}, x2: {}".format(x1,x2))
109+
110+
if (yBottom <= last_value <= yTop) and (
111+
yBottom <= value <= yTop
112+
): # both points are in range, plot the line
113+
self._plotLine(x1, last_value, x2, value, yBottom, yTop)
114+
115+
else: # at least one point is out of range, clip one or both ends the line
116+
if ((last_value > yTop) and (value > yTop)) or (
117+
(last_value < yBottom) and (value < yBottom)
118+
):
119+
# both points are on the same side out of range: don't draw anything
120+
pass
121+
else:
122+
xintBottom = self._xintercept(
123+
x1, last_value, x2, value, yBottom
124+
) # get possible new x intercept points
125+
xintTop = self._xintercept(
126+
x1, last_value, x2, value, yTop
127+
) # on the top and bottom of range
128+
129+
if (xintBottom is None) or (
130+
xintTop is None
131+
): # out of range doublecheck
132+
pass
133+
else:
134+
# Initialize the adjusted values as the baseline
135+
adj_x1 = x1
136+
adj_last_value = last_value
137+
adj_x2 = x2
138+
adj_value = value
139+
140+
if value > last_value: # slope is positive
141+
if xintBottom >= x1: # bottom is clipped
142+
adj_x1 = xintBottom
143+
adj_last_value = yBottom # y1
144+
if xintTop <= x2: # top is clipped
145+
adj_x2 = xintTop
146+
adj_value = yTop # y2
147+
else: # slope is negative
148+
if xintTop >= x1: # top is clipped
149+
adj_x1 = xintTop
150+
adj_last_value = yTop # y1
151+
if xintBottom <= x2: # bottom is clipped
152+
adj_x2 = xintBottom
153+
adj_value = yBottom # y2
154+
155+
self._plotLine(
156+
adj_x1,
157+
adj_last_value,
158+
adj_x2,
159+
adj_value,
160+
yBottom,
161+
yTop,
162+
)
163+
164+
last_value = value # store value for the next iteration
165+
166+
def values(self):
167+
return self._spark_list
168+
169+
170+
# The following is an example that shows the
171+
172+
# setup display
173+
# instance sparklines
174+
# add to the display
175+
# Loop the following steps:
176+
# add new values to sparkline `add_value`
177+
# update the sparklines `update`
178+
179+
import board
180+
import displayio
181+
import random
182+
import time
183+
from adafruit_ili9341 import ILI9341
184+
185+
# from sparkline import sparkline # use this if sparkline.py is used to define the sparkline Class
186+
187+
188+
# Setup the LCD display
189+
190+
displayio.release_displays()
191+
192+
193+
# setup the SPI bus
194+
spi = board.SPI()
195+
tft_cs = board.D9 # arbitrary, pin not used
196+
tft_dc = board.D10
197+
tft_backlight = board.D12
198+
tft_reset = board.D11
199+
200+
while not spi.try_lock():
201+
spi.configure(baudrate=32000000)
202+
pass
203+
spi.unlock()
204+
205+
display_bus = displayio.FourWire(
206+
spi,
207+
command=tft_dc,
208+
chip_select=tft_cs,
209+
reset=tft_reset,
210+
baudrate=32000000,
211+
polarity=1,
212+
phase=1,
213+
)
214+
215+
print("spi.frequency: {}".format(spi.frequency))
216+
217+
# Number of pixels in the display
218+
DISPLAY_WIDTH = 320
219+
DISPLAY_HEIGHT = 240
220+
221+
# create the display
222+
display = ILI9341(
223+
display_bus,
224+
width=DISPLAY_WIDTH,
225+
height=DISPLAY_HEIGHT,
226+
rotation=180,
227+
auto_refresh=True,
228+
native_frames_per_second=90,
229+
)
230+
231+
# reset the display to show nothing.
232+
display.show(None)
233+
234+
##########################################
235+
# Create background bitmaps and sparklines
236+
##########################################
237+
238+
# Baseline size of the sparkline chart, in pixels.
239+
chartWidth = 50
240+
chartHeight = 50
241+
242+
# Setup the first bitmap and sparkline
243+
palette = displayio.Palette(1) # color palette used for bitmap (one color)
244+
palette[0] = 0x444411
245+
246+
bitmap = displayio.Bitmap(chartWidth, chartHeight, 1) # create a bitmap
247+
tileGrid = displayio.TileGrid(
248+
bitmap, pixel_shader=palette, x=10, y=10
249+
) # Add the bitmap to tilegrid
250+
mySparkline = sparkline(
251+
width=chartWidth, height=chartHeight, max_items=40, yMin=-1, yMax=1.25, x=10, y=10
252+
)
253+
# mySparkline uses a vertical y range between -1 to +1.25 and will contain a maximum of 40 items
254+
255+
# Setup the second bitmap and sparkline
256+
palette2 = displayio.Palette(1) # color palette used for bitmap2 (one color)
257+
palette2[0] = 0x0000FF
258+
259+
bitmap2 = displayio.Bitmap(chartWidth * 2, chartHeight * 2, 1) # create bitmap2
260+
tileGrid2 = displayio.TileGrid(
261+
bitmap2, pixel_shader=palette2, x=150, y=10
262+
) # Add bitmap2 to tilegrid2
263+
mySparkline2 = sparkline(
264+
width=chartWidth * 2,
265+
height=chartHeight * 2,
266+
max_items=10,
267+
yMin=0,
268+
yMax=1,
269+
x=150,
270+
y=10,
271+
line_color=0xFF00FF,
272+
)
273+
# mySparkline2 uses a vertical y range between 0 to 1, and will contain a maximum of 10 items
274+
275+
# Setup the third bitmap and sparkline
276+
palette3 = displayio.Palette(1) # color palette used for bitmap (one color)
277+
palette3[0] = 0x11FF44
278+
bitmap3 = displayio.Bitmap(DISPLAY_WIDTH, chartHeight * 2, 1) # create bitmap3
279+
tileGrid3 = displayio.TileGrid(
280+
bitmap3, pixel_shader=palette3, x=0, y=120
281+
) # Add bitmap3 to tilegrid3
282+
mySparkline3 = sparkline(
283+
width=DISPLAY_WIDTH,
284+
height=chartHeight * 2,
285+
max_items=20,
286+
x=0,
287+
y=120,
288+
line_color=0xFFFFFF,
289+
)
290+
# mySparkline3 will contain a maximum of 20 items
291+
# since yMin and yMax are not specified, mySparkline3 uses autoranging for both the top and bottom ranges.
292+
# Note: Any unspecified edge limit (yMin, yMax) will autorange that edge based on the data in the list.
293+
294+
295+
# Create a group to hold the three bitmap TileGrids and the three sparklines and
296+
# append them into the group (myGroup)
297+
#
298+
# Note: In cases where display elements will overlap, then the order the elements are added to the
299+
# group will set which is on top. Latter elements are displayed on top of former elemtns.
300+
myGroup = displayio.Group(max_size=8)
301+
myGroup.append(tileGrid)
302+
myGroup.append(mySparkline)
303+
304+
myGroup.append(tileGrid2)
305+
myGroup.append(mySparkline2)
306+
307+
myGroup.append(tileGrid3)
308+
myGroup.append(mySparkline3)
309+
310+
311+
# Display myGroup that contains all the bitmap TileGrids and sparklines
312+
display.show(myGroup)
313+
314+
# Start the main loop
315+
while True:
316+
317+
# add a new random value to each sparkline
318+
mySparkline.add_value(random.uniform(0, 1))
319+
320+
mySparkline2.add_value(random.uniform(-1, 2))
321+
# Note: For mySparline2, the random value will sometimes
322+
# be out of the y-range to exercise the top and bottom clipping
323+
324+
mySparkline3.add_value(random.uniform(0, 1))
325+
326+
# Turn off the display refresh while updating the sparklines
327+
display.auto_refresh = False
328+
329+
# Update the drawings for all three sparklines
330+
mySparkline.update()
331+
mySparkline2.update()
332+
mySparkline3.update()
333+
334+
# Turn on the display refreshing
335+
display.auto_refresh = True
336+
337+
# The display seems to be less jittery if a small sleep time is provided
338+
time.sleep(0.1)

0 commit comments

Comments
 (0)