Anthony Ellis
← All projects

Live data · Canvas

Space Weather Monitor

Four NOAA feeds, no chart library, no build step. Solar wind speed, proton density, the interplanetary magnetic field and the planetary K index, drawn to canvas as they arrive.

A dark dashboard showing solar wind speed, magnetic field and K index charts.
Role
Sole developer
Stack
Vanilla JS · Canvas 2D
Data
NOAA SWPC, four endpoints
Refresh
Every five minutes
Dependencies
None

What it shows

Everything on the page comes from the Space Weather Prediction Center. The solar wind readings are taken at the L1 Lagrange point, roughly a million miles upstream of Earth, which is why the charts are a preview: what the spacecraft sees now reaches us in about thirty to sixty minutes.

The number that actually matters is Bz, the north-south component of the interplanetary magnetic field. When it points south it can connect with Earth's field and pour energy in. When it points north, almost nothing happens no matter how fast the wind is blowing. Speed gets the headlines; Bz decides the outcome.

The endpoints everyone links to don't work

The obvious solar wind feeds are the six-hour products at /products/solar-wind/plasma-6-hour.json and its magnetic-field twin. They are the ones in every tutorial. They are also the ones the first version of this page shipped with, and on the live site every reading was a dash.

Those files send no Access-Control-Allow-Origin header, so no browser on another origin is allowed to read them. Nothing about that is visible from the terminal: curl fetches them happily, because CORS is a rule browsers enforce on themselves and curl is not a browser. The only way to find it was to open the console on the deployed page and read the error.

The real-time files under /json/rtsw/ do send the header. The trade is that they arrive as 24 hours of one-minute samples, with no shorter version offered: roughly 4.4 MB of JSON between the two files, which gzips down to about 250 KB on the wire. The page takes the whole day and keeps the last six hours of it. That is a lot of rows to throw away, and still cheaper than standing up a proxy of my own to trim them, which would have put a server I have to keep alive between the visitor and NOAA.

It isn't one spacecraft

The bigger surprise was inside the file. Those real-time feeds are not a single instrument's record: ACE, IMAP and DSCOVR all appear in the same array, with overlapping timestamps. In a typical 24-hour file, about a third of the rows are a minute that already appeared, seen by a different spacecraft.

Plot the array as it arrives and you draw three spacecraft on top of each other. It does not throw, it does not look empty, and it does not look obviously wrong — it looks like a noisy chart, which is exactly what solar wind data is supposed to look like. NOAA marks the designated primary feed with active: true, so that flag decides what gets drawn. If it is missing entirely, which happens during a handover between spacecraft, the page falls back to whichever single source contributed the most rows rather than quietly blending them.

The general rule this is a case of: when you can choose between a failure that is loud and a failure that is plausible, take the loud one. Mixed-spacecraft data is the plausible kind, which is why it is worth going out of your way to rule out.

Three shapes, one parser

The feeds don't agree with each other either. The /json/ endpoints return arrays of objects. The older /products/ endpoints return arrays of arrays with a header row, like a CSV that took a wrong turn. And the same quantity arrives as Kp in the measured feed and kp in the forecast one.

So everything goes through one normaliser that flattens both shapes to plain objects and lowercases the keys. Values are then looked up by name, never by column position: if a column moves, a lookup by index doesn't crash, it just starts plotting temperature as speed.

The forecast feed has its own trap. It restates the last several measured periods before it reaches the predicted ones, all in the same array, distinguished only by an observed field reading observed, estimated or predicted. Take the first eight rows and you will draw last week as though it were next week.

Failing one panel at a time

The four feeds are fetched with Promise.allSettled rather than Promise.all. With all, one endpoint having a bad afternoon takes down the whole page. With allSettled, each panel renders if its own data arrived and reports "no data" if it didn't, and the status pill in the corner counts how many feeds are missing.

If everything fails, the page says NOAA is unreachable, names the failing feed in the console, and leaves the readings as dashes. It does not fall back to cached numbers or sample data. A dashboard showing stale values with a confident live indicator is worse than one admitting it knows nothing.

Drawing it

The charts are hand-drawn to canvas. That is a deliberate choice rather than stubbornness: importing a charting library would have been faster, and it would also have meant the most visible part of the page was somebody else's work.

Saying no honestly

The page ends by answering whether the aurora is visible from Las Vegas. The answer is almost always no, and it says so plainly rather than hedging.

It works from a coarse table mapping Kp to the equatorward edge of the auroral oval in geomagnetic latitude, compared against Las Vegas at about 43.5°. That is an estimate and the page labels it as one. It would have been easy to dress this up as a forecast, and several sites do. The difference between a measurement and a model is worth more than the extra confidence.

Try this

Open the page and watch Bz rather than the speed. Most of the time it oscillates gently either side of zero and nothing happens. If it drops below about −10 nT and stays there, check the Kp panel an hour later and watch the bars climb.

What I'd do next