Snippet Wednesday! - Add maps to your Automatic Reports ๐Ÿ—บ๏ธ

For this edition, I am building on @mslootweg 's snippet about adding plots to automatic reports by showing you how to add a map image!

Similarly to the snippet of Marcel, I will show you how to use plotly to turn a geojson file into a beautiful image.
Since you already know how to add a plot as an image, I will just show you how to turn the geojson into an image; the rest of the process is the same :sunglasses:

The Snippet
you will need a suitable geojson file, for this example you can download one from the internet. Also, you will require and an extra package called geopandas, so make sure to add it to your requirements.
(note: you may need to make some changes to the code to accommodate for the geojson that you are using)

:point_down:Click here to see the code :point_down:


   def make_plotly_map(self, params, **kwargs):
        with open(Path(__file__).parent / "countries.geojson") as f:
            geojson = json.load(f)

        geo_df = gpd.GeoDataFrame.from_features(geojson["features"])
        fig = px.choropleth_mapbox(geo_df,
                           geojson=geo_df.geometry,
                           locations=geo_df.index,
                           color=geo_df.fill,
                           opacity=0.3,
                           color_discrete_map={'your country': viktor_green_hex,
                                               'VIKTOR country' : viktor_blue_hex,
                                               'Other': viktor_yellow_hex},
                            mapbox_style="open-street-map",
                            title="VIKTOR developers around the world",
                            zoom=0.1      
                           )
        fig.update_layout(margin={"r":0,"t":0,"l":0,"b":0})
 

        return fig


What does this look like?
The final result of your plotly map will look something like this. As you can see, I was able to change some colors on the map too.


Some interesting things about this Choropleth plot is that you can choose between many map types, even satellite, and they are highly accurate. You can zoom in to streets and even see houses on most.
This does of course means that this map takes up quite a bit of memory so take care that you do not go over the limit!

I hope this snippet has inspired you and if you have any alternatives to making maps I am open for discussion on suggestions/feedback.

5 Likes

Nice post, @ThomasN !

To add to this, if you want to make an image of your MapView, you can extract the geojson from the MapResult. Here is a snippet of how you can set up your map view (taking the example provided in the documentation):

    def _get_map_view(self, params):
        # Create some points using coordinates
        markers = [
            MapPoint(25.7617, -80.1918, description='Miami'),
            MapPoint(18.4655, -66.1057, description='Puerto Rico'),
            MapPoint(32.3078, -64.7505, description='Bermudas')
        ]
        # Create a polygon
        polygon = MapPolygon(markers)

        # Visualize map
        features = markers + [polygon]
        return MapResult(features)

    @MapView('Map view', duration_guess=1)
    def get_map_view(self, params, **kwargs):
        return self._get_map_view(params)

Then, using the geojson property on the MapResult object, you can extract the geojson. Here is the snippet combining the code presented above:

    def make_plotly_map(self, params, **kwargs):
        geojson = self._get_map_view(params).geojson
        geo_df = gpd.GeoDataFrame.from_features(geojson["features"])
        # get average of bounds
        fig = px.choropleth_mapbox(geo_df,
                                   geojson=geo_df.geometry,
                                   locations=geo_df.index,
                                   color=geo_df.fill,
                                   opacity=0.3,
                                   mapbox_style="open-street-map",
                                   title="VIKTOR developers around the world",
                                   zoom=self._get_map_zoom(geo_df),
                                   center=self._get_map_center(geo_df),
                                   )
        fig.update_layout(margin={"r": 0, "t": 0, "l": 0, "b": 0})

        return fig

Notice that Iโ€™ve added methods to the zoom and center arguments. Here is the map center method:

    @staticmethod
    def _get_map_center(geo_dataframe):
        lat_min = geo_dataframe.bounds['miny'].min()
        lat_max = geo_dataframe.bounds['maxy'].max()
        lon_min = geo_dataframe.bounds['minx'].min()
        lon_max = geo_dataframe.bounds['maxx'].max()
        lat = (lat_min + lat_max) / 2
        lon = (lon_min + lon_max) / 2
        return {'lat': lat, 'lon': lon}

Basically, Iโ€™m taking the bounds of the geodataframe, and averaging them to get the center.

