Skip to content

Add example using line for a moving sparkline graph #14

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 25 commits into from
Jul 22, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
6e181df
Add example using line for a moving sparkline graph
Jun 7, 2020
46bceae
Separated class file, updated examples
Jul 21, 2020
2ba35db
update the simpletest to generalize the display setup
FoamyGuy Jul 21, 2020
9784b7b
Merge pull request #1 from FoamyGuy/generalize_display_in_examples
kmatch98 Jul 22, 2020
8de933e
Deleted unnecessary import in sparkline_simpletest
Jul 22, 2020
0a70198
Set for auto update upon add_value, updated examples with display che…
Jul 22, 2020
28dc4ae
Ran black on examples
Jul 22, 2020
81054a6
Merge pull request #2 from kmatch98/auto_update
kmatch98 Jul 22, 2020
7fe45cb
Updates for pylint, ran black
Jul 22, 2020
0b0ba27
Merge pull request #3 from kmatch98/auto_update
kmatch98 Jul 22, 2020
bf0ac9e
updated for pylint
Jul 22, 2020
d43ce23
Merge pull request #4 from kmatch98/auto_update
kmatch98 Jul 22, 2020
deb9a2c
pylint fixes
Jul 22, 2020
48123a0
Merge pull request #5 from kmatch98/auto_update
kmatch98 Jul 22, 2020
3367ba5
Updated examples for pylint
Jul 22, 2020
525a48a
Merge pull request #6 from kmatch98/auto_update
kmatch98 Jul 22, 2020
a972ae5
Ran black
Jul 22, 2020
1234d0a
Merge pull request #7 from kmatch98/auto_update
kmatch98 Jul 22, 2020
cf9551c
Pylint updates and ran black
Jul 22, 2020
2141e5d
Merge pull request #8 from kmatch98/auto_update
kmatch98 Jul 22, 2020
0289b8a
sparkline.py Updated camelCase to snake_case
Jul 22, 2020
ab90cc1
More updates of snake_case
Jul 22, 2020
d7dcab7
updated examples camelCase to snake_case to match pylint requirements
Jul 22, 2020
38cd21b
deleted one more dispable=invalid-name
Jul 22, 2020
7ddf501
Updated _triple to define DISPLAY_WIDTH for pyPortal else statement
Jul 22, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
218 changes: 218 additions & 0 deletions adafruit_display_shapes/sparkline.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,218 @@
# class of sparklines in CircuitPython
# created by Kevin Matocha - Copyright 2020 (C)

# See the bottom for a code example using the `sparkline` Class.

# # File: display_shapes_sparkline.py
# A sparkline is a scrolling line graph, where any values added to sparkline using `
# add_value` are plotted.
#
# The `sparkline` class creates an element suitable for adding to the display using
# `display.show(mySparkline)`
# or adding to a `displayio.Group` to be displayed.
#
# When creating the sparkline, identify the number of `max_items` that will be
# included in the graph. When additional elements are added to the sparkline and
# the number of items has exceeded max_items, any excess values are removed from
# the left of the graph, and new values are added to the right.
"""
`sparkline`
================================================================================

Various common shapes for use with displayio - Sparkline!


* Author(s): Kevin Matocha

Implementation Notes
--------------------

**Software and Dependencies:**

* Adafruit CircuitPython firmware for the supported boards:
https://github.com/adafruit/circuitpython/releases

"""

import displayio
from adafruit_display_shapes.line import Line


class Sparkline(displayio.Group):
# pylint: disable=too-many-arguments
""" A sparkline graph.

: param width: Width of the sparkline graph in pixels
: param height: Height of the sparkline graph in pixels
: param max_items: Maximum number of values housed in the sparkline
: param y_min: Lower range for the y-axis. Set to None for autorange.
: param y_max: Upper range for the y-axis. Set to None for autorange.
: param x: X-position on the screen, in pixels
: param y: Y-position on the screen, in pixels
: param color: Line color, the default value is 0xFFFFFF (WHITE)
"""

