💸 Reduce costs by consolidating proprietary analytics & reporting software to open-source & Dash.
Challenge us to replace your analytics with Dash and reduce costs.

Chloroplethmapbox slow to render

Hi all,

I’ve been playing around with Plotly and Dash for the first time over the past few days, with the hope of developing a browser-based data explorer for geographic NetCDF4 data. I’ve been impressed at how straightforward this has been so far, however I’m finding that some interactions are taking longer to update and render than I’d hoped. I believe this may be the same issue discussed here: Choroplethmapbox update _very_ slow when deployed

The following refers to the code and sample data available here: https://cloudstor.aarnet.edu.au/plus/s/nGWTSu40ouBYbu9

# Create the callback and callback function (update_figure)
@app.callback(Output('plot', 'figure'),
              [Input('slide', 'value')],
              [State('plot','relayoutData'),State('plot', 'figure')])
def update_figure(x,r,f):
    t0 = tme.time()
    f['layout']['mapbox']['center']['lat'] = f['layout']['mapbox']['center']['lat']
    f['layout']['mapbox']['center']['lon'] = f['layout']['mapbox']['center']['lon']
    f['layout']['mapbox']['zoom'] = f['layout']['mapbox']['zoom']

    # If the map window has been panned or zoomed, grab those values for the new figure
    if r is not None:
        if 'mapbox.center' in r:
            f['layout']['mapbox']['center']['lat'] = r['mapbox.center']['lat']
            f['layout']['mapbox']['center']['lon'] = r['mapbox.center']['lon']
            f['layout']['mapbox']['zoom'] = r['mapbox.zoom']

    # Extract the new time values from the NetCDF file
    tmp = nc['temp'][x, -1, :, :].values.flatten()
    # Repace the Z values in the original figure with the updated values, leave everything else (e.g. cell geojson and max/min ranges) as-is
    f['data'][0]['z'] = np.where(np.isnan(tmp), None, tmp).tolist()
    print("update_figure() time: ",tme.time()-t0)
    return f

The source of my data comes from a 4D NetCDF4 file (in this case a model of ocean temperature - temp.nc) with dimensions of time, depth, lat and lon. In my case I’m only plotting a 2D chloropleth map, but I’d like the user to interactively select the desired time interval (and eventually depth) as well (the render will always be in 2D space).

Using the examples from https://plotly.com/python/mapbox-county-choropleth/, I’m using a geojson file of the 2D grid cells coupled with a Pandas DataFrame to render the ocean temperature. Everything is working as expected, however any changes to the slider value (time) take a long time to update (approx six seconds on my machine). It appears as though there’s a second or so between selecting the slider value and running the update_figure() callback, then another 4-5 seconds before the new render starts to take place in the browser.

The update_figure() callback reads the requested data directly from the NetCDF4 file, then directly updates the Z values in the existing figure dictionary and returns this as a new figure. At first I was concerned that the slow response time was due to reading from the NetCDF4, however a basic timing function shows that the update_figure() callback runs in less than 0.01 seconds in most cases. So it appears the delay is either coming from the @app.callback or the render function (post update_figure() in Dash?

I suspect that the slow render times are somehow related to the geojson of each cell polygon (47k grid cell polygons are being rendered in total, with each polygon being defined by 6 points (i.e. 284k points total)), and unfortunately this can’t be simplified any further. It was also unclear to me from the documentation whether chloroplethmapbox uses WebGL for rendering? If it’s using SVG then this would explain the slow times with this number of geojson polygons.

As far as I understand, I can’t really implement a client side callback here as the data needs to be read directly out of the NetCDF file using xarray.

Can anyone see/explain what’s causing the slow render times when new slider values are selected? Any assistance to understand (and possible workarounds) would be amazing.

Thanks in advance.

Have you tried Dash Leaflet?

https://dash-leaflet.herokuapp.com/#us_states

I haven’t tested with that large geojson files though, so am not sure if the performance is better or worse.

EDIT: I just had a look at your code. I seems that you are plotting something that looks like a raster? Hence, you would probably be able to achieve a much better performance if you plotted it as an image overlay rathat than a GeoJSON object.

Thanks for your suggestion Emil.

Unfortunately an image approach won’t work in this situation, as the dataset needs to be explorable by the user (e.g. hover values need to be present and must have the ability to select a series of grid cells for further analysis using the select tools). As far as I’m aware, an image render won’t support this.

I have briefly tried an approach with Leaflet today but am seeing similar delays in updating as new time slider values are selected.

Perhaps there’s a way of getting the callback to just return the new Z values, then combining those with the existing geojson on the client side?

Depending on the setup, an image overlay approach (e.g. using GeoTIFF) does not rule out hover values. The selection might be more challenging, but not impossible.

It should be possible to pass only the Z values to the geojson overlay (at least in Leaflet), but there are still way too many polygons for the map to be usable (at least on my laptop).

I took a quick look at the WebGL options for Leaflet, and while there are some options, it would require some work to port them to Dash.

EDIT: Do you know if your data are on a regular grid? And if yes, regular in which coordinate projection? If no, would it be possible to interpolate the data onto a regular grid as a pre processing step?

Hi @jbeardsley welcome to the forum! A possible solution would be to display a raster version when zoomed out (at low resolution) and to switch to drawing the polygons with Choroplethmapbox when the zoom level is higher and there are less polygons to draw.