Skip to content

Commit 46bceae

Browse files
author
Margaret Matocha
committed
Separated class file, updated examples
1 parent 6e181df commit 46bceae

5 files changed

+758
-338
lines changed

adafruit_display_shapes/sparkline.py

Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
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+
from adafruit_display_shapes.line import Line
18+
19+
20+
class Sparkline(displayio.Group):
21+
def __init__(
22+
self,
23+
width,
24+
height,
25+
max_items,
26+
yMin=None, # None = autoscaling
27+
yMax=None, # None = autoscaling
28+
x=0,
29+
y=0,
30+
color=0xFFFFFF, # line color, default is WHITE
31+
):
32+
33+
# define class instance variables
34+
self.width = width # in pixels
35+
self.height = height # in pixels
36+
self.color = color #
37+
self._max_items = max_items # maximum number of items in the list
38+
self._spark_list = [] # list containing the values
39+
self.yMin = yMin # minimum of y-axis (None: autoscale)
40+
self.yMax = yMax # maximum of y-axis (None: autoscale)
41+
self.yBottom = yMin # yBottom: The actual minimum value of the vertical scale, will be updated if autorange
42+
self.yTop = yMax # yTop: The actual minimum value of the vertical scale, will be updated if autorange
43+
self._x = x
44+
self._y = y
45+
46+
super().__init__(
47+
max_size=self._max_items - 1, x=x, y=y
48+
) # self is a group of lines
49+
50+
def add_value(self, value):
51+
if value is not None:
52+
if (
53+
len(self._spark_list) >= self._max_items
54+
): # if list is full, remove the first item
55+
self._spark_list.pop(0)
56+
self._spark_list.append(value)
57+
# self.update()
58+
59+
@staticmethod
60+
def _xintercept(
61+
x1, y1, x2, y2, horizontalY
62+
): # finds intercept of the line and a horizontal line at horizontalY
63+
slope = (y2 - y1) / (x2 - x1)
64+
b = y1 - slope * x1
65+
66+
if slope == 0 and y1 != horizontalY: # does not intercept horizontalY
67+
return None
68+
else:
69+
xint = (
70+
horizontalY - b
71+
) / slope # calculate the x-intercept at position y=horizontalY
72+
return int(xint)
73+
74+
def _plotLine(self, x1, last_value, x2, value, yBottom, yTop):
75+
76+
y2 = int(self.height * (yTop - value) / (yTop - yBottom))
77+
y1 = int(self.height * (yTop - last_value) / (yTop - yBottom))
78+
self.append(Line(x1, y1, x2, y2, self.color)) # plot the line
79+
80+
def update(self):
81+
# What to do if there is 0 or 1 element?
82+
83+
# get the y range
84+
if self.yMin == None:
85+
self.yBottom = min(self._spark_list)
86+
else:
87+
self.yBottom = self.yMin
88+
89+
if self.yMax == None:
90+
self.yTop = max(self._spark_list)
91+
else:
92+
self.yTop = self.yMax
93+
94+
if len(self._spark_list) > 2:
95+
xpitch = self.width / (
96+
len(self._spark_list) - 1
97+
) # this is a float, only make int when plotting the line
98+
99+
for i in range(len(self)): # remove all items from the current group
100+
self.pop()
101+
102+
for count, value in enumerate(self._spark_list):
103+
if count == 0:
104+
pass # don't draw anything for a first point
105+
else:
106+
x2 = int(xpitch * count)
107+
x1 = int(xpitch * (count - 1))
108+
109+
# print("x1: {}, x2: {}".format(x1,x2))
110+
111+
if (self.yBottom <= last_value <= self.yTop) and (
112+
self.yBottom <= value <= self.yTop
113+
): # both points are in range, plot the line
114+
self._plotLine(
115+
x1, last_value, x2, value, self.yBottom, self.yTop
116+
)
117+
118+
else: # at least one point is out of range, clip one or both ends the line
119+
if ((last_value > self.yTop) and (value > self.yTop)) or (
120+
(last_value < self.yBottom) and (value < self.yBottom)
121+
):
122+
# both points are on the same side out of range: don't draw anything
123+
pass
124+
else:
125+
xintBottom = self._xintercept(
126+
x1, last_value, x2, value, self.yBottom
127+
) # get possible new x intercept points
128+
xintTop = self._xintercept(
129+
x1, last_value, x2, value, self.yTop
130+
) # on the top and bottom of range
131+
132+
if (xintBottom is None) or (
133+
xintTop is None
134+
): # out of range doublecheck
135+
pass
136+
else:
137+
# Initialize the adjusted values as the baseline
138+
adj_x1 = x1
139+
adj_last_value = last_value
140+
adj_x2 = x2
141+
adj_value = value
142+
143+
if value > last_value: # slope is positive
144+
if xintBottom >= x1: # bottom is clipped
145+
adj_x1 = xintBottom
146+
adj_last_value = self.yBottom # y1
147+
if xintTop <= x2: # top is clipped
148+
adj_x2 = xintTop
149+
adj_value = self.yTop # y2
150+
else: # slope is negative
151+
if xintTop >= x1: # top is clipped
152+
adj_x1 = xintTop
153+
adj_last_value = self.yTop # y1
154+
if xintBottom <= x2: # bottom is clipped
155+
adj_x2 = xintBottom
156+
adj_value = self.yBottom # y2
157+
158+
self._plotLine(
159+
adj_x1,
160+
adj_last_value,
161+
adj_x2,
162+
adj_value,
163+
self.yBottom,
164+
self.yTop,
165+
)
166+
167+
last_value = value # store value for the next iteration
168+
169+
def values(self):
170+
return self._spark_list

0 commit comments

Comments
 (0)