def __init__(
self,
width,
height,
max_items,
y_min=None, # None = autoscaling
y_max=None, # None = autoscaling
x=0,
y=0,
color=0xFFFFFF, # line color, default is WHITE
):

# define class instance variables
self.width = width # in pixels
self.height = height # in pixels
self.color = color #
self._max_items = max_items # maximum number of items in the list
self._spark_list = [] # list containing the values
self.y_min = y_min # minimum of y-axis (None: autoscale)
self.y_max = y_max # maximum of y-axis (None: autoscale)
self.y_bottom = y_min
# y_bottom: The actual minimum value of the vertical scale, will be
# updated if autorange
self.y_top = y_max
# y_top: The actual minimum value of the vertical scale, will be
# updated if autorange
self._x = x
self._y = y

super().__init__(
max_size=self._max_items - 1, x=x, y=y
) # self is a group of lines

def add_value(self, value):
""" Add a value to the sparkline.
: param value: The value to be added to the sparkline
"""

if value is not None:
if (
len(self._spark_list) >= self._max_items
): # if list is full, remove the first item
self._spark_list.pop(0)
self._spark_list.append(value)
self.update()

# pylint: disable=no-else-return
@staticmethod
def _xintercept(
x_1, y_1, x_2, y_2, horizontal_y
): # finds intercept of the line and a horizontal line at horizontalY
slope = (y_2 - y_1) / (x_2 - x_1)
b = y_1 - slope * x_1

if slope == 0 and y_1 != horizontal_y: # does not intercept horizontalY
return None
else:
xint = (
horizontal_y - b
) / slope # calculate the x-intercept at position y=horizontalY
return int(xint)

def _plotline(self, x_1, last_value, x_2, value, y_bottom, y_top):

y_2 = int(self.height * (y_top - value) / (y_top - y_bottom))
y_1 = int(self.height * (y_top - last_value) / (y_top - y_bottom))
self.append(Line(x_1, y_1, x_2, y_2, self.color)) # plot the line

# pylint: disable= too-many-branches, too-many-nested-blocks

def update(self):
"""Update the drawing of the sparkline

"""

# get the y range
if self.y_min is None:
self.y_bottom = min(self._spark_list)
else:
self.y_bottom = self.y_min

if self.y_max is None:
self.y_top = max(self._spark_list)
else:
self.y_top = self.y_max

if len(self._spark_list) > 2:
xpitch = self.width / (
len(self._spark_list) - 1
) # this is a float, only make int when plotting the line

for _ in range(len(self)): # remove all items from the current group
self.pop()

for count, value in enumerate(self._spark_list):
if count == 0:
pass # don't draw anything for a first point
else:
x_2 = int(xpitch * count)
x_1 = int(xpitch * (count - 1))

if (self.y_bottom <= last_value <= self.y_top) and (
self.y_bottom <= value <= self.y_top
): # both points are in range, plot the line
self._plotline(
x_1, last_value, x_2, value, self.y_bottom, self.y_top
)

else: # at least one point is out of range, clip one or both ends the line
if ((last_value > self.y_top) and (value > self.y_top)) or (
(last_value < self.y_bottom) and (value < self.y_bottom)
):
# both points are on the same side out of range: don't draw anything
pass
else:
xint_bottom = self._xintercept(
x_1, last_value, x_2, value, self.y_bottom
) # get possible new x intercept points
xint_top = self._xintercept(
x_1, last_value, x_2, value, self.y_top
) # on the top and bottom of range

if (xint_bottom is None) or (
xint_top is None
): # out of range doublecheck
pass
else:
# Initialize the adjusted values as the baseline
adj_x_1 = x_1
adj_last_value = last_value
adj_x_2 = x_2
adj_value = value

if value > last_value: # slope is positive
if xint_bottom >= x_1: # bottom is clipped
adj_x_1 = xint_bottom
adj_last_value = self.y_bottom # y_1
if xint_top <= x_2: # top is clipped
adj_x_2 = xint_top
adj_value = self.y_top # y_2
else: # slope is negative
if xint_top >= x_1: # top is clipped
adj_x_1 = xint_top
adj_last_value = self.y_top # y_1
if xint_bottom <= x_2: # bottom is clipped
adj_x_2 = xint_bottom
adj_value = self.y_bottom # y_2

