How to Render Interactive Weather Models Entirely in the Browser | by Thomas Horner | Towards Data Science

Weather enthusiasts, meteorologists, and storm chasers are aware of the myriad of weather model visualization websites out on the web. These platforms allow you to view various parameters, levels, and forecast hours of dozens of different weather models. For example, when preparing for a day of storm chasing, a chaser may look at a few parameters for a certain area — the dewpoint in the morning, the CAPE later in the day, and the HRRR’s simulated reflectivity in the afternoon. Menus, buttons, and sliders are usually provided to switch forecast hours, models, and parameters.
Typically, all of these weather model viewers are simply ways for the user to access pre-rendered, nonspatial weather models, despite the original data being geospatial in nature. Though interfaces differ, many things are consistent in weather model viewers:
Geographic regions are fixed. You can only view preset areas, such as from a list of cities, states, or regions. The output images are fixed in size as well. A few viewers let you zoom in to certain areas and wait for a new render.
Geographic layers and information are scarce or non-existent. You may only have roads or counties visible on top of the weather model image, which can make specific areas difficult to pinpoint.
Many providers have viewers that are generally non-queryable, which means to get the actual value for any given point you need to compare the color of the data against the legend.
Model layer combinations and color schemes are static. You cannot overlay, for instance, wind streamlines at a certain height over another variable, unless the provider has made that combination available.
Though many of the providers have put serious care and thought into the presentation of the data, the aforementioned shortcomings can be significant and aggravating in certain circumstances. For instance, evaluating forecasted snowfall totals in mountainous terrain can be incredibly challenging if you just have county boundaries to orient yourself with, considering that snowfall totals may vary by tens of inches over just a few miles.
Consider the below image:
Even as a forecaster with a geography degree I struggle with getting useful data out of this graphic. Looking at the state of Colorado:
Where is Steamboat in relation to that yellow isoband in north Colorado? Is it in the blue or in the green? (this would be the difference between 2" and 6" of snow).On I-70, how does snowfall distribution change from the east of Vail Pass to west of it?How much snow is the Gore Range getting compared to the Tenmile Range to its immediate south?
Where is Steamboat in relation to that yellow isoband in north Colorado? Is it in the blue or in the green? (this would be the difference between 2" and 6" of snow).
On I-70, how does snowfall distribution change from the east of Vail Pass to west of it?
How much snow is the Gore Range getting compared to the Tenmile Range to its immediate south?
If you do enough forecasting in Colorado, you’ll eventually have the county outlines vs. mountain range relationship memorized down to the mile, because that’s usually all you have to work with when looking at the distribution of weather across the state on many model viewers.
That said, it is important to mention the benefits of these platforms — the most obvious being the use of pre-rendered static images. These are fast and it is easy to load all forecast hours of a model in just a few seconds, and you can often easily have the server generate an animated GIF of the data. The fixed regions prevent ‘misuse’ of the data — such as zooming in to a resolution beyond what the weather model is suited for. These two factors allow for consistent weather model visualization, which is helpful if you are downloading the images and archiving them for comparison or redistribution.
In a previous article, I discussed how you can build and automate your own weather model visualizer. This technique had an interesting twist in that, though the server is still rendering the data, it is serving it as a geospatial layer (OGC WMS) that can be added to interactive web maps. This strikes an nice balance between the performance of pre-rendered static images and the interactivity of fully geospatial data. I will show you how to take this one step farther and do the rendering entirely in the browser, no server needed! Per my link above, this article will assume you have the weather models available in GeoTIFF format and projected to EPSG:4326. You can convert raw weather model data (in GRIB2 format) to GeoTIFF with just one GDAL command.
The disadvantages of rendering weather model data on the client, using a web browser, are plentiful — but there are some potential use cases for the technology. Web development in the year 2020 seems to be just mature and powerful enough to make this an actually viable proposition for certain purposes. Here are the main issues we’ll encounter:
Slowness. JavaScript is not an ideal language to turn raw multidimensional rasters into RGB images or vector isobands.
Data use. The raw weather model data is not as compact as a simple JPEG.
Compatibility. The final code we write wont work in Internet Explorer, and it will perform miserably on older hardware.
In contrast, here are some benefits we will enjoy:
The ability to present the data on a fully interactive web map, which means we can add as many layers of geographic data (or other model parameters) as we want, and style it however we want, in real-time.
The data can be accessed directly, such as to query points or areas for the raw model data. This goes beyond just clicking on a map and getting the data at a point — a use case may be the ability to define a list of locations or set of points that present the data in a tabular format or overlaid on the map.
The ability to render weather model data, or combinations of the data, in any way we choose — as a raster, as isobands, as isolines (isopleths/contours), as streamlines, etc. The rendering, styling (for instance, width of the contours), and color bands can all be modified by the user (if desired) and update the visualization instantaneously. Various models can even be combined mathematically to produce specific visualizations, instead of having to set up the automated processing scripts to do so ahead of time.
We also have a few strategies to alleviate some of the pain points:
Use web workers to process and render the GeoTIFFs. This will spread the intensive calculations across the various cores of the user’s CPU.
Use cloud-optimized GeoTIFFs (COGs) or the WCS standard to stream the raw data to the user only for the region and scale they are interested in. In addition, applying appropriate compression to the GeoTIFFs can bring them down to reasonable sizes.
With those points in mind, can you think of some use cases where you might enjoy the benefits of client-side rendering? For me, the quality of the visualizations comes immediately to mind. I used to have custom QGIS projects to export nice-looking maps with weather model data, but there was still manual labor involved. With a well-presented web browser visualization platform, I can do the analysis across multiple models, parameters, and forecast hours quickly while being able to export the weather model graphic and map for redistribution with just a click.
There were also some specific issues I encountered in my previously-mentioned article about using MapServer to distribute the weather model data. These issues were all related to visualization products where the graphics are heavily removed from the data itself, requiring various rasters to be rendered into a final RGB image (for example, streamlines or wind barbs on top of a wind speed raster) — which is incompatible with the scale and styling agnostic model data distribution strategy model I intended to employ with MapServer. These issues were fairly trivial to tackle when handled in the web browser, however:
Weather model combination calculations. Several popular weather model visualizations are not possible simply by distributing the converted data without rendering an RGB image. The best example is Composite Reflectivity + Precipitation Type, which shows the simulated radar as you might expect — rain shows up with the typical color scale of green-yellow-red, while snow displays as shades of blue. This actually requires a combination of at least three different weather model parameter rasters, or five if you want to handle all precipitation types (ice pellets and frozen rain). There’s no way that I know of to do this on-the-fly in MapServer, as different color scales need to be used based on the precipitation type and combined into the final image. A more simple example is that wind direction in most models is not a single parameter, but instead two rasters (a horizontal and vertical vector component) that need to be combined with trigonometry to calculate the actual direction and speed.
Wind streamlines. MapServer is capable of producing contours from rasters, or even directional arrows across a raster field, but streamlines are not possible. Yet many popular products rely on streamlines to show things like mid-atmospheric winds.
One thing you may have noticed in the above examples is that, while serviceable, the visualizations don’t look that great. The images are small and the data is cramped, which doesn’t make things easy for forecasters. They’re even more troublesome when redistributed to weather forecast readers, who are likely not data geeks and just want to know where they should ski next weekend. I suppose one could concede that the pixelated, late-90s aesthetic of them does give a sense of pure data authority, and they certainly don’t waste any time on doing anything besides conveying the raw data.
Let’s take a look at what weather models may look like when rendered entirely in the web browser. These were taken from my website and are used in my forecasts. First off, here’s the interface I built for it in Angular:
Some example output images:
Now, let’s zoom in on the interactive map and enable some other layers. When I say adjustable, I mean this can be done in real-time, by the user, on the webpage:
How about a layer with streamlines?
We even have the dreaded Composite Reflectivity + Type layer working properly. You can see the snow as dark blues over the Rockies, with very light rain as greys (and, well, sorry about the slightly misleading light blue color) in Nebraska. This was all calculated in the browser.
Here are the main steps needed to accomplish client-side weather model rendering, once you have your web map in place. I used Leaflet, but ended up needing to extend it so much that OpenLayers may likely be more suitable if you’re starting from scratch.
Fetch the GeoTIFF data for the appropriate model, timestamp, parameter, level, and forecast hour, and read the stream as an ArrayBuffer.Parse the ArrayBuffer to get the geographic information and a nested array of raw data. Geotiff.js is the go-to library for this, although it does not play nicely with Angular unless you import the minified version. You’ll want to cache this data in case you want to make queries against it or re-render the visualizations.Pull out the appropriate band’s data (for multidimensional data, such as multiple forecast hours in one TIFF or multiple parameters in one) and use web workers to run a marching squares algorithm on the nested data array which you can use to produce isobands (polygons) or isolines (polylines), depending on if you want filled/colorized data or contours. You can use a library like raster-marching-squares. For streamlines, you can use the raster-streamlines library. This is the most computationally-expensive step. You’re going to want to cache all this data once it is calculated.Apply the necessary geographic transform to the isobands and isolines as you create them, and then supply them to your web map. For instance, in Leaflet, I make this data available as an L.GeoJSON layer. Make sure to supply the value of the data to each band or line so that it can be styled appropriately. Add it to the map.Add other functionality, such as point querying and visualization download (“Save as JPEG”).
Fetch the GeoTIFF data for the appropriate model, timestamp, parameter, level, and forecast hour, and read the stream as an ArrayBuffer.
Parse the ArrayBuffer to get the geographic information and a nested array of raw data. Geotiff.js is the go-to library for this, although it does not play nicely with Angular unless you import the minified version. You’ll want to cache this data in case you want to make queries against it or re-render the visualizations.
Pull out the appropriate band’s data (for multidimensional data, such as multiple forecast hours in one TIFF or multiple parameters in one) and use web workers to run a marching squares algorithm on the nested data array which you can use to produce isobands (polygons) or isolines (polylines), depending on if you want filled/colorized data or contours. You can use a library like raster-marching-squares. For streamlines, you can use the raster-streamlines library. This is the most computationally-expensive step. You’re going to want to cache all this data once it is calculated.
Apply the necessary geographic transform to the isobands and isolines as you create them, and then supply them to your web map. For instance, in Leaflet, I make this data available as an L.GeoJSON layer. Make sure to supply the value of the data to each band or line so that it can be styled appropriately. Add it to the map.
Add other functionality, such as point querying and visualization download (“Save as JPEG”).
Let’s dig in and go over specifics! I will be using TypeScript and ES6 syntax direct from my codebase, which uses the Angular framework.
This is the easiest part. Some strategies might be:
Simply have the files downloadable from a web server.
Create an API that retrieves the appropriate file for the model, parameter, forecast hour, geographic region, etc.
✔️ Use Cloud-Optimized GeoTIFFs
Use MapServer or another geographic data server to handle WCS requests and return data as a GeoTIFF.
Think about how you want your GeoTIFFs to be structured. To save on data, you likely want one TIFF returned per model, parameter, level, forecast hour, etc. However, you could store multiple forecast hours as several bands in one TIFF if the client wants to preload everything (for instance, to use a slider to view all the forecast hours). This will create an initial, longer download time but speed up overall parsing time slightly.
Don’t forget about compression. Though you can use JPEG compression on TIFFs to get their filesize down to, well, something comparable a JPEG’s — this isn’t responsible use of the data. These are not images, but instead arrays of raw data, the integrity of which should be respected before they are rendered. Also, I don’t think the geotiff.js library can handle that method of compression. I have found that a high level (zlevel = 9) of deflate reduced sizes by up to 50% without impacting parsing performance too badly.
You’ll likely retrieve the GeoTIFF with fetch or using the built-in functions of geotiff.js. The library can natively read cloud-optimized GeoTIFFs! If using a regular GeoTIFF, you’ll need to read the fetch request stream (if you made one) as an ArrayBuffer :
const response = await fetch( <the url of your tiff> );const arrayBuffer = await response.arrayBuffer();
Following the above example, we can use geotiff.js to parse the ArrayBuffer into a GeoTiff object. We can retrieve the actual image data itself from that object:
const geoTiff = await fromArrayBuffer( arrayBuffer );const geoTiffImage = await geoTiff.getImage();
Don’t forget to grab the image metadata and geographic data as well, we’ll need it later:
const tiffWidth = geoTiffImage.getWidth();const tiffHeight = geoTiffImage.getHeight();const tiepoint = geoTiffImage.getTiePoints()[ 0 ];const pixelScale = geoTiffImage.getFileDirectory().ModelPixelScale;const geoTransform = [     tiepoint.x,     pixelScale[ 0 ],     0,     tiepoint.y,     0,     -1 * pixelScale[ 1 ] ];
If you have multiple bands in the image, you can get the exact number of them easily:
const bands = await geoTiffImage.getSamplesPerPixel();
Besides wind barbs (which can be nice for weather forecasters, but pretty useless for the general public), I can’t think of any other ways of rendering the weather model data besides isobands, isolines, or streamlines. Isobands are polygons that pertain to specific ranges of the data. You can use them to generate colorful temperature or snow accumulation maps. Isolines (or isopleths) are contour lines drawn at the boundary of data ranges. Streamlines help visualize data that has both a velocity and a direction. These can all be created as vectors and added to the map.
We will need to crack the GeoTIFF data open to build our geographic vectors. You can also just render it as an image to a canvas, but I wont cover that here (see the Acknowledgments section).
Get the image data for the appropriate band (where i is the band number):
const bandData = await geoTiffImage.readRasters({ samples: [ i ] });
Build a nested array of the raw data:
const dataArr = new Array( tiffHeight );for ( let i = 0; i < tiffHeight; i++ ) {    dataArr[ i ] = new Array( tiffWidth );    for ( let ii = 0; ii < tiffWidth; ii++ ) {        dataArr[ i ][ ii ] = bandData[ 0 ][ ii + i * tiffWidth ];    }}
We’re going to use the raster-marching-squares library to generate our features, but for performance purposes I want to run it as a web worker so that the user interface doesn’t lock up. If you’re using Angular, it is easy to set a web worker up with Angular-CLI by running ng generate web-worker.
Inside of the web worker, we’ll import raster-marching-squares as rastertools:
import * as rastertools from 'raster-marching-squares';
This provides isoband and isoline rendering as rastertools.isobands() or rastertools.isolines(). Both functions expect the following arguments:
The nested data array (dataArr)
The projection of the raster (geoTransform)
The intervals to break / space the data at. These intervals will always be present in the output from these functions, regardless of if there are actually any features in them or not. For instance, if you passed [-5,0,5] as the interval for a temperature model, you would receive three isobands or isolines pertaining to -5 degrees, 0 degrees, and 5 degrees. If the weather model had data within those ranges, those isolines or isobands would have geometry containing those regions. If not, they would be empty features.
We’ll eventually want to style the isobands or put labels on the isolines, so one thing I did was set up color scales for all of the various parameters I wanted to visualize, passing a color for a specific value. I then set up a function that would produce a fixed number of intervals (such as 100 — smooth, but takes awhile to calculate) from the color scale and use tinygradient to produce the CSS representation of that color scale to use for the legend. For instance, the color scale for snow accumulation looks like this:
"SnowAccumM": [    { value: 1.8288, color: "#ffcc01" },    { value: 1.2192, color: "#ff4501" },    { value: 0.6096, color: "#e51298" },    { value: 0.3048, color: "#9f0ded" },    { value: 0.0762, color: "#5993ff" },    { value: 0.00254, color: "#d6dadd" },    { value: 0, color: "#ffffff" }],
The interval creation function also bounds the intervals array with Number.MAX_SAFE_INTEGER and its negative equivalent. If you do it right, and throw some unit conversion in there, you can handle the isoband creation, styling, and legend in one compact class.
The worker that will run the isobands or isolines function will listen for the three necessary arguments. We’ll also pass in a type parameter to tell it which function to use:
addEventListener( 'message', ( { data } ) => {    try {        const features = rastertools[type](             data.data,             data.geoTransform,             data.intervals         );        postMessage ( { features } );    }    catch ( err ) {        postMessage( { error: err.message } );    }}
The function has been wrapped in a try...catch, which is a nice idea, but useless in practice, because when the function screws up, it tends to just get stuck in a do...while loop until you run out of memory. This is likely due to some of the thousands of weather model rasters I convert a day getting corrupted, which I need to fix but...pull request anyone?
Now we can call the worker, pass it the necessary arguments, and wait for a response. You don’t have to use a Promise, it just suited me for my particular workflow:
const type = 'isobands';const rastertoolsWorker = new Worker(    `../workers/rastertools.worker`,     // the worker class ☭    { type: `module` } );new Promise( ( resolve, reject ) => {    rastertoolsWorker.postMessage( {         dataArr,         geoTransform,         intervals,      // array of numbers        type     } );    rastertoolsWorker.addEventListener(         'message',         event => resolve( event.data )    );} ).then (...)          // the isobands are generated!
If you did 100 intervals, this might take a little while. Your laptop fan might kick on with surprising ferocity.
Note that I did not mention streamlines. If you use the raster-streamlines library, this calculation is refreshingly fast and likely doesn’t need to be put in a web worker.
If you used raster-marching-squares, this part is all taken care of for you. Basically, the vector features are generated from the rasters, and need to be tied back to where those pixels are actually located geographically. We can use the geoTransform provided by geotiff.js to get that transformation information (and hopefully your GeoTIFF was originally in EPSG:4326). We also want the information as a GeoJSON, which is an agnostic collection of geometries. Again, the library provides this collection, we just need to wrap it with L.geoJSON(<the features>) to create a layer we can add to the map. raster-streamlines works the same way.
The library also makes available the isoband’s data range in the properties of each feature. We can use this to style the isobands or add labels to the isolines. This is the other tricky part of the process, which I’ll cover shortly.
You also want to think about what renderer you want the web map to use. By default, Leaflet uses SVG. If you have huge, detailed isobands, performance will be abysmal. I instead opted to use a canvas-based renderer instead, which comes with a few headaches, but allows for acceptable performance once the features have been drawn onto the canvas.
We’re ready to style our layers now.
These are the easiest to style. You just need to pass a style function:
L.geoJSON(<the features>, { style: feature => {    return {        fillColor: getColor( feature.properties[ 0 ].lowerValue ),        weight: 1.5,        opacity: 1,        color: getColor( feature.properties[ 0 ].lowerValue ),        fillOpacity: 1    };});
getColor() is a function I wrote that takes a value and gets the interpolated color (using tinygradient) from the appropriate color scale as mentioned before. feature.properties has been populated with the value range, thanks to raster-marching-squares.
You’ll notice I also set the stroke/outline weight to 1.5, as there are often tiny slivers in between the polygons if they aren’t given outlines. With outlines in place, an important caveat is that if you lower the opacity of the layer (via the style function), the overlap of the fills and outlines will become noticeable. Instead, you’ll need to have the layer on its own map pane and change the opacity of the entire pane.
Styling the isolines themselves is super easy:
return feature => {    return {        weight: 1.5,        color: '#000000'    }}
But what about labels? This is actually very tricky. Since we are using a canvas renderer, the Leaflet plugin for drawing polyline labels is out of the question, as it only works on SVG. I don’t want to use different renderers for each layer either, in the case of overlaying contours/isolines onto isobands.
In this case, I found an awkwardly-named plugin (Leaflet.streetlabels) which does draw labels along polylines in the canvas renderer, by actually extending the renderer (so it is not a per-layer solution). It has a couple dependencies (one of which isn’t available via npm) so getting it set up was not straightforward.
There are a number of modifications necessary to get labels working properly.
By default, the plugin cant parse feature.properties as the isolines function places it in an array. It’s looking for a key, so it’s expecting feature.properties to be an object.The plugin has a filter so that it doesn’t attempt to draw labels on all features, just the ones with an appropriate properties object. For some reason, I think maybe due to how GeoJSON files can be found in the wild, it checks if the property is undefined by checking if the property is set to the string value 'undefined'. So if the key does not exist at all, it still tries to draw on the feature, which produces an error. I needed to modify the plugin to do this (and you can take care of issue #1 in the process).Drawing issues. The big issue is that it loves to draw labels upside down (even in the screenshot on its GitHub page), which is bad cartography. A smaller issue is that the labels follow the curve of the contours, which is nice, except on sharp curves where the label becomes unreadable.
By default, the plugin cant parse feature.properties as the isolines function places it in an array. It’s looking for a key, so it’s expecting feature.properties to be an object.
The plugin has a filter so that it doesn’t attempt to draw labels on all features, just the ones with an appropriate properties object. For some reason, I think maybe due to how GeoJSON files can be found in the wild, it checks if the property is undefined by checking if the property is set to the string value 'undefined'. So if the key does not exist at all, it still tries to draw on the feature, which produces an error. I needed to modify the plugin to do this (and you can take care of issue #1 in the process).
Drawing issues. The big issue is that it loves to draw labels upside down (even in the screenshot on its GitHub page), which is bad cartography. A smaller issue is that the labels follow the curve of the contours, which is nice, except on sharp curves where the label becomes unreadable.
In the end, I downloaded the code and prerequisites into my codebase so I could modify the plugin to filter and read from the feature properties more effectively. To fix the upside down label issue, I modified the base dependency (Canvas-TextPath) to flip all letters 180 degrees if they were upside down, while reversing their order. I also put a heavy clamp on the maximum amount of contour-following rotation, so that it didn’t create unreadable squished labels on curvy lines. Effective contour labeling is debatable — there’s not necessarily anything wrong with perfectly horizontal labels bisecting a contour line, either.
There were also some ‘todo’ comments about setting a custom font so I threw Roboto into the canvas draw commands to match the rest of my website’s layout. Maybe I’ll put a pull request out there some day :)
For point #1 listed above, we can circumvent the issue with arrays by doing a bit of work in the initialization of our isobands GeoJSON layer:
L.geoJSON( ..., {    style,    onEachFeature: ( feature, layer ) => {        if ( !feature.properties[ 0 ] ) return;        const contour = feature.properties[ 0 ].value;        feature.properties = {            contour // probably want to convert unit, add suffix :)        }    }}
Finally, in the initialization of the renderer I made it filter out shapes with paths that were too small (squished labels), turn on label collisions, and set the fill/stroke colors. This is where TypeScript gets annoying, as no definitions exist for these sketchy (as much as I appreciate their existence) plugins.
const options: any = {    collisionFlg: true,    propertyName: 'contour',    fontStyle: {        ...    }}return new ( L as any ).ContourLabels( options );
That was all very annoying and frighteningly kludgy, but the renderer actually works nicely when you get it going — it redraws the labels as you pan and zoom.
Streamlines are a little easier as there is a Leaflet plugin that allows for arbitrary shapes or markers to be placed along polylines (not text though, which is why we had to go with the above strategy for contour lines).
The plugin is Leaflet.PolylineDecorator and it does its job pretty well. Once you generate the streamline layer, you can parse it for the LineString features the plugin expects:
const lines = layer.getLayers()    .filter( ( layer: any ) => {         return layer.feature.geometry.type === "LineString";     } )    .map( (layer: any ) => {        const coordinates = layer.feature.geometry.coordinates;        coordinates.forEach( coordinate => {             coordinate.reverse();         } );        return L.polyline( coordinates );    } );
Yes, you can see I have some typing issues in my codebase.
We’ll also need to set up the pattern for the decorator, which is probably going to be some sort of arrow along the line. I guess the plugin handles rotation of the decorator automatically. Nice.
const pattern = {    offset: '10%',    repeat: '20%',    symbol: L.Symbol.arrowHead( {        pixelSize: 8,        polygon: true,        pathOptions: {            stroke: false,            fill: true,            fillColor: '#000000',            fillOpacity: opacity        }     } )}
Now pass it all to the plugin:
const streamlineArrows = L.polylineDecorator( lines, {    patterns: [ pattern ]} );
Finally, you’ll probably want to combine the decorator with the streamline layer itself (let’s call it layer) into a single LayerGroup, for easy management.
L.layerGroup ([layer, streamlineArrows]);
Done! The only issues I encountered is that the plugin always puts the decorator on the overlay map pane (even when I try to circumvent that), which means if you have pane-based layer ordering, you’ll encounter some z-index problems. Maybe another pull request is in order...
Our map works great now, provided you extrapolated from the example code above. But what if we want to distribute the visualizations? We can of course use the Snipping Tool (or equivalent) to manually take a screenshot of the map, but that’s inconsistent. Unfortunately, we just don’t have any great options for downloading the map as we see it, as not everything is rendered onto the canvas (tooltips, annotations, etc.).
Keeping in the spirit of keeping things ̶s̶k̶e̶t̶c̶h̶y̶ ...I mean, client-side, we can further densify our node_modules directory towards a critical mass and use a library like dom-to-image to hopefully render the entire DOM tree of our map container as an image. This actually works pretty well out-of-the-box, but chokes on external web fonts and map layers that use CORS, so be wary.
Once you solve those issues, you can render the map pretty seamlessly, but you’ll need need file-saver to actually save the image to the disk.
import domtoimage from 'dom-to-image';import { saveAs } from 'file-saver';const image = await domtoimage.toJpeg( document.getElementsByClassName( 'map-container' )[ 0 ], { bgcolor: '#ffffff' } )saveAs( image, `my_weather_model_map_works.jpg` );
The performance of domtoimage is not great, so if you’re hoping to automate animated GIF creation, this doesn’t seem like a viable approach, although there is perhaps a way to do it with web workers. Unfortunately, we’ve definitely run into the brick wall of performance constraints on doing everything with JavaScript.
There’s lots of other things you can do with the data, perhaps the most famous example being those fancy animated wind maps. You can query it (hint: use the geoTransform), generate statistics, write a Discord server bot... But of course, you can do this all even easier on a server. Where the client-side rendering shines is in the visualization and interactivity of the weather models. Outside of that, it is difficult to justify the overhead and performance concerns of doing everything in the web browser.
This is certainly not the only way to go about things. Check out what is being done Leaflet.CanvasLayer.Field, for example. The potential of this technology is quickly developing and I’m sure in the next few years more robust and performant tools will become available. In fact, there are even WebAssembly wrappers for GDAL!
I’d like to thank Roger Veciana i Rovira for providing the wonderful resources and libraries that showed me this was even possible, and let me start building right away without having to write low-level raster processing algorithms.
If you attempt to dig up my website, it’s not production-ready and none of the clientside rendering is even available. However, all code and example images come from a working test version. One day...