Dash Leaflet - Geojson Popup Sizing

Hello,

I am using dash leaflet to visualize locations on a map. I am utilizing the dl.GeoJSON class to prepare my data, as it supports a great range of functionality that I need (clustering, hover labels and also pop ups). I am having a bit of trouble getting the pop ups to work as I had hoped for.

For every location, I have an image (named by the location ID) which I want to show on click of the marker. These images are contained in the asset folder of my dash application, and integrating them to the map is very easy with all the nice dash leaflet functionality. For the geojson data attribute, I simply create the following dictionary

[
        dict(
            lat=row["latitude"],
            lon=row["longitude"],
            ...
            popup= f'<img src="/assets/pictures/{row["id"]}.png">'
        )
        for _, row in data.iterrows()
]

The issue I have is that I don’t want to set a fixed widthon the popup HTML, since all pictures have different dimensions. I would want them to be displayed however large they are. But if I just specify the geojson as above (i.e. without setting a fixed width for the popup image), on initial load of the map, the width of the container for the popup HTML does not scale with the image size.

Looking at the CSS, I can see that the width of the “leaflet-popup-content” div is set to 51px, which is much smaller than the image.

<div class="leaflet-popup-content-wrapper">
  <div class="leaflet-popup-content" style="width: 51px;">
    <img src="/assets/<some_id>.png">
  </div>
</div>

If I click on that same marker again, the container is scaled correctly .

I guess the information on the width of the image is somehow not available initially when the container is drawn, but I would assume this to be quite a common use case to have images of varying sizes where I don’t want to fix the width.

Setting the following CSS

.leaflet-popup-content {
    width: auto !important;
}

does yield a sensibly sized container, but then the popup shows up in the wrong location the first time I click on it (on the second click, it is again ok)

Does anyone have an idea on what is the correct way to do this (preferrable using only CSS/plotly dash python components and no self-written javascript if possible)?

The simple approach would be to set the size configuration within the component you have within the popup like:

from dash import Dash, html, dcc, _dash_renderer
import dash_leaflet as dl
from dash.dependencies import Input, Output
import plotly.express as px
import pandas as pd
import numpy as np
import dash_mantine_components as dmc
from dash_pannellum import DashPannellum

_dash_renderer._set_react_version("18.2.0")

partial_panorama_config = {
    "title": "Example Panorama",
                    "hfov": 110,
                    "pitch": -3,
                    "yaw": 117,
                    "type": "equirectangular",
                    "panorama": "https://pannellum.org/images/from-tree.jpg"
}
# Initialize the Dash app
app = Dash(__name__, external_stylesheets=dmc.styles.ALL)

data = [
    {"month": "January", "Smartphones": 1200, "Laptops": 900, "Tablets": 200},
    {"month": "February", "Smartphones": 1900, "Laptops": 1200, "Tablets": 400},
    {"month": "March", "Smartphones": 400, "Laptops": 1000, "Tablets": 200},
    {"month": "April", "Smartphones": 1000, "Laptops": 200, "Tablets": 800},
    {"month": "May", "Smartphones": 800, "Laptops": 1400, "Tablets": 1200},
    {"month": "June", "Smartphones": 750, "Laptops": 600, "Tablets": 1000}
]

# Create sample data for the popup graph
def generate_sample_data():
    dates = pd.date_range(start='2024-01-01', end='2024-12-31', freq='ME')
    values = np.random.normal(100, 15, len(dates))
    df = pd.DataFrame({
        'Date': dates,
        'Value': values
    })
    return df

# Create the plotly figure
def create_popup_figure():
    df = generate_sample_data()
    fig = px.line(df, x='Date', y='Value', title='Monthly Trends')
    fig.update_layout(
        margin=dict(l=0, r=0, t=30, b=0),
        height=200,
        width=300
    )
    return fig

# Define the layout
app.layout = dmc.MantineProvider([
    dl.Map([
        dl.TileLayer(),
        dl.Marker(
            position=[38.7128, -74.0060],  # New York City coordinates
            children=[
                dl.Popup(children=[
                    dcc.Graph(figure=create_popup_figure())
                ])
            ]
        ),
        dl.Marker(
            position=[40.7128, -74.0060],  # New York City coordinates
            children=[
                dl.Popup(children=[DashPannellum(
    id='partial-panorama-component',
    tour={"default": {"firstScene": "scene1"}, "scenes": {"scene1": partial_panorama_config}},
    width='700px',
    height='400px',
    autoLoad=True,
)], closeOnClick=True, maxWidth=700
                )
            ]
        )
    ], style={'width': '100%', 'height': '100vh'}, center=[40.7128, -74.0060], zoom=12)
])