self._plotline(
adj_x_1,
adj_last_value,
adj_x_2,
adj_value,
self.y_bottom,
self.y_top,
)

last_value = value # store value for the next iteration

def values(self):
"""Returns the values displayed on the sparkline
"""

return self._spark_list
135 changes: 135 additions & 0 deletions examples/display_shapes_sparkline_simpletest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
# class of sparklines in CircuitPython
# created by Kevin Matocha - Copyright 2020 (C)

# See the bottom for a code example using the `sparkline` Class.

# # File: display_shapes_sparkline.py
# A sparkline is a scrolling line graph, where any values added to sparkline using
# `add_value` are plotted.
#
# The `sparkline` class creates an element suitable for adding to the display using
# `display.show(mySparkline)`
# or adding to a `displayio.Group` to be displayed.
#
# When creating the sparkline, identify the number of `max_items` that will be
# included in the graph.
# When additional elements are added to the sparkline and the number of items has
# exceeded max_items, any excess values are removed from the left of the graph,
# and new values are added to the right.


# The following is an example that shows the

# setup display
# instance sparklines
# add to the display
# Loop the following steps:
# add new values to sparkline `add_value`
# update the sparklines `update`

import time
import random
import board
import displayio


from adafruit_display_shapes.sparkline import Sparkline

if "DISPLAY" not in dir(board):
# Setup the LCD display with driver
# You may need to change this to match the display driver for the chipset
# used on your display
from adafruit_ili9341 import ILI9341

displayio.release_displays()

# setup the SPI bus
spi = board.SPI()
tft_cs = board.D9 # arbitrary, pin not used
tft_dc = board.D10
tft_backlight = board.D12
tft_reset = board.D11

while not spi.try_lock():
spi.configure(baudrate=32000000)

spi.unlock()

display_bus = displayio.FourWire(
spi,
command=tft_dc,
chip_select=tft_cs,
reset=tft_reset,
baudrate=32000000,
polarity=1,
phase=1,
)

print("spi.frequency: {}".format(spi.frequency))

# Number of pixels in the display
DISPLAY_WIDTH = 320
DISPLAY_HEIGHT = 240

# create the display
display = ILI9341(
display_bus,
width=DISPLAY_WIDTH,
height=DISPLAY_HEIGHT,
rotation=180, # The rotation can be adjusted to match your configuration.
auto_refresh=True,
native_frames_per_second=90,
)

# reset the display to show nothing.
display.show(None)
else:
# built-in display
display = board.DISPLAY

##########################################
# Create background bitmaps and sparklines
##########################################

# Baseline size of the sparkline chart, in pixels.
chart_width = display.width
chart_height = display.height

# sparkline1 uses a vertical y range between 0 to 10 and will contain a
# maximum of 40 items
sparkline1 = Sparkline(
width=chart_width, height=chart_height, max_items=40, y_min=0, y_max=10, x=0, y=0
)

# Create a group to hold the sparkline and append the sparkline into the
# group (my_group)
#
# Note: In cases where display elements will overlap, then the order the elements
# are added to the group will set which is on top. Latter elements are displayed
# on top of former elemtns.
my_group = displayio.Group(max_size=1)

# add the sparkline into my_group
my_group.append(sparkline1)


# Add my_group (containing the sparkline) to the display
display.show(my_group)

# Start the main loop
while True:

# turn off the auto_refresh of the display while modifying the sparkline
display.auto_refresh = False

# add_value: add a new value to a sparkline
# Note: The y-range for mySparkline1 is set to 0 to 10, so all these random
# values (between 0 and 10) will fit within the visible range of this sparkline
sparkline1.add_value(random.uniform(0, 10))

# turn the display auto_refresh back on
display.auto_refresh = True

# The display seems to be less jittery if a small sleep time is provided
# You can adjust this to see if it has any effect
time.sleep(0.01)
Loading