Frontend
How barometer.today draws its isobars
SimpleMeteo DEV Community
3 views
barometer.today shows air pressure for any city: the reading now, the change since yesterday, and the week ahead. It also has a world isobar map, animated hour by hour from a day back to four days ahead.
This post explains what isobars are and the algorithms that we use to draw them. The code is Python with numpy, scipy, contourpy and shapely, and anyone drawing contour maps from gridded data will meet the same problems.
What an isobar is
Air pressure varies from place to place, and a weather map shows this with isobars - lines joining points of equal pressure. Where lines crowd together pressure changes fast over a short distance and the wind is usually strong. Where they spread out the air is calmer. The rings close around centers of high pressure marked H and low pressure, marked L. A deep low with tight rings is a storm. A broad high with loose rings is settled weather.
The pressure on such a map is sea-level pressure. A barometer in Denver reads about 835 hPa because it sits 1,600 m up, which says nothing about the weather. So the reading is converted to what it would be at sea level, and the map becomes comparable everywhere. With one exception, covered below.
The data
The map is drawn from the ECMWF global forecast. It arrives as a grid of sea-level pressure values every 0.25 degrees, about 28 km, for the whole planet. The model's own time step is three hours; Open-Meteo interpolates that to hourly values. We fetch it from Open-Meteo, which we run on our own server for all SimpleMeteo.com sites. uvi.today and pollen.today use the same instance.
The model updates four times a day. After each update a job produces one frame per hour, about 120 in total.
Smoothing
A raw model grid is jagged cell to cell, so lines traced straight from it wobble. We use a Gaussian blur with a sigma of one cell, 28 km. Wider blurs give calmer lines but flatten the extremes: with a 110 km blur, a hurricane with a 936 hPa core comes out as a mild 993 hPa low. It could produce a map that looks fine and is wrong.
The blur has to know about mountains. Over a high plateau sea-level pressure is a calculation about air that does not exist, and it usually comes out several hPa too high. A plain Gaussian would spread those inflated values into the real field around the plateau. Along the Himalayan front this moves lowland values by more than an isobar interval.
The fix is a normalized convolution. Blur the field with the plateau cells set to zero, blur a mask of ones and zeros the same way, and divide one by the other. Each output cell is then the weighted mean of only the valid cells nearby. Two lines of scipy:
num = gaussian_filter(np.where(valid, z, 0.0), sigma)
den = gaussian_filter(valid.astype(float), sigma)
smoothed = np.where(valid, num / den, z)
The same trick handles a missing value: instead of a single null spreading into a 9 by 9 hole after blurring, it contributes nothing and stays a one-cell gap.
Where the mask is
The mask is ground above 2,500 m. It comes from a digital elevation model, smoothed to 55 km so that a single peak does not count, then only massifs of at least 265,000 km² are kept. Four regions qualify: the Andes, the Tibetan plateau with the Himalaya and the Pamir, the Greenland ice sheet and Antarctica.
Over the mask the isobars are still drawn, but dashed. The map stays continuous and the reader is not left with a hole in Asia, while the dashes say the values here are extrapolated. Each line segment is tested against the mask individually, so an isobar crossing the Andes is dashed for exactly the part over high ground.
Tracing the lines
Contours are traced with marching squares, the standard algorithm that matplotlib uses. Each grid cell has a value at its four corners. For a given level, say 1012 hPa, the algorithm marks each corner as above or below, which gives 16 possible patterns, and each pattern says where the line enters and leaves the cell. The crossing point along an edge is found by linear interpolation between the corner values. Done for every cell, the pieces join into continuous lines. Done for every level, you have the map.
A world map adds a twist: longitude wraps around. The algorithm sees a rectangle with left and right edges and would stop every line at 180 degrees. So the grid is padded with two degrees of columns copied from the opposite edge, contoured, and the lines clipped back to the world. They then cross the Pacific date line without a seam.
pad = int(math.ceil(2.0 / res))
zp = np.hstack([z[:, -pad:], z, z[:, :pad]])
xp = np.concatenate([lons[-pad:] - 360, lons, lons[:pad] + 360])
gen = contourpy.contour_generator(x=xp, y=lats, z=np.ma.array(zp, mask=mp))
Each line is then simplified with the Douglas-Peucker algorithm at 0.05 degrees. It removes points that lie within that distance of a straight line between their neighbours, so a nearly straight 900 km isobar collapses to three points while a tight ring around a storm keeps its shape. A frame ends up as a few hundred kilobytes of GeoJSON, under 100 KB compressed.
Finding the H and L
Centers are found on the smoothed field with min and max filters: a cell is a candidate if it is the extreme within a window of about 850 km. The window is built per band of latitude, so it is a real distance and not a cell count. A fixed cell count would cover only half the distance at 60 degrees north that it covers at the equator.
A candidate must also stand out. The field within the window must range by at least 3 hPa, or 5 hPa inside 20 degrees of the equator, where the tropical atmosphere has a natural twice-daily tide of 2 to 4 hPa that would otherwise put a letter at every ripple.
Two details matter in practice. The masked cells are filled with minus infinity for the max filter and plus infinity for the min filter, never NaN. Scipy's filters treat a NaN differently depending on where it falls in the window, and the result is that a real low on the Antarctic coast can silently vanish. And no letter is placed on ground above 1,000 m. In the tropics a high is also dropped when ground above 700 m lies within a degree of it, because the nightly cooling of a highland spills a fictitious high onto the coastal cells around it. Lows are exempt: the Afar heat low sits near sea level inside a ring of highlands and is real.
z_hi = np.where(mask, -np.inf, z) # for maximum_filter
z_lo = np.where(mask, np.inf, z) # for minimum_filter
The final step is the one that matters most. Each surviving candidate is refined on the raw, unsmoothed grid: the printed value and position are the true extreme within one degree of the candidate. Smoothing is right for the lines and wrong for the extremes. This way the L over a hurricane carries the model's own minimum, even when the core is only a few cells wide.
Keeping the animation steady
A center must also appear in the neighbouring hour, within 800 km, in either the frame before or the frame after. A letter that exists for a single frame is noise at the threshold, and dropping it keeps the animation calm. The rule is deliberately loose: a deep Atlantic low can move 500 km in a few hours, and a stricter test took its letter away while the rings were plainly there.
In the browser, the map is Leaflet. The image layers are created once and only swap their source between frames, and the lines are redrawn on a single canvas. A frame is shown only when its lines and fill have fully decoded. And the number labels on the lines stay in place as long as the same isobar still runs under them, so you can scrub through five days of weather on a phone and the numbers do not jump.
The map is at barometer.today/pressure-map. barometer.today is part of the SimpleMeteo family with uvi.today (UV index), pollen.today (pollen), airindex.today (air quality) and weatherjourney.com (climate history since 1940). All are free, with no accounts, cookies or ads.
Read original: https://dev.to/simplemeteo/how-barometertoday-draws-its-isobars-4ega
← Previous
My Wife’s Steam Deck Is a Server Now. She Doesn’t Know Yet. 🫠
Next →
The Application Shell: GtkApplicationWindow vs AdwApplicationWindow
Related
Tencent EdgeOne Makers: My Technical Review and Best Practices for Website Deployment
Frontend
0
DEV Community
I'm 15 and I got on the front page of Hacker News with my side project
Frontend
0
DEV Community
Our regex found 199 records in a 1,723-record corpus and reported no errors
Frontend
5
DEV Community
Our site served every URL the same 3,780 bytes, and Google believed it
Frontend
4
DEV Community
Comments0
No comments yet — be the first