For the zoom, it was a bit more difficult, and definitely not accurate. For this Iโ€™ve applied my own crude algorithm (I recommend adjusting the constants if they do not fit the cases that you would apply it to):

    @staticmethod
    def _get_map_zoom(geo_dataframe):
        lat_min = geo_dataframe.bounds['miny'].min()
        lat_max = geo_dataframe.bounds['maxy'].max()
        lon_min = geo_dataframe.bounds['minx'].min()
        lon_max = geo_dataframe.bounds['maxx'].max()
        lat_diff = lat_max - lat_min
        lon_diff = lon_max - lon_min
        alpha = 20
        beta = 0.55
        # set up a simple algorithm based on two constants
        zoom = alpha / (max(lat_diff, lon_diff)) ** beta
        return zoom

Iโ€™ve only hacked this together quickly, and noticed that many things can be improved to making the maps similar. Hopefully we can get more developers onboard to get this snippet improved.

Here is my test case comparison:

MapView

Plotly image

1 Like

Did somebody actually get this working in a published app? Locally it works fine with plotly but when I publish it, the generation of the image takes endless and will not succeed.

I use the plotly function plotly_graph.write_image(<path>)

It uses the kaleido package under the hood. I found online that basically only version 0.1.0 works well with plotly. So when I install that, it works locally but not in a published app. We use plotly 5.24.1

Hi Vincent,

I just checked with the latest versions of plotly and kaleido on Python 3.13. The download is working both in development and published app:

import viktor as vkt

import geopandas as gpd
import plotly.express as px


class Parametrization(vkt.Parametrization):
    download = vkt.DownloadButton('Download', "download_plot")

class Controller(vkt.Controller):
    parametrization = Parametrization

    def _get_map_view(self, params):
        # Create some points using coordinates
        markers = [
            vkt.MapPoint(25.7617, -80.1918, description='Miami'),
            vkt.MapPoint(18.4655, -66.1057, description='Puerto Rico'),
            vkt.MapPoint(32.3078, -64.7505, description='Bermudas')
        ]
        # Create a polygon
        polygon = vkt.MapPolygon(markers)

        # Visualize map
        features = markers + [polygon]
        return vkt.MapResult(features)

    @vkt.MapView('Map view', duration_guess=1)
    def get_map_view(self, params, **kwargs):
        return self._get_map_view(params)
    
    def make_plotly_map(self, params, **kwargs):
        geojson = self._get_map_view(params).geojson
        geo_df = gpd.GeoDataFrame.from_features(geojson["features"])
        # get average of bounds
        fig = px.choropleth_mapbox(geo_df,
                                    geojson=geo_df.geometry,
                                    locations=geo_df.index,
                                    color=geo_df.fill,
                                    opacity=0.3,
                                    mapbox_style="open-street-map",
                                    title="VIKTOR developers around the world",
                                    zoom=self._get_map_zoom(geo_df),
                                    center=self._get_map_center(geo_df),
                                    )
        fig.update_layout(margin={"r": 0, "t": 0, "l": 0, "b": 0})

        return fig
    
    @staticmethod
    def _get_map_center(geo_dataframe):
        lat_min = geo_dataframe.bounds['miny'].min()
        lat_max = geo_dataframe.bounds['maxy'].max()
        lon_min = geo_dataframe.bounds['minx'].min()
        lon_max = geo_dataframe.bounds['maxx'].max()
        lat = (lat_min + lat_max) / 2
        lon = (lon_min + lon_max) / 2
        return {'lat': lat, 'lon': lon}

    @staticmethod
    def _get_map_zoom(geo_dataframe):
        lat_min = geo_dataframe.bounds['miny'].min()
        lat_max = geo_dataframe.bounds['maxy'].max()
        lon_min = geo_dataframe.bounds['minx'].min()
        lon_max = geo_dataframe.bounds['maxx'].max()
        lat_diff = lat_max - lat_min
        lon_diff = lon_max - lon_min
        alpha = 20
        beta = 0.55
        # set up a simple algorithm based on two constants
        zoom = alpha / (max(lat_diff, lon_diff)) ** beta
        return zoom
    
    def download_plot(self, params, **kwargs):
        fig = self.make_plotly_map(params)
        file = vkt.File()
        with file.open_binary() as f:
            fig.write_image(f)
        return vkt.DownloadResult(file, "map.png")

Also, for plotly to image conversion, you can refer to this post:

Thanks for that. That did the trick. The compute power on viktor is a bit to minimal to render large maps but atleast it works on smaller scale maps

Hi @Vincentvd ,

If the computational resource limit that we provide is too limiting for your case, please pick up contact with us.

1 Like