diff --git a/adafruit_display_shapes/sparkline.py b/adafruit_display_shapes/sparkline.py new file mode 100644 index 0000000..99d2f93 --- /dev/null +++ b/adafruit_display_shapes/sparkline.py @@ -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 diff --git a/examples/display_shapes_sparkline_simpletest.py b/examples/display_shapes_sparkline_simpletest.py new file mode 100755 index 0000000..2faa026 --- /dev/null +++ b/examples/display_shapes_sparkline_simpletest.py @@ -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) diff --git a/examples/display_shapes_sparkline_ticks.py b/examples/display_shapes_sparkline_ticks.py new file mode 100755 index 0000000..f05d892 --- /dev/null +++ b/examples/display_shapes_sparkline_ticks.py @@ -0,0 +1,195 @@ +# 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 random +import time +import board +import displayio +import terminalio +from adafruit_display_shapes.sparkline import Sparkline +from adafruit_display_shapes.line import Line +from adafruit_display_shapes.rect import Rect +from adafruit_display_text import label + +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 - 50 +chart_height = display.height - 50 + +font = terminalio.FONT + +line_color = 0xFFFFFF + +# Setup the first bitmap and sparkline +# This sparkline has no background bitmap +# mySparkline1 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=40, + y=30, + color=line_color, +) + +# Label the y-axis range + +text_xoffset = -10 +text_label1a = label.Label( + font=font, text=str(sparkline1.y_top), color=line_color +) # yTop label +text_label1a.anchor_point = (1, 0.5) # set the anchorpoint at right-center +text_label1a.anchored_position = ( + sparkline1.x + text_xoffset, + sparkline1.y, +) # set the text anchored position to the upper right of the graph + +text_label1b = label.Label( + font=font, text=str(sparkline1.y_bottom), color=line_color +) # yTop label +text_label1b.anchor_point = (1, 0.5) # set the anchorpoint at right-center +text_label1b.anchored_position = ( + sparkline1.x + text_xoffset, + sparkline1.y + chart_height, +) # set the text anchored position to the upper right of the graph + + +bounding_rectangle = Rect( + sparkline1.x, sparkline1.y, chart_width + 1, chart_height + 1, outline=line_color +) + + +# Create a group to hold the sparkline, text, rectangle and tickmarks +# append them 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=20) + +my_group.append(sparkline1) +my_group.append(text_label1a) +my_group.append(text_label1b) +my_group.append(bounding_rectangle) + +total_ticks = 10 + +for i in range(total_ticks + 1): + x_start = sparkline1.x - 5 + x_end = sparkline1.x + y_both = sparkline1.y + i * int(round(chart_height / (total_ticks))) + my_group.append(Line(x_start, y_both, x_end, y_both, color=line_color)) +my_group.append( + Line( + x_start, + sparkline1.y + chart_height, + x_end, + sparkline1.y + chart_height, + color=line_color, + ) +) + +# Set the display to show my_group that contains the sparkline and other graphics +display.show(my_group) + +# Start the main loop +while True: + + # Turn off auto_refresh to prevent partial updates of the screen during updates + # of the sparkline drawing + 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 on auto_refresh for the display + 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) diff --git a/examples/display_shapes_sparkline_triple.py b/examples/display_shapes_sparkline_triple.py new file mode 100755 index 0000000..f58a99d --- /dev/null +++ b/examples/display_shapes_sparkline_triple.py @@ -0,0 +1,269 @@ +# 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 random +import time +import board +import displayio +import terminalio +from adafruit_display_shapes.sparkline import Sparkline +from adafruit_display_text import label + +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 + DISPLAY_WIDTH = board.DISPLAY.width + +########################################## +# Create background bitmaps and sparklines +########################################## + +# Baseline size of the sparkline chart, in pixels. +chart_width = 50 +chart_height = 50 + +font = terminalio.FONT + +# Setup the first bitmap and sparkline +# This sparkline has no background bitmap +# sparkline1 uses a vertical y range between -1 to +1.25 and will contain a maximum of 40 items +sparkline1 = Sparkline( + width=chart_width, + height=chart_height, + max_items=40, + y_min=-1, + y_max=1.25, + x=10, + y=10, +) + +# Label the y-axis range +text_label1a = label.Label( + font=font, text=str(sparkline1.y_top), color=0xFFFFFF +) # y_top label +text_label1a.anchor_point = (0, 0.5) # set the anchorpoint +text_label1a.anchored_position = ( + 10 + chart_width, + 10, +) # set the text anchored position to the upper right of the graph + +text_label1b = label.Label( + font=font, text=str(sparkline1.y_bottom), color=0xFFFFFF +) # y_bottom label +text_label1b.anchor_point = (0, 0.5) # set the anchorpoint +text_label1b.anchored_position = ( + 10 + chart_width, + 10 + chart_height, +) # set the text anchored position to the upper right of the graph + + +# Setup the second bitmap and sparkline +# sparkline2 uses a vertical y range between 0 to 1, and will contain a +# maximum of 10 items +# +palette2 = displayio.Palette(1) # color palette used for bitmap2 (one color) +palette2[0] = 0x0000FF + +bitmap2 = displayio.Bitmap(chart_width * 2, chart_height * 2, 1) # create bitmap2 +tilegrid2 = displayio.TileGrid( + bitmap2, pixel_shader=palette2, x=150, y=10 +) # Add bitmap2 to tilegrid2 +sparkline2 = Sparkline( + width=chart_width * 2, + height=chart_height * 2, + max_items=10, + y_min=0, + y_max=1, + x=150, + y=10, + color=0xFF00FF, +) + + +# Setup the third bitmap and third sparkline +# sparkline3 contains a maximum of 10 items +# since y_min and y_max are not specified, sparkline3 uses autoranging for both +# the top and bottom of the y-axis. +# Note1: Any unspecified edge limit (y_min or y_max) will autorange that edge based +# on the data in the list. +# Note2: You can read back the current value of the y-axis limits by using +# sparkline3.y_bottom or sparkline3.y_top + + +palette3 = displayio.Palette(1) # color palette used for bitmap (one color) +palette3[0] = 0x11FF44 +bitmap3 = displayio.Bitmap(DISPLAY_WIDTH - 30, chart_height * 2, 1) # create bitmap3 +tilegrid3 = displayio.TileGrid( + bitmap3, pixel_shader=palette3, x=0, y=120 +) # Add bitmap3 to tilegrid3 + +sparkline3 = Sparkline( + width=DISPLAY_WIDTH - 30, + height=chart_height * 2, + max_items=10, + x=0, + y=120, + color=0xFFFFFF, +) + +# Initialize the y-axis labels for mySparkline3 with no text +text_label3a = label.Label( + font=font, text="", color=0x11FF44, max_glyphs=20 +) # y_top label +text_label3a.anchor_point = (0, 0.5) # set the anchorpoint +text_label3a.anchored_position = ( + sparkline3.width, + 120, +) # set the text anchored position to the upper right of the graph + +text_label3b = label.Label( + font=font, text="", color=0x11FF44, max_glyphs=20 +) # y_bottom label +text_label3b.anchor_point = (0, 0.5) # set the anchorpoint +text_label3b.anchored_position = ( + sparkline3.width, + 120 + sparkline3.height, +) # set the text anchored position to the upper right of the graph + +# Create a group to hold the three bitmap TileGrids and the three sparklines and +# append them 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=20) + +my_group.append(sparkline1) +my_group.append(text_label1a) +my_group.append(text_label1b) + +my_group.append(tilegrid2) +my_group.append(sparkline2) + +my_group.append(tilegrid3) +my_group.append(sparkline3) +my_group.append(text_label3a) +my_group.append(text_label3b) + +# Set the display to show my_group that contains all the bitmap TileGrids and +# sparklines +display.show(my_group) + +i = 0 # This is a counter for changing the random values for mySparkline3 + +# Start the main loop +while True: + + # Turn off auto_refresh to prevent partial updates of the screen during updates + # of the sparklines + display.auto_refresh = False + + # add_value: add a new value to a sparkline + # Note: The y-range for sparkline1 is set to -1 to 1.25, so all these random + # values (between 0 and 1) will fit within the visible range of this sparkline + sparkline1.add_value(random.uniform(0, 1)) + + # Note: For sparkline2, the y-axis range is set from 0 to 1. + # With the random values set between -1 and +2, the values will sometimes + # be out of the y-range. This example shows how the fixed y-range (0 to 1) + # will "clip" values (it will not display them) that are above or below the + # y-range. + sparkline2.add_value(random.uniform(-1, 2)) + + # sparkline3 is set autoranging for both the top and bottom of the Y-axis + + # In this example, for 15 values, this adds points in the range from 0 to 1. + # Then, for the next 15 values, it adds points in the range of 0 to 10. + # This is to highlight the autoranging of the y-axis. + # Notice how the y-axis labels show that the y-scale is changing. + # + # An exercise for the reader: You can set only one or the other sparkline axis + # to autoranging by setting its value to None. + if i < 15: + sparkline3.add_value(random.uniform(0, 1)) + else: + sparkline3.add_value(random.uniform(0, 10)) + text_label3a.text = str(sparkline3.y_top) + text_label3b.text = str(sparkline3.y_bottom) + i += 1 # increment the counter + if i > 30: # After 30 times through the loop, reset the counter + i = 0 + + # Turn on auto_refresh for the display + 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)