Random Data Project -- National Data Buoy Center

John WalleyJohn Walley
8 min read

I thought it might be a fun little project to build a dashboard with wave height data in the northern Gulf of America. NOAA’s NDBC provides access to buoy data from a number of sources. Map view of the area of interest.

https://www.ndbc.noaa.gov/obs.shtml?lat=28.174505&lon=-90.007324&zoom=6&type=oceans&status=r&pgm=&op=&ls=n

Data

Examination

The type and frequency of data varies across stations. For example, not all stations have wave height, and some have more detailed wave information than others like period and direction.

https://www.weather.gov/akq/wavedetail

There are three fundamental properties of ocean waves: height, period, and direction. Wave height generally refers to how tall a wave is from trough to crest, wave direction is the direction the wave is coming from, and wave period is the time it takes for successive waves to pass a fixed point, such as a buoy. The period is also directly related to how fast waves move, how deep they extend into the ocean, how much energy they contain, which, in turn, influences the size of breaking waves at the coast, and more.

Let’s start simple and find some random stations along the northern Gulf that publish wave height info (we’ll get to what I settled on later).

Options on getting data is covered here https://www.ndbc.noaa.gov/faq/rt_data_access.shtml, and we’ll use the https://www.ndbc.noaa.gov/data/realtime2/<station ID>.txt method that returns a station’s last 45 days of data.

Measurement descriptions and units

Data sample

% curl https://www.ndbc.noaa.gov/data/realtime2/42020.txt > 42020.txt
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100  598k  100  598k    0     0  3259k      0 --:--:-- --:--:-- --:--:-- 3269k

% head 42020.txt 
#YY  MM DD hh mm WDIR WSPD GST  WVHT   DPD   APD MWD   PRES  ATMP  WTMP  DEWP  VIS PTDY  TIDE
#yr  mo dy hr mn degT m/s  m/s     m   sec   sec degT   hPa  degC  degC  degC  nmi  hPa    ft
2025 05 01 13 30 130  7.0  9.0    MM    MM    MM  MM 1012.5  25.3  25.2  23.7   MM   MM    MM
2025 05 01 13 20 140  8.0 10.0   1.6     7   5.1 119 1012.4  25.4  25.2  23.6   MM   MM    MM
2025 05 01 13 10 140  8.0 10.0   1.6    MM   5.1 119 1012.0  25.3  25.2  23.7   MM   MM    MM
2025 05 01 13 00 140  8.0 10.0    MM    MM    MM  MM 1011.8  25.3  25.2  23.8   MM +1.2    MM
2025 05 01 12 50 140  8.0 10.0   1.6    MM   5.0 124 1011.6    MM  25.2    MM   MM   MM    MM
2025 05 01 12 40 140  8.0 10.0    MM    MM    MM  MM 1011.6  25.3  25.2  23.6   MM   MM    MM
2025 05 01 12 30 140  8.0 10.0    MM    MM    MM  MM 1011.6  25.3    MM  23.7   MM   MM    MM
2025 05 01 12 20 140  8.0  9.0   1.6    MM   5.0 127 1011.7    MM    MM    MM   MM   MM    MM

Database setup

Going to use Supabase here, and start with a couple of tables.

Table: buoys

Description: stores buoy metadata, and defines stations we should poll for data

CREATE TABLE buoys (
  id TEXT PRIMARY KEY,
  name TEXT NOT NULL,
  latitude DOUBLE PRECISION NOT NULL,
  longitude DOUBLE PRECISION NOT NULL,
  active BOOLEAN DEFAULT TRUE
);

Manually seed it for now.

INSERT INTO buoys (id, name, latitude, longitude, active) VALUES
('42020', 'Station 42020',                     26.970,  -96.679, TRUE),
('42354', 'Station 42354 - Chandeleur Island SE, LA (279)', 29.579,  -88.643, TRUE),
('LOPL1', 'Station LOPL1 - Louisiana Offshore Oil Port, LA', 28.885,  -90.025, TRUE),
('42012', 'Station 42012 - ORANGE BEACH - 44 NM SE of Mobile, AL', 30.060,  -87.548, TRUE),
('42035', 'Station 42035 - GALVESTON,TX - 22 NM East of Galveston, TX', 29.235,  -94.410, TRUE),
('42036', 'Station 42036 - WEST TAMPA - 112 NM WNW of Tampa, FL', 28.500,  -84.505, TRUE),
('42001', 'Station 42001 - MID GULF - 180 nm South of Southwest Pass, LA', 25.926,  -89.662, TRUE),
('42002', 'Station 42002 - WEST GULF - 207 NM East of Brownsville, TX', 25.950,  -93.780, TRUE);

Table: wave_observations

Description: stores historical wave height data

CREATE TABLE wave_observations (
  id BIGSERIAL PRIMARY KEY,
  buoy_id TEXT REFERENCES buoys(id),
  timestamp TIMESTAMP WITH TIME ZONE NOT NULL,
  wave_height_meters DOUBLE PRECISION,
  UNIQUE (buoy_id, timestamp)
);

That gives us the following schema

My plan for the wave_observations table is to populate it with a scheduled edge function. Unfortunately, it doesn’t look like you can request less than 45 days of data, so we’ll skip a lot of the duplicate info and just upsert on the most recent 96 rows (for now—maybe I’ll change my mind on the number of logic).

Edge function: fetch_buoy_data

