This version is a little fancier. It provides a graphical plot of the predicted tide level over the 24 hour span of the current day.

It works pretty much the same as the simple version - go grab the data, parse it, display it. Here we just have more data to deal with and we display it a little fancier.

Let's get the code loaded up and running first...

Add CircuitPython Code and Assets

In the embedded code element below, click on the Download: Project Zip link, and save the .zip archive file to your computer.

Then uncompress the.zip file, it will unpack to a folder named PyPortal_Tides.

Copy the contents of the PyPortal_Tides directory to your PyPortal's CIRCUITPY drive, and then be sure to rename pp_tides_graphical.py to code.py so it will automatically run when the PyPortal restarts.

Editing the Code

At the top of the code, find the line that sets the station ID and change it for your location:

STATION_ID = "9447130"   # tide location, find yours here: https://tidesandcurrents.noaa.gov/

Note that it is entered as a string, not a number.

# SPDX-FileCopyrightText: 2019 Carter Nelson for Adafruit Industries
#
# SPDX-License-Identifier: MIT

import time
import board
import displayio
from adafruit_pyportal import PyPortal
from adafruit_bitmap_font import bitmap_font
from adafruit_display_text.label import Label

# --| USER CONFIG |--------------------------
STATION_ID = (
    "9447130"  # tide location, find yours here: https://tidesandcurrents.noaa.gov/
)
PLOT_SIZE = 2  # tide plot thickness
PLOT_COLOR = 0x00FF55  # tide plot color
MARK_SIZE = 6  # current time marker size
MARK_COLOR = 0xFF0000  # current time marker color
DATE_COLOR = 0xE0CD1A  # date text color
TIME_COLOR = 0xE0CD1A  # time text color
VSCALE = 20  # vertical plot scale
# -------------------------------------------

# pylint: disable=line-too-long
DATA_SOURCE = (
    "https://api.tidesandcurrents.noaa.gov/api/prod/datagetter?date=today&product=predictions&datum=mllw&format=json&units=metric&time_zone=lst_ldt&station="
    + STATION_ID
)
DATA_LOCATION = ["predictions"]

WIDTH = board.DISPLAY.width
HEIGHT = board.DISPLAY.height

# gotta have one of these
pyportal = PyPortal(status_neopixel=board.NEOPIXEL, default_bg="/images/tides_bg_graph.bmp")

# Connect to the internet and get local time
pyportal.get_local_time()

# Setup palette used for plot
palette = displayio.Palette(3)
palette[0] = 0x0
palette[1] = PLOT_COLOR
palette[2] = MARK_COLOR
palette.make_transparent(0)

# Setup tide plot bitmap
tide_plot = displayio.Bitmap(WIDTH, HEIGHT, 3)
pyportal.graphics.splash.append(displayio.TileGrid(tide_plot, pixel_shader=palette))

# Setup font used for date and time
date_font = bitmap_font.load_font("/fonts/mono-bold-8.bdf")
date_font.load_glyphs(b"1234567890-")

# Setup date label
date_label = Label(date_font, text="0000-00-00", color=DATE_COLOR, x=7, y=14)
pyportal.graphics.splash.append(date_label)

# Setup time label
time_label = Label(date_font, text="00:00:00", color=TIME_COLOR, x=234, y=14)
pyportal.graphics.splash.append(time_label)

# Setup current time marker
time_marker_bitmap = displayio.Bitmap(MARK_SIZE, MARK_SIZE, 3)
time_marker_bitmap.fill(2)
time_marker = displayio.TileGrid(
    time_marker_bitmap, pixel_shader=palette, x=-MARK_SIZE, y=-MARK_SIZE
)
pyportal.graphics.splash.append(time_marker)