# Run the app
if __name__ == '__main__':
    app.run_server(debug=True, port=3253)

Another approach you can do is to set this up with an assign javascript function that passes through a more dynamic GeoJson render like:

point_to_layer_js = assign(
    """
function(feature, latlng){
    const iconData = feature.properties.icon_data;
    const flag = L.icon({
        iconUrl: iconData.url, 
        iconSize: [35, 35],
        tooltipAnchor: [20, 0]
    });
    const marker = L.marker(latlng, {icon: flag});
    marker.bindTooltip('<img src="' + feature.properties.image + '" style="width: 30vw; height: 20vh; max-width: 250px; max-height: 250px;">');
    marker.on('click', function(e) {
        var event = new CustomEvent('marker_click', {detail: feature.properties.pk});
        window.dispatchEvent(event);
    });
    return marker;
}
"""
)

dl.Map(

                    [
                        dl.TileLayer(
                            url="https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}",
                        ),
                        dl.GeoJSON(
                            data=geojson_dict,
                            id="locations_layer",
                            cluster=True,
                            zoomToBoundsOnClick=True,
                            superClusterOptions=dict(radius=40),
                            hideout=dict(
                                m_type_colors=m_type_colors,
                                circleOptions=dict(fillOpacity=0.5, stroke=False, radius=3),
                                min=0,
                            ),
                            pointToLayer=point_to_layer_js,
                            clusterToLayer=cluster_to_layer,
                        ),
                        dl.EasyButton(
                            icon="fa-search",
                            title="Search Map",
                            id="search_map_display_btn",
                            n_clicks=1,
                        ),
                        dl.EasyButton(
                            icon="fa-list",
                            title="Show / Hide Ledger",
                            id="show_or_hide_ledger_btn",
                            n_clicks=1,
                        ),
                        legend
                    ],
                    style={
                        "position": "absolute",
                        "width": "100%",
                        "height": "92%",
                        "left": "0",
                        "z-index": 0,
                    },
                    attributionControl=False,
                    className="stylecursor",
                    center=[27.94093, -97.20840],
                    zoom=10,
                    id="map",
                )

This approach is a bit more complex but I basically focused on that pointToLayer prop in the geojson prop and creating custom javascript with the marker.bindTooltip('<img src="' + feature.properties.image + '" style="width: 30vw; height: 20vh; max-width: 250px; max-height: 250px;">');

Hello @PipInstallPython,

thanks a lot for the reply. I am not sure it solves my problem though, but maybe I am misunderstanding you. One important requirement is that I would like to keep using Geojson as basis for the markers of my map as in the past it seemed to be super flexible and it supports everything I need (clustering, hoverlabels and pop-ups) out of the box. I think especially for clustering, using geojson is the way to go. The second question I have is that it seems to me that both your solutions actually fix the width of the popup image which is what I don’t want to do. For the first solution, you state that this is the case, but also for the second solution, the width is added to the image:

'<img src="' + feature.properties.image + '" style="width: 30vw; height: 20vh; max-width: 250px; max-height: 250px;">'

Adding the width for the image style does work but that is exactly what I would like to avoid. For example, I have some images which are in portrait format and some which are in landscape format, so I would like the width and height to be taken from the picture itself (so just taking the size of the picture as it is, as is happening when I set the width to auto, which would be great if the location of the pop-up container would still work then).

Here is a minimal working example (a file called “my_image.png” is required in the assets folder).

import dash_leaflet as dl
import dash_leaflet.express as dlx
from dash import Dash, html

app = Dash(__name__)

geojson_data = [
    dict(
        lat=40.7128,
        lon=-74.0060,
        popup=f'<img src="/assets/my_image.png">'
    )
]

app.layout = html.Div(
    [
        dl.Map(
            children=[
                dl.TileLayer(),
                dl.GeoJSON(data=dlx.dicts_to_geojson(geojson_data)),
            ],
            style={"width": "100%", "height": "100vh"},
            center=[40.7128, -74.0060],
            zoom=12,
        )
    ]
)

if __name__ == "__main__":
    app.run_server(debug=True, port=3253)

I was hoping there was some easy way to do this while still using the GeoJson component, since normally this setup has been super flexible (something like adding the auto width to.leaflet-popup-content, if that had not ruined the location of the pop up). Any ideas? :slight_smile:

What about trying something like:

'<img src="' + feature.properties.image + '" style="width: feature.properties.image.width; height: feature.properties.image.height; max-width: 250px; max-height: 250px;">'

Another approach that might work but I’m not entirely sure how to set up or if it would be possible without making changes to dash leaflet source code would be using

in place of the image… think geojson has to be html though