import { serve } from 'https://deno.land/std@0.177.0/http/server.ts';
import { createClient } from 'https://esm.sh/@supabase/supabase-js';
const supabase = createClient(Deno.env.get('SUPABASE_URL'), Deno.env.get('SUPABASE_SERVICE_ROLE_KEY'));
serve(async ()=>{
  const { data: buoys, error } = await supabase.from('buoys').select('id').eq('active', true);
  if (error) {
    console.error('Failed to fetch buoy list:', error);
    return new Response('Error fetching buoys', {
      status: 500
    });
  }
  for (const buoy of buoys){
    const buoyId = buoy.id;
    const url = `https://www.ndbc.noaa.gov/data/realtime2/${buoyId}.txt`;
    try {
      const res = await fetch(url);
      if (!res.ok) throw new Error(`Bad response for ${buoyId}`);
      const text = await res.text();
      const lines = text.split('\n').filter((line)=>/^\d{4}/.test(line)).slice(0, 96); // Only consider the most recent 96 observations
      for (const line of lines){
        const parts = line.trim().split(/\s+/);
        if (parts.length < 11) continue;
        const [yy, mm, dd, hh, min, , , , waveHeightStr] = parts;
        const timestamp = new Date(`${yy}-${mm}-${dd}T${hh}:${min}:00Z`);
        const waveHeight = waveHeightStr === 'MM' ? null : parseFloat(waveHeightStr);
        const { error: insertError } = await supabase.from('wave_observations').upsert({
          buoy_id: buoyId,
          timestamp,
          wave_height_meters: waveHeight
        }, {
          onConflict: 'buoy_id,timestamp'
        });
        if (insertError) {
          console.warn(`Insert error for ${buoyId} @ ${timestamp}:`, insertError.message);
        }
      }
    } catch (err) {
      console.error(`Error processing buoy ${buoyId}:`, err);
    }
  }
  return new Response('OK', {
    status: 200
  });
});

I then created a cron job to run fetch_buoy_data every 8 hours.

I also don’t want this to keep the historical wave_observations around forever, so I’ll add a weekly job to prune data older than 4 months.

DELETE FROM wave_observations WHERE timestamp < NOW() - INTERVAL '4 months';

Front-end

Let’s see how far we can get with no- or low-code option, and to make sure we have max pain let’s try to have it include mapping in the initial version 🫠

Maybe maps weren’t a great idea for version 1…

Long story short

I tried Bolt.new, lovable, v0, and Firebase Studio (all free tiers).

Takeaways

  • You quickly run out of credits (except on Firebase) so your prompts are important

    • Example 1: I think lovable initially tried to use Mapbox, but my personal account is expired and required a credit card to reactivate—no thanks. I asked it for a free alternative and it suggested Leaflet and OpenStreetMap.

    • Example 2: If you’re connecting to an existing db, etc. try to get the schema info in early so it doesn’t write code against made up column names

  • Trying to fix bugs eats through your credits faster than you’d like

  • Maybe start with something simple

    • My initial Firebase app would render the map correctly. After a ton of fix attempts, I had it just replace the map with a table of results, then a map with no data, and then add a single data point. Those all worked, and we ended up getting a working map (see below). I also started laying on the encouragement at unnatural levels (for me at least).
  • It’s about on par with working with human developers!

You: Can you change the label on this button?

AI: Yes. I’ve made the change. Let me know if you like it.

You: Uh, it crashes now.

AI: Ok, I’ve fixed it. Let me know if you like it.

You: Uh, still crashes

(repeat 10 times)

Now, in fairness to the no-code platforms I think they have improved in that they now often see errors have happened and offer to fix them. But, we still have some room for improvement here.

Samples

Well, did I actually see a working prototype? Kind of!

lovable

lovable actually did have a working map with buoys being displayed, but we broke that while trying to address the NaN/10 bug shown below. Ran out of credits.

Firebase Studio

After much back and forth, Firebase Studio came through (not deployed publicly)!

This was the starting prompt for the Firebase attempt

Build a geo-enabled website that visualizes wave height data from buoys in the northern Gulf of Mexico. The data is stored in a Supabase database, and includes buoy metadata (id, name, lat, long) and wave observations (timestamp, buoy_id, wave_height_m).

The site should: • Display buoy locations on a map using their lat/long • Show latest wave height for each buoy with size or color-coded markers • Allow clicking on a buoy to see a sparkline or mini-chart of wave height over the last 24 hours • Include a header and title that leans into humor (e.g. “Is It Choppy Out There?”) • Be mobile-friendly and fast

Use a free mapping solution like Leaflet with OpenStreetMap.

Bonus: Add a real-time-ish “Chop Score” for the Gulf based on average wave height.

I'm attaching a picture of the database schema. I will provide the Supabase URL and key when you're ready.

That it?

I may come back to the other platforms on a different day with a full sack of credits and see if they can finish what they started.

Let’s dial this up a notch

I guess I’ve committed myself to wasting a lot of time on this, so let’s give my buddy Grok a chance too.

We discussed the idea and settled on Next.js. It did a pretty good job of generating code and instructions, we only had a couple of errors that I remember. Here’s the repo

https://github.com/ThatOrJohn/buoy-wave-vibes

I ended up deploying it to Vercel.

https://buoy-wave-vibes.vercel.app (I don’t expect this to live forever)

Uh, couldn’t you have just written something yourself in the time you spent on all of this? Maybe 🤷‍♂️! It is kind of fun bossing the computer around though, give it a try.

Update

I decided to go for round 2 of some of the incomplete project.

lovable

It was able to correct the problems, and here’s a look at what I ended up with

v0

I was able to get a successful preview here too

Bolt.new

My daily credits haven’t reset yet, I’ll probably be distracted with something else by then.

0
Subscribe to my newsletter

Read articles from John Walley directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

John Walley
John Walley