def get_tide_data():
    """Fetch JSON tide data and return parsed results in a list."""

    # Get raw JSON data
    raw_data = pyportal.network.fetch_data(DATA_SOURCE, json_path=DATA_LOCATION)

    # Results will be stored in a list that is display WIDTH long
    new_tide_data = [None] * WIDTH

    # Convert raw data to display coordinates
    for data in raw_data[0]:
        _, t = data["t"].split(" ")  # date and time
        h, m = t.split(":")  # hours and minutes
        v = data["v"]  # water level
        x = round((WIDTH - 1) * (60 * float(h) + float(m)) / 1440)
        y = (HEIGHT // 2) - round(VSCALE * float(v))
        y = 0 if y < 0 else y
        y = HEIGHT - 1 if y >= HEIGHT else y
        new_tide_data[x] = y

    return new_tide_data


def draw_data_point(x, y, size=PLOT_SIZE, color=1):
    """Draw data point on to the tide plot bitmap at (x,y)."""
    if y is None:
        return
    offset = size // 2
    for xx in range(x - offset, x + offset + 1):
        for yy in range(y - offset, y + offset + 1):
            try:
                tide_plot[xx, yy] = color
            except IndexError:
                pass


def draw_time_marker(time_info):
    """Draw a marker on the tide plot for the current time."""
    h = time_info.tm_hour
    m = time_info.tm_min
    x = round((WIDTH - 1) * (60 * float(h) + float(m)) / 1440)
    y = tide_data[x]
    if y is not None:
        x -= MARK_SIZE // 2
        y -= MARK_SIZE // 2
        time_marker.x = x
        time_marker.y = y


def update_display(time_info, update_tides=False):
    """Update the display with current info."""

    # Tide data plot
    if update_tides:
        # out with the old
        for i in range(WIDTH * HEIGHT):
            tide_plot[i] = 0
        # in with the new
        for x in range(WIDTH):
            draw_data_point(x, tide_data[x])

    # Current location marker
    draw_time_marker(time_info)

    # Date and time
    date_label.text = "{:04}-{:02}-{:02}".format(
        time_info.tm_year, time_info.tm_mon, time_info.tm_mday
    )
    time_label.text = "{:02}:{:02}:{:02}".format(
        time_info.tm_hour, time_info.tm_min, time_info.tm_sec
    )


# First run update
tide_data = get_tide_data()
current_time = time.localtime()
update_display(current_time, True)
current_yday = current_time.tm_yday

# Run forever
while True:
    current_time = time.localtime()
    new_tides = False
    if current_time.tm_yday != current_yday:
        # new day, time to update
        tide_data = get_tide_data()
        new_tides = True
        current_yday = current_time.tm_yday
    update_display(current_time, new_tides)
    time.sleep(0.5)

How It Works

This code also does all the data mangling in a single function - get_tide_data(). The general idea is to take the time vs. tide level information and map it into the display's (x, y) coordinates. That way all we have to do is draw pixels at all the (x, y) locations and it will generate a plot.

But first, we go get the data. Same as before:

    raw_data = pyportal.fetch()

We store this in a list that has an entry for each x pixel on the display. The index of the list corresponds to the x pixel. The entry itself is the y value. So we know we need as many entries as the display has pixels across, i.e. its WIDTH:

    new_tide_data = [None]*WIDTH

And then we loop and parse the data again. We split out the date and time. We further split out the time into hours and minutes, so we can do some math on what will become our x value. The tide level v is what will become our y value. After some math, the results are stored in our list and finally returned.

    for data in raw_data:
        _, t = data["t"].split(" ") # date and time
        h, m = t.split(":")         # hours and minutes
        v = data["v"]               # water level
        x = round( (WIDTH - 1) * (60 * float(h) + float(m)) / 1440 )
        y = (HEIGHT // 2) - round(VSCALE * float(v))
        y = 0 if y < 0 else y
        y = HEIGHT-1 if y >= HEIGHT else y
        new_tide_data[x] = y

    return new_tide_data

Let's talk about that math a little more. First, the math for the horizontal or x position:

        x = round( (WIDTH - 1) * (60 * float(h) + float(m)) / 1440 )

The display is WIDTH pixels across. A day has 24 hours which is 24*60=1440 total minutes. So there are 1440 minutes per WIDTH pixels. To get the x coordinate for any given minute, we just multiply by that ratio:

x = minutes * (WIDTH / 1440)

That's all that's happening. There's a little more in the code to compute total minutes for hours+minutes time data. And the - 1 is to deal with the 0 based indexing of the x pixels - they start at 0, not 1.

Now the math for the vertical or y position:

        y = (HEIGHT // 2) - round(VSCALE * float(v))

We want the vertical plot to vary above/below the middle of the display. So we just compute the middle of display with HEIGHT / 2. From the this we subtract the tide level value v.

y = (HEIGHT / 2) - v

And that's pretty much it.

The extra things being done are to use // instead of / to force integer math, since the y pixel needs to be an integer. We also scale the v value by multiplying by VSCALE, which is just an arbitrary value to make the plot spread out more. We wrap that in round() to also make sure it ends up being an integer.

The other two lines just make sure the bounds are with the actual display values of 0 to HEIGHT-1.

What's That Little Red Mark?

That's the location of the current time on the tide plot. It should move along the plot as the day progresses. If it doesn't show up when you initially run the code, wait a bit. It should eventually show up.

This guide was first published on Apr 17, 2019. It was last updated on Nov 27, 2023.

This page (Graphical Tide Level Viewer) was last updated on Dec 01, 2023.

Text editor powered by tinymce.