Skip to content

Commit 5dee81e

Browse files
authored
Merge pull request #14 from kmatch98/master
Add example using line for a moving sparkline graph
2 parents c3bd995 + 7ddf501 commit 5dee81e

File tree

4 files changed

+817
-0
lines changed

4 files changed

+817
-0
lines changed

adafruit_display_shapes/sparkline.py

Lines changed: 218 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,218 @@
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 `
8+
# add_value` are plotted.
9+
#
10+
# The `sparkline` class creates an element suitable for adding to the display using
11+
# `display.show(mySparkline)`
12+
# or adding to a `displayio.Group` to be displayed.
13+
#
14+
# When creating the sparkline, identify the number of `max_items` that will be
15+
# included in the graph. When additional elements are added to the sparkline and
16+
# the number of items has exceeded max_items, any excess values are removed from
17+
# the left of the graph, and new values are added to the right.
18+
"""
19+
`sparkline`
20+
================================================================================
21+
22+
Various common shapes for use with displayio - Sparkline!
23+
24+
25+
* Author(s): Kevin Matocha
26+
27+
Implementation Notes
28+
--------------------
29+
30+
**Software and Dependencies:**
31+
32+
* Adafruit CircuitPython firmware for the supported boards:
33+
https://github.com/adafruit/circuitpython/releases
34+
35+
"""
36+
37+
import displayio
38+
from adafruit_display_shapes.line import Line
39+
40+
41+
class Sparkline(displayio.Group):
42+
# pylint: disable=too-many-arguments
43+
""" A sparkline graph.
44+
45+
: param width: Width of the sparkline graph in pixels
46+
: param height: Height of the sparkline graph in pixels
47+
: param max_items: Maximum number of values housed in the sparkline
48+
: param y_min: Lower range for the y-axis. Set to None for autorange.
49+
: param y_max: Upper range for the y-axis. Set to None for autorange.
50+
: param x: X-position on the screen, in pixels
51+
: param y: Y-position on the screen, in pixels
52+
: param color: Line color, the default value is 0xFFFFFF (WHITE)
53+
"""
54+
55+
def __init__(
56+
self,
57+
width,
58+
height,
59+
max_items,
60+
y_min=None, # None = autoscaling
61+
y_max=None, # None = autoscaling
62+
x=0,
63+
y=0,
64+
color=0xFFFFFF, # line color, default is WHITE
65+
):
66+
67+
# define class instance variables
68+
self.width = width # in pixels
69+
self.height = height # in pixels
70+
self.color = color #
71+
self._max_items = max_items # maximum number of items in the list
72+
self._spark_list = [] # list containing the values
73+
self.y_min = y_min # minimum of y-axis (None: autoscale)
74+
self.y_max = y_max # maximum of y-axis (None: autoscale)
75+
self.y_bottom = y_min
76+
# y_bottom: The actual minimum value of the vertical scale, will be
77+
# updated if autorange
78+
self.y_top = y_max
79+
# y_top: The actual minimum value of the vertical scale, will be
80+
# updated if autorange
81+
self._x = x
82+
self._y = y
83+
84+
super().__init__(
85+
max_size=self._max_items - 1, x=x, y=y
86+
) # self is a group of lines
87+
88+
def add_value(self, value):
89+
""" Add a value to the sparkline.
90+
: param value: The value to be added to the sparkline
91+
"""
92+
93+
if value is not None:
94+
if (
95+
len(self._spark_list) >= self._max_items
96+
): # if list is full, remove the first item
97+
self._spark_list.pop(0)
98+
self._spark_list.append(value)
99+
self.update()
100+
101+
# pylint: disable=no-else-return
102+
@staticmethod
103+
def _xintercept(
104+
x_1, y_1, x_2, y_2, horizontal_y
105+
): # finds intercept of the line and a horizontal line at horizontalY
106+
slope = (y_2 - y_1) / (x_2 - x_1)
107+
b = y_1 - slope * x_1
108+
109+
if slope == 0 and y_1 != horizontal_y: # does not intercept horizontalY
110+
return None
111+
else:
112+
xint = (
113+
horizontal_y - b
114+
) / slope # calculate the x-intercept at position y=horizontalY
115+
return int(xint)
116+
117+
def _plotline(self, x_1, last_value, x_2, value, y_bottom, y_top):
118+
119+
y_2 = int(self.height * (y_top - value) / (y_top - y_bottom))
120+
y_1 = int(self.height * (y_top - last_value) / (y_top - y_bottom))
121+
self.append(Line(x_1, y_1, x_2, y_2, self.color)) # plot the line
122+
123+
# pylint: disable= too-many-branches, too-many-nested-blocks
124+
125+
def update(self):
126+
"""Update the drawing of the sparkline
127+
128+
"""
129+
130+
# get the y range
131+
if self.y_min is None:
132+
self.y_bottom = min(self._spark_list)
133+
else:
134+
self.y_bottom = self.y_min
135+
136+
if self.y_max is None:
137+
self.y_top = max(self._spark_list)
138+
else:
139+
self.y_top = self.y_max
140+
141+
if len(self._spark_list) > 2:
142+
xpitch = self.width / (
143+
len(self._spark_list) - 1
144+
) # this is a float, only make int when plotting the line
145+
146+
for _ in range(len(self)): # remove all items from the current group
147+
self.pop()
148+
149+
for count, value in enumerate(self._spark_list):
150+
if count == 0:
151+
pass # don't draw anything for a first point
152+
else:
153+
x_2 = int(xpitch * count)
154+
x_1 = int(xpitch * (count - 1))
155+
156+
if (self.y_bottom <= last_value <= self.y_top) and (
157+
self.y_bottom <= value <= self.y_top
158+
): # both points are in range, plot the line
159+
self._plotline(
160+
x_1, last_value, x_2, value, self.y_bottom, self.y_top
161+
)
162+
163+
else: # at least one point is out of range, clip one or both ends the line
164+
if ((last_value > self.y_top) and (value > self.y_top)) or (
165+
(last_value < self.y_bottom) and (value < self.y_bottom)
166+
):
167+
# both points are on the same side out of range: don't draw anything
168+
pass
169+
else:
170+
xint_bottom = self._xintercept(
171+
x_1, last_value, x_2, value, self.y_bottom
172+
) # get possible new x intercept points
173+
xint_top = self._xintercept(
174+
x_1, last_value, x_2, value, self.y_top
175+
) # on the top and bottom of range
176+
177+
if (xint_bottom is None) or (
178+
xint_top is None
179+
): # out of range doublecheck
180+
pass
181+
else:
182+
# Initialize the adjusted values as the baseline
183+
adj_x_1 = x_1
184+
adj_last_value = last_value
185+
adj_x_2 = x_2
186+
adj_value = value
187+
188+
if value > last_value: # slope is positive
189+
if xint_bottom >= x_1: # bottom is clipped
190+
adj_x_1 = xint_bottom
191+
adj_last_value = self.y_bottom # y_1
192+
if xint_top <= x_2: # top is clipped
193+
adj_x_2 = xint_top
194+
adj_value = self.y_top # y_2
195+
else: # slope is negative
196+
if xint_top >= x_1: # top is clipped
197+
adj_x_1 = xint_top
198+
adj_last_value = self.y_top # y_1
199+
if xint_bottom <= x_2: # bottom is clipped
200+
adj_x_2 = xint_bottom
201+
adj_value = self.y_bottom # y_2
202+
203+
self._plotline(
204+
adj_x_1,
205+
adj_last_value,
206+
adj_x_2,
207+
adj_value,
208+
self.y_bottom,
209+
self.y_top,
210+
)
211+
212+
last_value = value # store value for the next iteration
213+
214+
def values(self):
215+
"""Returns the values displayed on the sparkline
216+
"""
217+
218+
return self._spark_list
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
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
8+
# `add_value` are plotted.
9+
#
10+
# The `sparkline` class creates an element suitable for adding to the display using
11+
# `display.show(mySparkline)`
12+
# or adding to a `displayio.Group` to be displayed.
13+
#
14+
# When creating the sparkline, identify the number of `max_items` that will be
15+
# included in the graph.
16+
# When additional elements are added to the sparkline and the number of items has
17+
# exceeded max_items, any excess values are removed from the left of the graph,
18+
# and new values are added to the right.
19+
20+
21+
# The following is an example that shows the
22+
23+
# setup display
24+
# instance sparklines
25+
# add to the display
26+
# Loop the following steps:
27+
# add new values to sparkline `add_value`
28+
# update the sparklines `update`
29+
30+
import time
31+
import random
32+
import board
33+
import displayio
34+
35+
36+
from adafruit_display_shapes.sparkline import Sparkline
37+
38+
if "DISPLAY" not in dir(board):
39+
# Setup the LCD display with driver
40+
# You may need to change this to match the display driver for the chipset
41+
# used on your display
42+
from adafruit_ili9341 import ILI9341
43+
44+
displayio.release_displays()
45+
46+
# setup the SPI bus
47+
spi = board.SPI()
48+
tft_cs = board.D9 # arbitrary, pin not used
49+
tft_dc = board.D10
50+
tft_backlight = board.D12
51+
tft_reset = board.D11
52+
53+
while not spi.try_lock():
54+
spi.configure(baudrate=32000000)
55+
56+
spi.unlock()
57+
58+
display_bus = displayio.FourWire(
59+
spi,
60+
command=tft_dc,
61+
chip_select=tft_cs,
62+
reset=tft_reset,
63+
baudrate=32000000,
64+
polarity=1,
65+
phase=1,
66+
)
67+
68+
print("spi.frequency: {}".format(spi.frequency))
69+
70+
# Number of pixels in the display
71+
DISPLAY_WIDTH = 320
72+
DISPLAY_HEIGHT = 240
73+
74+
# create the display
75+
display = ILI9341(
76+
display_bus,
77+
width=DISPLAY_WIDTH,
78+
height=DISPLAY_HEIGHT,
79+
rotation=180, # The rotation can be adjusted to match your configuration.
80+
auto_refresh=True,
81+
native_frames_per_second=90,
82+
)
83+
84+
# reset the display to show nothing.
85+
display.show(None)
86+
else:
87+
# built-in display
88+
display = board.DISPLAY
89+
90+
##########################################
91+
# Create background bitmaps and sparklines
92+
##########################################
93+
94+
# Baseline size of the sparkline chart, in pixels.
95+
chart_width = display.width
96+
chart_height = display.height
97+
98+
# sparkline1 uses a vertical y range between 0 to 10 and will contain a
99+
# maximum of 40 items
100+
sparkline1 = Sparkline(
101+
width=chart_width, height=chart_height, max_items=40, y_min=0, y_max=10, x=0, y=0
102+
)
103+
104+
# Create a group to hold the sparkline and append the sparkline into the
105+
# group (my_group)
106+
#
107+
# Note: In cases where display elements will overlap, then the order the elements
108+
# are added to the group will set which is on top. Latter elements are displayed
109+
# on top of former elemtns.
110+
my_group = displayio.Group(max_size=1)
111+
112+
# add the sparkline into my_group
113+
my_group.append(sparkline1)
114+
115+
116+
# Add my_group (containing the sparkline) to the display
117+
display.show(my_group)
118+
119+
# Start the main loop
120+
while True:
121+
122+
# turn off the auto_refresh of the display while modifying the sparkline
123+
display.auto_refresh = False
124+
125+
# add_value: add a new value to a sparkline
126+
# Note: The y-range for mySparkline1 is set to 0 to 10, so all these random
127+
# values (between 0 and 10) will fit within the visible range of this sparkline
128+
sparkline1.add_value(random.uniform(0, 10))
129+
130+
# turn the display auto_refresh back on
131+
display.auto_refresh = True
132+
133+
# The display seems to be less jittery if a small sleep time is provided
134+
# You can adjust this to see if it has any effect
135+
time.sleep(0.01)

0 commit comments

Comments
 (0)